diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 9ee00d8d7..0026d97e5 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -103,9 +103,21 @@ jobs: - name: Install dependencies run: npm ci + - name: Vendor bundle freshness (bmad-module yaml) + run: npm run vendor:check + - name: Test agent compilation components run: npm run test:install + - name: Test IDE sync (engine/bundle parity) + run: npm run test:ide-sync + + - name: Test bmad-module skill (source parsing + channels) + run: npm run test:skill-source + + - name: Test bmad-module skill (end-to-end install/update/remove) + run: npm run test:skill + - name: Validate file references run: npm run validate:refs diff --git a/.gitignore b/.gitignore index b903b294a..149cbbe13 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,9 @@ CLAUDE.local.md .analysis/ +# Test fixtures need a committed .mcp.json (the rule above ignores real projects') +!src/core-skills/bmad-module/tests/fixtures/**/.mcp.json + z*/ !docs/zh-cn/ diff --git a/.prettierignore b/.prettierignore index 604b5865f..36dccb69a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,6 +11,9 @@ _bmad*/ # IDE integration folders (user-specific, not in repo) .junie/ +# Generated vendored bundles in the bmad-module skill (not authored source) +src/core-skills/bmad-module/scripts/lib/vendor/** + # Quality scan artifacts produced by bmad-workflow-builder # (per-skill .analysis/ folders contain JSON/HTML reports that should # not block commits with formatting checks) diff --git a/docs/reference/bmad-module-cli.md b/docs/reference/bmad-module-cli.md new file mode 100644 index 000000000..2ce3d1302 --- /dev/null +++ b/docs/reference/bmad-module-cli.md @@ -0,0 +1,123 @@ +--- +title: bmad-module CLI +description: Reference for the bmad-module command — install, update, remove, and list BMad community/custom modules from inside a project. +sidebar: + order: 7 +--- + +`bmad-module` installs, updates, removes, and lists **community and custom BMad modules** in a project that already has BMad installed (a `_bmad/` directory). It is the command behind the `bmad-module` skill, and it mirrors the official installer's behavior for custom modules so a skill-driven install lands the same on-disk state. + +Run it from your project root (the directory containing `_bmad/`), or point it elsewhere with `--project-dir`. + +## Commands + +``` +bmad-module install [--ref ] [--channel ] [--module ] [--set .=] [--dry-run] +bmad-module update [--ref ] [--channel ] [--set .=] +bmad-module remove [--purge] +bmad-module list [--json] +``` + +### install `` + +Resolve a module from ``, copy it into `_bmad//`, generate its config and agent roster, create its declared working directories, and distribute its skills to the coding assistants (IDEs) you chose at `bmad install` time. + +`` may be: + +- A GitHub shorthand — `acme/acme-devlog` +- A full Git URL — `https://github.com/acme/acme-devlog` (optionally with `@ref` or `/tree/`) +- A local path — `./examples/minimal/acme-md-lint` +- A legacy `marketplace.json` repo — `bmad-code-org/bmad-module-game-dev-studio` + +| Flag | Description | +| --- | --- | +| `--ref ` | Clone a specific git tag/branch/commit. Implies `--channel pinned`. | +| `--channel ` | Release channel: `stable`, `next`, or `pinned` (see [Channels](#channels)). | +| `--module ` | Pick one module by `code` when a legacy `marketplace.json` repo resolves to more than one. | +| `--set .=` | Override a module config answer. Repeatable. | +| `--dry-run` | Print the resolved install plan without writing anything. | + +### update `` | `--all` + +Re-resolve an installed module and atomically swap in the new version. Aborts (exit `80`) if you have locally modified tracked files, so your edits are never silently overwritten. + +| Flag | Description | +| --- | --- | +| `--all` | Update every installed community/custom module instead of a single ``. | +| `--ref ` | Update to a specific git ref. | +| `--channel ` | Switch/track a release channel (`stable`, `next`, `pinned`). | +| `--set .=` | Override a module config answer. Repeatable. | + +### remove `` + +Remove an installed module: delete `_bmad//`, prune its entries from the central config and help catalog, and remove its skills from your IDE directories. + +| Flag | Description | +| --- | --- | +| `--purge` | Also delete the module's user customizations under `_bmad/custom/` (e.g. `_bmad/custom/.toml`). Without `--purge`, customizations are left intact. | + +### list + +List installed community/custom modules with their code, version, channel, and source. + +| Flag | Description | +| --- | --- | +| `--json` | Emit machine-readable JSON instead of a formatted table. | + +## Global flags + +| Flag | Description | +| --- | --- | +| `--project-dir ` | Project root containing `_bmad/` (default: current directory). | + +## Channels + +`bmad-module` mirrors the official installer's channel semantics: + +- **`stable`** — the latest non-prerelease GitHub release tag. Falls back to `next` (with a note) when the repo has no tags, isn't a GitHub repo, or the tags API is unreachable. +- **`next`** — the repository's default branch (`main`). +- **`pinned`** — the exact `--ref` you supply (or an `@ref` / `/tree/` parsed from the source). `--channel pinned` without a `--ref` falls back to `next`. + +An explicit `--ref` (or an `@ref` in the source) implies `pinned`. An unknown `--channel` value is rejected with a usage error rather than silently tracking `next`. + +## Examples + +```bash +# Install from GitHub shorthand +bmad-module install acme/acme-devlog + +# Install a local module +bmad-module install ./examples/minimal/acme-md-lint + +# Pin to a release tag +bmad-module install https://github.com/acme/acme-devlog --ref v0.4.0 + +# Install a legacy marketplace.json module +bmad-module install bmad-code-org/bmad-module-game-dev-studio + +# Override a config answer at install time +bmad-module install acme/acme-devlog --set devlog.author="Ada Lovelace" + +# List, update, and remove +bmad-module list +bmad-module update devlog +bmad-module update --all +bmad-module remove mdlint --purge +``` + +## Exit codes + +| Code | Meaning | +| --- | --- | +| `0` | Success | +| `2` | Usage error (bad arguments, unknown flag/channel) | +| `5` | Skill runtime files missing/corrupt — reinstall the skill | +| `10` | No `_bmad/` directory in the project | +| `20` | Missing or invalid `plugin.json` | +| `21` | Reserved `bmad.code` | +| `30` | Prefix collision with an existing module | +| `50` | Filesystem commit failed | +| `60` | Network / git clone failed | +| `70` | Path traversal detected in a manifest path | +| `80` | Update aborted: locally modified files | +| `90` | No such installed module | diff --git a/eslint.config.mjs b/eslint.config.mjs index 1bf3e270e..b37675be2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,9 @@ export default [ 'dist/**', 'coverage/**', '**/*.min.js', + // Generated, self-contained vendored bundles shipped with the bmad-module + // skill (regenerated by its build-vendor.mjs) — not authored source. + 'src/core-skills/bmad-module/scripts/lib/vendor/**', 'test/template-test-generator/**', 'test/fixtures/**', '_bmad*/**', @@ -117,6 +120,29 @@ export default [ }, }, + // bmad-module core skill: self-contained ESM CLI support scripts. + // Same internal-script relaxations as tools/** and src/scripts/** above, + // plus a few cosmetic rules. The code is reviewed and integration-tested + // as-is (the exit-code contract relies on process.exit). + { + files: ['src/core-skills/bmad-module/scripts/**/*.mjs', 'src/core-skills/bmad-module/scripts/**/*.js'], + rules: { + 'n/hashbang': 'off', + 'n/no-process-exit': 'off', + 'unicorn/no-process-exit': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'no-unused-vars': 'off', + 'unicorn/no-array-reduce': 'off', + 'unicorn/no-array-callback-reference': 'off', + 'unicorn/no-array-for-each': 'off', + 'unicorn/catch-error-name': 'off', + 'unicorn/switch-case-braces': 'off', + 'unicorn/explicit-length-check': 'off', + 'unicorn/prefer-string-replace-all': 'off', + 'unicorn/prefer-string-raw': 'off', + }, + }, + // ESLint config file should not be checked for publish-related Node rules { files: ['eslint.config.mjs'], diff --git a/package.json b/package.json index 505c6e8e0..a633bf9ba 100644 --- a/package.json +++ b/package.json @@ -40,15 +40,20 @@ "lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix", "lint:md": "markdownlint-cli2 \"**/*.md\"", "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0", - "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:urls && npm run validate:refs && npm run validate:skills && npm run docs:validate-sidebar", + "quality": "npm run vendor:check && npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:ide-sync && npm run test:urls && npm run test:skill-source && npm run test:skill && npm run validate:refs && npm run validate:skills && npm run docs:validate-sidebar", "rebundle": "node tools/installer/bundlers/bundle-web.js rebundle", - "test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:channels && npm run lint && npm run lint:md && npm run format:check", + "test": "npm run vendor:check && npm run test:refs && npm run test:install && npm run test:ide-sync && npm run test:urls && npm run test:channels && npm run test:skill-source && npm run test:skill && npm run lint && npm run lint:md && npm run format:check", "test:channels": "node test/test-installer-channels.js", + "test:ide-sync": "node test/test-ide-sync.js", "test:install": "node test/test-installation-components.js", "test:refs": "node test/test-file-refs-csv.js", + "test:skill": "bash src/core-skills/bmad-module/tests/integration.test.sh", + "test:skill-source": "node test/test-bmad-module-source.mjs", "test:urls": "node test/test-parse-source-urls.js", "validate:refs": "node tools/validate-file-refs.js --strict", - "validate:skills": "node tools/validate-skills.js --strict" + "validate:skills": "node tools/validate-skills.js --strict", + "vendor:build": "node src/core-skills/bmad-module/scripts/lib/vendor/build-vendor.mjs && node src/core-skills/bmad-module/scripts/lib/vendor/build-ide-sync.mjs", + "vendor:check": "node src/core-skills/bmad-module/scripts/lib/vendor/build-vendor.mjs --check && node src/core-skills/bmad-module/scripts/lib/vendor/build-ide-sync.mjs --check" }, "lint-staged": { "*.{js,cjs,mjs}": [ diff --git a/src/core-skills/bmad-module/README.md b/src/core-skills/bmad-module/README.md new file mode 100644 index 000000000..f31915eb6 --- /dev/null +++ b/src/core-skills/bmad-module/README.md @@ -0,0 +1,50 @@ +# bmad-module + +The core BMAD skill for installing, updating, removing, and listing community BMAD modules. Modules are standalone GitHub repos that conform to the BMAD Module Manifest Spec. + +## How it fits + +- **Authors** publish a single repo with `.claude-plugin/plugin.json` that works in both Claude Code's plugin marketplace and BMAD-METHOD. +- **Legacy modules** (a `.claude-plugin/marketplace.json` + `module.yaml`, the pre-`plugin.json` format) also install: `install` resolves a legacy repo into a synthetic manifest and runs it through the same pipeline. See `lib/legacy-resolver.mjs`, a self-contained port of the full installer's `PluginResolver` strategies. +- **Users** install via this skill — no CLI required. Modules are staged under `_bmad//`, then their skills are distributed to the coding assistants the user chose at `bmad install` time (the `ides:` list in `_bmad/_config/manifest.yaml`), exactly like official modules. +- **BMAD-METHOD** treats community-installed modules as a new `source: 'community'` row in `manifest.yaml`; re-running `bmad install` preserves them (`manifest-generator.js` carries `source: 'community'` rows through regeneration). + +## Verbs + +``` +bmad-module install [--ref ] [--channel ] [--module ] [--dry-run] +bmad-module update [--ref ] [--channel ] +bmad-module remove [--purge] +bmad-module list [--json] +``` + +`` accepts `owner/repo`, a full git URL (`https://…`, `git@…`, `ssh://`, `git://`), or a local path. A git source may carry an `@` suffix and may be a browser-style deep link (`/tree|blob/[/]`, GitLab `/-/tree/…`, Gitea `/src/branch/…`, or `?path=`); `parseSource` in `lib/source.mjs` extracts the embedded ref and a repo subdirectory, mirroring the installer's `custom-module-manager.js`. + +## Behavior notes + +- **Source resolution & caching.** Git sources are cloned into a shared cache at `~/.bmad/cache/custom-modules////` (with `.bmad-source.json` / `.bmad-channel.json` metadata), the same cache the full installer uses; a matching ref is reused, otherwise the clone is fetched/refreshed, and a fetch failure keeps the stale copy so installs work offline. The install then copies the module root (the subdir, if the URL named one) out of the cache into a throwaway temp tree to stage from — the cache is never mutated. Local sources are copied straight to the temp tree. See `lib/cache.mjs`. +- **Channels.** `lib/channel-resolver.mjs` (a `node:`-only port of the installer's `channel-resolver.js`) resolves `--channel`: `pinned` → an explicit `--ref`/`@ref`; `stable` → the latest non-prerelease GitHub release tag (falls back to `next` when there are no tags, the URL isn't GitHub, or the tags API is unreachable); `next` (the default for a bare git source) → the default branch. `update` re-resolves the channel the module was installed with. +- **Source of truth** for what was installed is `_bmad/_config/files-manifest.csv` (per-file hashes) and `_bmad/_config/skill-manifest.csv` (one row per shipped skill). `manifest.yaml` carries the source/version/sha tuple. +- **`update`** refuses to overwrite locally-modified files (hash mismatch against the recorded hash). Move overrides into `_bmad/custom//` and retry. +- **`remove`** without `--purge` preserves `_bmad/custom//` so a re-install picks the customizations back up. `--purge` deletes them. Remove also prunes the module's skills from every configured IDE. +- **IDE distribution** runs after every install/update/remove via a self-contained bundle of BMAD's real IDE engine, shipped at `lib/vendor/ide-sync.mjs` (built from `tools/installer/ide/*` by `lib/vendor/build-ide-sync.mjs`, gated by `vendor:check`). The skill execs it locally — no npx, no network. The same engine also backs the `bmad ide-sync` CLI command and the full installer's IDE setup, so all three stay in lockstep. If the bundle is unreachable on an older install, the skill says so and points the user at `bmad ide-sync`. +- **Hooks / MCP / LSP / Claude subagents** declared in the module manifest are _copied_ but NOT auto-activated by this skill (they are Claude Code plugin surfaces, not skills). Use Claude Code's plugin manager to wire them up. +- **Legacy resolution** keys off the absence of a `plugin.json#bmad`: if `marketplace.json` is present, the skill resolves the module via `module.yaml` (or synthesizes one from SKILL.md frontmatter when none exists). A repo defining more than one module exits 20 with the available codes; re-run with `--module `. The reserved-code guard (exit 21) is relaxed on the legacy path so first-party modules (`gds`, `bmm`, …) install; current-spec `plugin.json` authors still get exit 21. + +## Implementation + +The skill itself is a thin verb router (`SKILL.md`). `scripts/bmad-module.mjs` is a zero-import launcher that guards the import graph (a missing/corrupt runtime file becomes a documented exit code, not a raw stack trace); the verb dispatcher lives in `scripts/cli.mjs` and all filesystem work happens in the `lib/` modules. These carry **no registry dependencies** — important because the installer copies the skill into the IDE skills directories (e.g. `.claude/skills/bmad-module/`) without `node_modules` and never runs `npm install` there: + +- `manifest.yaml` is read/written with a **vendored copy of the real `yaml` library** (`lib/vendor/yaml.mjs`, regenerated by `lib/vendor/build-vendor.mjs`) so it stays byte-identical to BMAD core's writer. +- `semver` validity/range checks **and** the version comparison the stable-channel resolver needs (`prerelease`, `compare`, `rcompare`) use a small `node:`-only helper (`lib/semver-lite.mjs`) instead of the `semver` package. +- The shared clone cache (`lib/cache.mjs`) and channel resolution (`lib/channel-resolver.mjs`) use only `node:child_process` / `node:https` so the skill needs no dependencies after distribution. + +## Exit codes + +See `SKILL.md` for the full table. The script's stderr always names the condition; the codes are stable so tooling can branch. + +## Tests + +Integration tests live in `tests/integration.test.sh` and run end-to-end on a fresh BMAD install. Fixtures for negative cases (collisions, path traversal, reserved codes) are under `tests/fixtures/`; legacy-format fixtures (strategy-1 module files, a reserved code, and the synthesize fallback) are under `tests/fixtures/examples/legacy/`. + +The pure (no-network) install plumbing — `parseSource` URL/`@ref`/subdir parsing, `semver-lite`, and the `channel-resolver` helpers — is unit-tested in `test/test-bmad-module-source.mjs` (run via `npm run test:skill-source`, included in `npm test`), kept at parity with the installer's `test/test-parse-source-urls.js` and `test/test-installer-channels.js`. diff --git a/src/core-skills/bmad-module/SKILL.md b/src/core-skills/bmad-module/SKILL.md new file mode 100644 index 000000000..ac1c352cf --- /dev/null +++ b/src/core-skills/bmad-module/SKILL.md @@ -0,0 +1,104 @@ +--- +name: bmad-module +description: Install, update, remove, or list community BMAD modules. Use when the user says "install module ", "install bmad module", "update module", "remove module", "uninstall module", or "list modules". +--- + +# bmad-module + +Manage community BMAD modules — installable packages of skills, agents, and supporting assets that ship as standalone GitHub repos. Both module formats install: the current spec (a `.claude-plugin/plugin.json` with a `bmad{}` block) and the **legacy** format (a `.claude-plugin/marketplace.json` + `module.yaml`, e.g. `bmad-code-org/bmad-module-game-dev-studio`) — the script resolves a legacy repo into the same on-disk layout automatically. Modules are staged under `_bmad//` and tracked in the existing manifests. On `install`, `update`, and `remove`, the script distributes (or prunes) the module's skills to **every coding assistant the user selected at `bmad install`** — read from the `ides:` list in `_bmad/_config/manifest.yaml` — so the module lands in Claude Code, Cursor, Copilot, etc. The canonical end state is skills living in the IDE directories (e.g. `.claude/skills//`), not in `_bmad/`. The same artifact also loads as a Claude Code plugin via its `.claude-plugin/plugin.json` manifest. + +The script also completes the install in place, best-effort: it runs `npm install` when the module ships a `package.json` (skip with `bmad.install.skipNpm: true`), generates the module's `[modules.]` / `[agents.]` config blocks from its `module.yaml` (overridable with `--set`), creates the working directories it declares under `directories:`, and rebuilds `_bmad/_config/bmad-help.csv` so its skills appear in `bmad-help`. A failure in any of these is reported as a warning, not a failed install. Interactive config refinement remains the job of the module's `postInstallSkill`, if it declares one. + +## CRITICAL RULES + +- NEVER write directly to files under `_bmad/` or into IDE directories (`.claude/skills/`, `.agents/skills/`, etc.). All filesystem changes go through the Node script at `scripts/bmad-module.mjs` — it handles staging, atomic swaps, manifest updates, IDE distribution, and rollback on failure. +- HALT and report cleanly if `_bmad/` is not present in the current working directory (exit code 10 from the script). +- DO NOT execute hooks, MCP server commands, or any code shipped inside the module during install. The install copies files; activation is a separate step the user opts into via Claude Code's plugin manager. +- If the script exits non-zero, report the exit code and stderr verbatim and stop. Do NOT retry, do NOT try a different verb. The one exception is exit code 5 (the skill's own bundled runtime files are missing/corrupt): that's a fixable setup/packaging problem, not a module rejection — relay the script's "reinstall the skill" guidance instead of reporting a failed install. + +## EXECUTION + +### Step 1 — Identify the verb + +The user's request maps to exactly one of: + +| Verb | Phrasing | +| --------- | ---------------------------------------------------------------------- | +| `install` | "install module X", "add the X module", "set up X" | +| `update` | "update module X", "upgrade X", "pull the latest X" | +| `remove` | "remove module X", "uninstall X", "delete X module" | +| `list` | "list modules", "what modules are installed", "show installed modules" | + +If the verb is ambiguous (e.g. the user says "manage modules"), ASK which verb they want before continuing. + +### Step 2 — Parse the args + +- **install:** the user supplies `` — `owner/repo` (GitHub short), a full git URL (`https://…` or `git@…`), or a local path. A source may carry an `@` suffix (`owner/repo@v1.2.3`) and a git URL may be a browser-style deep link (`https://github.com/owner/repo/tree//`, GitLab `/-/tree/…`, Gitea `/src/branch/…`, or `?path=`): the script extracts the ref and a repo subdirectory automatically, so a module living in a monorepo subfolder installs directly. Optional flags: `--ref `, `--channel `, `--set .=` (override a module config answer; repeatable), `--module `, `--dry-run`. Channels: `pinned` clones an explicit `--ref`/`@ref`; `stable` resolves the latest non-prerelease GitHub release tag (falls back to the default branch when there are no tags / the host isn't GitHub / the tags API is unreachable); `next` (the default for a bare git source) tracks the default branch. Use `--module ` only when a legacy marketplace.json repo defines more than one module: the script exits 20 listing the available codes, then re-run picking one. First-party legacy modules whose codes are reserved (`gds`, `bmm`, …) install on the legacy path; the same reserved code in a current-spec `plugin.json` is still rejected (exit 21). +- **update:** the user supplies `` (the `_bmad//` folder name) or asks for "all"; in that case use `--all`. Optional `--ref`, `--channel `, `--set .=`. Without overrides, update re-resolves the channel the module was installed with — a `stable` module moves to the latest release tag, a `pinned` module stays put unless `--ref` moves it, and a `next` module re-pulls the default branch. +- **remove:** the user supplies ``. Use `--purge` only if they explicitly say "also remove customizations" or "purge". +- **list:** no args. Use `--json` if the user asks for machine-readable. + +If anything is missing or ambiguous, ASK before invoking. + +### Step 3 — Confirm before destructive verbs + +For `install`, `update`, and `remove`, summarize what will happen and confirm once with the user: + +> About to **install** `acme/acme-devlog` (will create `_bmad/devlog/`). +> Proceed? [y/N] + +For `install` you may run a dry-run first (`--dry-run`) and show the file plan; that counts as the summary — still confirm before the real run. + +Skip the confirmation step only if the user has already pre-authorized in this turn (e.g. "go ahead and install acme-md-lint without asking"). + +### Step 4 — Invoke the Node script + +Run from the project root (the dir containing `_bmad/`): + +``` +node /scripts/bmad-module.mjs [args...] +``` + +`` is this skill's own directory: the script ships alongside this `SKILL.md`, so resolve it relative to this file rather than assuming a fixed path — `/scripts/bmad-module.mjs` (e.g. `.claude/skills/bmad-module/scripts/bmad-module.mjs` once distributed, or `src/core-skills/bmad-module/scripts/bmad-module.mjs` during development in this repo). If the script isn't found next to this `SKILL.md`, the skill's bundled runtime is missing — that's the exit-code-5 case (see CRITICAL RULES and EXIT CODES): relay the "reinstall the skill" guidance rather than guessing another location. + +Stream stdout and stderr verbatim. Do NOT silence or rewrite them — the script's own messages are designed for end-user consumption. + +### Step 5 — Report + +On exit 0: paraphrase the script's final line(s) and note any next-step hint (e.g. "next: run the `bmad-devlog-setup` skill to finish setup"). The script also prints `[ide-sync]` lines naming each coding assistant the skills were synced to — relay them so the user knows where the module landed. + +Note two non-fatal cases the script reports on exit 0: + +- If the script prints `[bmad-module] note: no coding assistants are configured…`, the module is staged under `_bmad/` but no IDEs were selected at `bmad install` time — tell the user to run `bmad install` to choose their assistants. +- If it prints `[bmad-module] warning:` about IDE distribution, the module installed fine but skills may not have reached every assistant — relay the script's suggestion to run `bmad ide-sync`. Do NOT treat this as a failed install. + +On non-zero exit: print the exit code, the stderr message, and stop. Do not suggest workarounds beyond what the script's message itself suggests (e.g. "use `update` instead", "move changes into `_bmad/custom//`"). + +## EXIT CODES + +The script's stderr always names the condition, so for most non-zero exits you just relay it (see CRITICAL RULES). These few change what you tell the user next: + +| Code | Meaning | What to tell the user | +| ---- | ------------------------------------------------------------ | ------------------------------------------------------ | +| 5 | skill runtime files missing/corrupt — NOT a module rejection | reinstall the skill (relay the script's guidance) | +| 10 | no `_bmad/` directory in project | run `bmad install` first | +| 80 | update aborted: locally modified files would be overwritten | move overrides into `_bmad/custom//`, then retry | +| 90 | no such installed module (for `update`/`remove`) | check the code, or run `list` to see what's installed | + +Any other non-zero exit: report the code and stderr verbatim and stop — stderr names the condition. For the full list of codes, run the script with `--help`. + +## EXAMPLES + +User: "Install the devlog module from acme/acme-devlog" → Confirm, then run: `node …/scripts/bmad-module.mjs install acme/acme-devlog` + +User: "Try installing examples/minimal/acme-md-lint first as a dry-run" → Run with `--dry-run`, show the plan, then ask whether to proceed for real. + +User: "What modules do I have installed?" → Run `… list`. No confirmation needed (read-only). + +User: "Update the devlog module to v0.5.0" → Confirm, then run `… update devlog --ref v0.5.0`. + +User: "Install the studio module on the stable channel" → Confirm, then run `… install acme/acme-studio --channel stable` (resolves the latest release tag). + +User: "Install the linter that lives in the tools/ folder of this monorepo" → Confirm, then run `… install https://github.com/acme/monorepo/tree/main/tools/linter` (ref + subdir are parsed from the URL). + +User: "Remove the mdlint module and wipe its customizations too" → Confirm, then run `… remove mdlint --purge`. diff --git a/src/core-skills/bmad-module/scripts/bmad-module.mjs b/src/core-skills/bmad-module/scripts/bmad-module.mjs new file mode 100755 index 000000000..dea54f16e --- /dev/null +++ b/src/core-skills/bmad-module/scripts/bmad-module.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// bmad-module — thin launcher (entry point). +// +// This file has NO imports on purpose. It must ALWAYS load, so that a broken or +// incomplete skill copy — e.g. a missing lib/vendor/yaml.mjs after a partial +// install — is reported as a DOCUMENTED exit code with actionable guidance, +// instead of crashing at module-load with a raw ESM resolver stack trace and a +// bare exit 1 (which is not in the exit-code table). The real CLI is cli.mjs. +// +// TOOLING must equal EXIT.TOOLING in ./lib/exit.mjs. It is duplicated here as a +// literal so this guard depends on nothing and can never itself fail to load. +const TOOLING = 5; + +try { + // A failure resolving/evaluating this import graph means a runtime asset is + // missing or corrupt. Runtime and usage errors are handled INSIDE cli.mjs + // (which maps them to their own structured exit codes and calls process.exit + // before returning), so they never reach the catch below. + const { main } = await import('./cli.mjs'); + await main(); +} catch (err) { + const code = err && err.code; + const isLoadError = + err instanceof SyntaxError || + code === 'ERR_MODULE_NOT_FOUND' || + code === 'ERR_UNSUPPORTED_DIR_IMPORT' || + code === 'ERR_UNKNOWN_FILE_EXTENSION' || + code === 'ERR_DLOPEN_FAILED' || + code === 'ERR_REQUIRE_ESM'; + if (isLoadError) { + process.stderr.write( + `[bmad-module] the skill's bundled runtime files are missing or corrupt ` + + `(${code || err.name}: ${err.message}).\n` + + `[bmad-module] this is a setup/packaging problem, not a module-rejection ` + + `decision — do not treat it as a failed install of the target module.\n` + + `[bmad-module] fix: reinstall the skill so its scripts/ tree (including ` + + `scripts/lib/vendor/) is complete — re-run \`npx bmad-method install\`, ` + + `or re-copy the bmad-module skill folder in full.\n`, + ); + process.exit(TOOLING); + } + // Anything else escaping cli.mjs is a genuine, unexpected bug. + process.stderr.write(`[bmad-module] unexpected error: ${err && (err.stack || err.message)}\n`); + process.exit(1); +} diff --git a/src/core-skills/bmad-module/scripts/cli.mjs b/src/core-skills/bmad-module/scripts/cli.mjs new file mode 100644 index 000000000..eeac451ce --- /dev/null +++ b/src/core-skills/bmad-module/scripts/cli.mjs @@ -0,0 +1,196 @@ +// bmad-module CLI — verb dispatcher. Loaded by the thin launcher in +// bmad-module.mjs, which guards this whole import graph: if any runtime asset +// (e.g. lib/vendor/yaml.mjs) is missing, the launcher reports a documented +// setup error instead of leaking a raw ESM stack trace. See bmad-module.mjs. +// +// Usage: +// node bmad-module.mjs install [--ref ] [--channel ] [--module ] [--dry-run] [--project-dir

] +// node bmad-module.mjs update [--ref ] [--channel ] [--project-dir

] +// node bmad-module.mjs remove [--purge] [--project-dir

] +// node bmad-module.mjs list [--json] [--project-dir

] +// +// Exit codes — see SKILL.md / lib/exit.mjs. 0 = ok; everything ≥5 = structured error. + +import { runInstall } from './install.mjs'; +import { runUpdate } from './update.mjs'; +import { runRemove } from './remove.mjs'; +import { runList } from './list.mjs'; +import { EXIT, BmadModuleError } from './lib/exit.mjs'; + +const VERBS = new Set(['install', 'update', 'remove', 'list']); +const BOOLEAN_FLAGS = new Set(['dry-run', 'purge', 'all', 'json']); +const VALUE_FLAGS = new Set(['ref', 'channel', 'module', 'set', 'project-dir']); + +function parseArgs(argv) { + const out = { _: [], flags: {} }; + let i = 0; + while (i < argv.length) { + const a = argv[i]; + if (a.startsWith('--')) { + const key = a.slice(2); + // Reject unknown flags so typos fail fast instead of silently running + // with defaults. + if (!BOOLEAN_FLAGS.has(key) && !VALUE_FLAGS.has(key)) { + throw new BmadModuleError(EXIT.USAGE, `unknown flag --${key}`); + } + // boolean flags + if (BOOLEAN_FLAGS.has(key)) { + out.flags[key] = true; + i++; + continue; + } + // value flags + const val = argv[i + 1]; + if (val === undefined || val.startsWith('--')) { + throw new BmadModuleError(EXIT.USAGE, `flag --${key} requires a value`); + } + // --set is repeatable; collect into an array. All other flags take the + // last value seen. + if (key === 'set') { + (out.flags.set ||= []).push(val); + } else { + out.flags[key] = val; + } + i += 2; + continue; + } + out._.push(a); + i++; + } + return out; +} + +export async function main() { + const argv = process.argv.slice(2); + if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') { + printUsage(); + process.exit(EXIT.USAGE); + } + const verb = argv[0]; + if (!VERBS.has(verb)) { + process.stderr.write(`[bmad-module] unknown verb "${verb}". Valid: install, update, remove, list.\n`); + process.exit(EXIT.USAGE); + } + + let parsed; + try { + parsed = parseArgs(argv.slice(1)); + } catch (e) { + if (e instanceof BmadModuleError) { + process.stderr.write(`[bmad-module] ${e.message}\n`); + process.exit(e.code); + } + throw e; + } + + const projectDir = parsed.flags['project-dir'] || process.cwd(); + const setOverrides = parseSetOverrides(parsed.flags.set); + + try { + switch (verb) { + case 'install': + await runInstall({ + source: parsed._[0], + ref: parsed.flags['ref'] || null, + channel: parsed.flags['channel'] || null, + module: parsed.flags['module'] || null, + dryRun: !!parsed.flags['dry-run'], + setOverrides, + projectDir, + }); + break; + case 'update': + await runUpdate({ + code: parsed._[0] || null, + all: !!parsed.flags['all'], + ref: parsed.flags['ref'] || null, + channel: parsed.flags['channel'] || null, + setOverrides, + projectDir, + }); + break; + case 'remove': + await runRemove({ + code: parsed._[0], + purge: !!parsed.flags['purge'], + projectDir, + }); + break; + case 'list': + await runList({ + json: !!parsed.flags['json'], + projectDir, + }); + break; + } + } catch (e) { + if (e instanceof BmadModuleError) { + process.stderr.write(`[bmad-module] ${e.message}\n`); + process.exit(e.code); + } + process.stderr.write(`[bmad-module] unexpected error: ${e.stack || e.message}\n`); + process.exit(1); + } +} + +// Parse repeatable `--set .=` flags into a nested map +// { [code]: { [key]: value } }. Mirrors the full installer's --set spec. +function parseSetOverrides(rawList) { + const out = {}; + if (!Array.isArray(rawList)) return out; + for (const spec of rawList) { + const eq = spec.indexOf('='); + if (eq === -1) throw new BmadModuleError(EXIT.USAGE, `--set expects .=, got "${spec}"`); + const lhs = spec.slice(0, eq); + const value = spec.slice(eq + 1); + const dot = lhs.indexOf('.'); + if (dot === -1) throw new BmadModuleError(EXIT.USAGE, `--set expects .=, got "${spec}"`); + const code = lhs.slice(0, dot); + const key = lhs.slice(dot + 1); + if (!code || !key) throw new BmadModuleError(EXIT.USAGE, `--set expects .=, got "${spec}"`); + (out[code] ||= {})[key] = value; + } + return out; +} + +function printUsage() { + process.stderr.write(`bmad-module — install, update, remove, or list BMAD community modules. + +USAGE + bmad-module install [--ref ] [--channel ] [--module ] [--set .=] [--dry-run] + bmad-module update [--ref ] [--channel ] [--set .=] + bmad-module remove [--purge] + bmad-module list [--json] + +INSTALL FLAGS + --module Pick one module by code when a legacy marketplace.json + repo resolves to more than one + +GLOBAL FLAGS + --project-dir Project root containing _bmad/ (default: cwd) + --set .= Override a module config answer (repeatable) + +EXAMPLES + bmad-module install acme/acme-devlog + bmad-module install ./examples/minimal/acme-md-lint + bmad-module install https://github.com/acme/acme-devlog --ref v0.4.0 + bmad-module install bmad-code-org/bmad-module-game-dev-studio # legacy module + bmad-module list + bmad-module update devlog + bmad-module remove mdlint --purge + +EXIT CODES + 0 success + 2 usage error + 5 skill runtime files missing/corrupt — reinstall the skill + 10 no _bmad/ in project + 20 missing or invalid plugin.json + 21 reserved bmad.code + 30 prefix collision with existing module + 50 filesystem commit failed + 60 network/git clone failed + 70 path traversal in manifest + 80 update aborted: locally modified files + 90 no such installed module +`); +} diff --git a/src/core-skills/bmad-module/scripts/install.mjs b/src/core-skills/bmad-module/scripts/install.mjs new file mode 100644 index 000000000..82570d46b --- /dev/null +++ b/src/core-skills/bmad-module/scripts/install.mjs @@ -0,0 +1,299 @@ +import path from 'node:path'; +import { EXIT, BmadModuleError } from './lib/exit.mjs'; +import { findBmadDir, ensureConfigDir } from './lib/bmad-dir.mjs'; +import fsp from 'node:fs/promises'; +import { parseSource, materializeSource } from './lib/source.mjs'; +import { resolveChannel } from './lib/channel-resolver.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 { stageCopyPlan, atomicSwapDir } from './lib/fs-safe.mjs'; +import { readManifestYaml, addModuleToManifest, appendSkillManifestRows, appendFilesManifestRows } from './lib/manifest-ops.mjs'; +import { distributeToIdes } from './lib/ide-sync.mjs'; +import { installModuleDeps } from './lib/npm-deps.mjs'; +import { regenerateCentralConfig, readModuleConfigValues, resolveSectionKey } from './lib/config-gen.mjs'; +import { createModuleDirectories } from './lib/module-dirs.mjs'; +import { regenerateHelpCatalog } from './lib/help-catalog.mjs'; + +// Run the install verb. `opts` shape: +// { source, ref, sha, channel, dryRun, module, setOverrides, projectDir } +// `module` selects one module by code when a legacy marketplace.json resolves to +// more than one. Returns nothing; throws BmadModuleError on failure. +export async function runInstall(opts) { + const projectDir = opts.projectDir || process.cwd(); + + // §1. Resolve _bmad/ first — fail fast if BMAD is not installed. + const bmadDir = await findBmadDir(projectDir); + if (!bmadDir) { + throw new BmadModuleError(EXIT.NO_BMAD_DIR, `no _bmad/ found in ${projectDir}. Run \`bmad install\` first.`); + } + await ensureConfigDir(bmadDir); + + // §2. Normalize source, resolve the channel/ref to a concrete clone target, + // then materialize (clone into the shared cache and copy out a working tree). + const descriptor = parseSource(opts.source); + const target = await resolveCloneTarget(descriptor, opts); + const materialized = await materializeSource(descriptor, { ref: target.ref }); + + try { + // §3. Read + validate the manifest. New-spec modules carry a + // `.claude-plugin/plugin.json#bmad`; legacy modules carry a + // `.claude-plugin/marketplace.json` + module.yaml, which we resolve into a + // synthetic manifest of the same shape. + let manifest; + let synthesized = null; + if (await hasBmadPluginJson(materialized.dir)) { + manifest = await readAndValidateManifest(materialized.dir); + } else { + const legacy = await resolveLegacyModule(materialized.dir, { selector: opts.module || null }); + 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; + process.stdout.write(`[bmad-module] resolved legacy module ${manifest.bmad.code} from marketplace.json\n`); + } + const code = manifest.bmad.code; + + // §4. Collision check against installed manifest. + const existing = await readManifestYaml(bmadDir); + const existingEntry = existing?.modules?.find((m) => m && m.name === code); + if (existingEntry) { + const sameSource = + (existingEntry.rawSource && existingEntry.rawSource === descriptor.rawInput) || + (existingEntry.repoUrl && descriptor.kind === 'git' && existingEntry.repoUrl === descriptor.url); + const sameSha = materialized.sha && existingEntry.sha === materialized.sha; + if (sameSource && sameSha) { + process.stdout.write(`[bmad-module] ${code} ${existingEntry.version} already installed at this sha — no-op.\n`); + return; + } + if (existingEntry.source === 'community' && sameSource) { + // Same module, different sha — user should use `update`. + throw new BmadModuleError( + EXIT.PREFIX_COLLISION, + `${code} already installed from this source at sha ${existingEntry.sha || '?'}. ` + + `Run \`bmad-module update ${code}\` to change version.`, + ); + } + throw new BmadModuleError( + EXIT.PREFIX_COLLISION, + `code "${code}" already used by ${existingEntry.source} module ` + + `${existingEntry.repoUrl || existingEntry.rawSource || existingEntry.npmPackage || '(local)'}. ` + + `Module authors should pick a unique bmad.code.`, + ); + } + + // 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 (the + // synthetic manifest already points moduleDefinition/moduleHelpCsv at them). + 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'); + } + } + + // §5. Build install plan. + validateDeclaredPaths(materialized.dir, manifest); + const userIgnores = await readUserIgnores(materialized.dir, manifest); + const matchIgnore = buildIgnoreMatcher(userIgnores); + const { plan, skillDestDirs } = await buildCopyPlan(materialized.dir, manifest, matchIgnore); + const rewrittenManifestJson = rewriteManifestPaths(manifest); + + if (opts.dryRun) { + process.stdout.write(`[bmad-module] dry-run: would install ${code} (${manifest.name} ${manifest.version})\n`); + process.stdout.write(`[bmad-module] target: ${path.join(bmadDir, code)}\n`); + process.stdout.write(`[bmad-module] files (${plan.length + 1}):\n`); + process.stdout.write(` .claude-plugin/plugin.json (rewritten to canonical paths)\n`); + for (const { srcRel, destRel } of plan) { + process.stdout.write(srcRel === destRel ? ` ${destRel}\n` : ` ${destRel} (from ${srcRel})\n`); + } + return; + } + + // §6. Stage to tmp/staged-out, then atomic swap. + const stagedDir = path.join(path.dirname(materialized.dir), 'staged-out'); + await stageCopyPlan(materialized.dir, stagedDir, plan, { + '.claude-plugin/plugin.json': rewrittenManifestJson, + }); + const targetDir = path.join(bmadDir, code); + try { + await atomicSwapDir(stagedDir, targetDir); + } catch (e) { + throw new BmadModuleError(EXIT.COMMIT_FAILURE, `failed to swap into ${targetDir}: ${e.message}`); + } + + // §7. Register in manifests. + await addModuleToManifest(bmadDir, code, { + // Git installs record the resolved channel version (tag for stable/pinned, + // 'main' for next) like the full installer; local installs keep the + // module's declared version since there is no clone ref. + version: descriptor.kind === 'git' ? target.version : manifest.bmad.moduleVersion || manifest.version, + repoUrl: descriptor.kind === 'git' ? descriptor.url : null, + sha: materialized.sha, + ref: materialized.ref, + channel: target.channel, + rawSource: descriptor.rawInput, + moduleName: manifest.name, + }); + + const destPaths = ['.claude-plugin/plugin.json', ...plan.map((p) => p.destRel)]; + await appendSkillManifestRows(bmadDir, code, skillDestDirs); + await appendFilesManifestRows(bmadDir, code, destPaths); + + process.stdout.write( + `[bmad-module] installed ${code} (${manifest.name} ${manifest.version})${materialized.sha ? ` @ ${materialized.sha.slice(0, 7)}` : ''}\n`, + ); + process.stdout.write(`[bmad-module] copied ${destPaths.length} file(s) to ${path.relative(projectDir, targetDir)}\n`); + + // §7.5. Complete the install the way the full installer does for custom + // modules: install JS deps, generate central config + agent roster, create + // declared working directories, and rebuild the merged help catalog. All are + // non-fatal — the module is already committed to _bmad//. + await finishModuleInstall({ bmadDir, code, targetDir, manifest, setOverrides: opts.setOverrides }); + + // §8. Distribute the module's skills to the coding assistants the user chose + // at `bmad install` time (read from _bmad/_config/manifest.yaml). This is the + // same distribution the full installer performs; without it the skills would + // sit in _bmad/ and never reach Claude Code / Cursor / Copilot / etc. + const ideResult = await distributeToIdes({ projectDir, bmadDir }); + if (ideResult.skipped) { + process.stdout.write( + `[bmad-module] note: no coding assistants are configured in _bmad/_config/manifest.yaml — ` + + `skills are in _bmad/${code}/ only. Run \`bmad install\` to choose your IDEs.\n`, + ); + } else if (!ideResult.ok) { + process.stderr.write(`[bmad-module] warning: ${ideResult.hint}\n`); + } + + // §9. Warn about Claude-plugin-only surfaces (not distributed as skills). + const claudeOnly = []; + if (manifest.hooks) claudeOnly.push('hooks'); + if (manifest.mcpServers) claudeOnly.push('mcpServers'); + if (manifest.lspServers) claudeOnly.push('lspServers'); + if (Array.isArray(manifest.agents) && manifest.agents.length) claudeOnly.push('agents'); + if (Array.isArray(manifest.commands) && manifest.commands.length) claudeOnly.push('commands'); + if (claudeOnly.length) { + process.stdout.write( + `[bmad-module] note: ${claudeOnly.join(', ')} are Claude Code plugin surfaces and were copied but ` + + `NOT auto-activated. Use Claude Code's plugin manager to wire them up.\n`, + ); + } + if (manifest.bmad?.install?.postInstallSkill) { + process.stdout.write(`[bmad-module] next: run the \`${manifest.bmad.install.postInstallSkill}\` skill to finish setup.\n`); + } + } finally { + await materialized.cleanup(); + } +} + +// Resolve a parsed source + CLI flags into a concrete clone target: +// { ref, channel, version } +// ref — git ref to clone (null = default branch / local source) +// channel — manifest channel tag: 'stable' | 'pinned' | 'next' | null (local) +// version — manifest version string for git installs (tag for stable/pinned, +// 'main' for next); null for local (caller uses the module version). +// +// Mirrors the installer's channel semantics: an explicit --ref (or an @ref / +// /tree/ parsed from the source) pins; --channel stable resolves the latest +// non-prerelease GitHub tag, falling back to next (with a warning) when there are +// no tags, the URL isn't a GitHub repo, or the tags API is unreachable. +const VALID_CHANNELS = new Set(['stable', 'pinned', 'next']); + +export async function resolveCloneTarget(descriptor, opts) { + // Reject typo'd channels up front (e.g. `--channel stabl`) so they error + // instead of silently falling through the branches below to the `next` default. + if (opts.channel && !VALID_CHANNELS.has(opts.channel)) { + throw new BmadModuleError(EXIT.USAGE, `unknown --channel "${opts.channel}" (expected: stable, pinned, next)`); + } + + if (descriptor.kind !== 'git') { + return { ref: null, channel: null, version: null }; + } + + const explicitRef = opts.ref ?? descriptor.ref ?? null; + let channel = opts.channel || (explicitRef ? 'pinned' : 'next'); + + if (channel === 'pinned') { + if (explicitRef) { + return { ref: explicitRef, channel: 'pinned', version: explicitRef }; + } else { + process.stderr.write(`[bmad-module] warning: --channel pinned needs a --ref; falling back to next.\n`); + channel = 'next'; + } + } + + if (channel === 'stable') { + try { + const r = await resolveChannel({ channel: 'stable', repoUrl: descriptor.url }); + if (r.resolvedFallback) { + process.stderr.write( + `[bmad-module] note: no stable release found for ${descriptor.displayName} (${r.reason}); tracking the default branch.\n`, + ); + return { ref: null, channel: 'next', version: 'main' }; + } + return { ref: r.ref, channel: 'stable', version: r.version }; + } catch (e) { + process.stderr.write(`[bmad-module] warning: could not resolve stable channel (${e.message}); tracking the default branch.\n`); + return { ref: null, channel: 'next', version: 'main' }; + } + } + + // next + return { ref: null, channel: 'next', version: 'main' }; +} + +// Shared post-copy completion for install and update: install JS deps, generate +// the central config + agent roster, create declared working directories, and +// rebuild the merged help catalog. Mirrors what the full installer does for a +// custom module so a skill-driven install lands the same on-disk state. Every +// step is non-fatal — the module files are already committed under _bmad//. +export async function finishModuleInstall({ bmadDir, code, targetDir, manifest, setOverrides }) { + // 1. npm deps (in place — see npm-deps.mjs for the design note). + const dep = await installModuleDeps(targetDir, manifest); + if (dep.ran && dep.ok) process.stdout.write(`[bmad-module] installed npm dependencies for ${code}\n`); + else if (dep.ran && !dep.ok) process.stderr.write(`[bmad-module] warning: npm install failed for ${code}: ${dep.error}\n`); + + // 2. Capture prior config (for directory move-detection on update) before regen. + const sectionKey = await resolveSectionKey(bmadDir, code); + let existingConfig = {}; + try { + existingConfig = await readModuleConfigValues(bmadDir, sectionKey); + } catch { + /* no prior config — fine */ + } + + // 3. Central config + agent roster. + let resolved = { values: {} }; + try { + resolved = await regenerateCentralConfig(bmadDir, code, { setOverrides: setOverrides || {} }); + } catch (e) { + process.stderr.write(`[bmad-module] warning: config generation failed for ${code}: ${e.message}\n`); + } + + // 4. Declared working directories. + try { + const dirs = await createModuleDirectories(bmadDir, code, resolved.values, existingConfig); + const made = dirs.createdDirs.length; + const moved = dirs.movedDirs.length; + if (made) process.stdout.write(`[bmad-module] created ${made} working director${made === 1 ? 'y' : 'ies'} for ${code}\n`); + if (moved) process.stdout.write(`[bmad-module] moved ${moved} working director${moved === 1 ? 'y' : 'ies'} for ${code}\n`); + } catch (e) { + process.stderr.write(`[bmad-module] warning: directory creation failed for ${code}: ${e.message}\n`); + } + + // 5. Merged help catalog. + try { + await regenerateHelpCatalog(bmadDir); + } catch (e) { + process.stderr.write(`[bmad-module] warning: help catalog rebuild failed: ${e.message}\n`); + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/bmad-dir.mjs b/src/core-skills/bmad-module/scripts/lib/bmad-dir.mjs new file mode 100644 index 000000000..708f2f3a9 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/bmad-dir.mjs @@ -0,0 +1,23 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +// Locate the _bmad/ directory for a project. Matches BMAD-METHOD's +// Installer.findBmadDir semantics exactly: no upward search — always +// `/_bmad`. Returns absolute path or null if absent. +export async function findBmadDir(projectDir) { + const candidate = path.join(path.resolve(projectDir), '_bmad'); + try { + const stat = await fs.stat(candidate); + return stat.isDirectory() ? candidate : null; + } catch { + return null; + } +} + +// Resolve a writable _config dir, ensuring it exists. Modules always +// register into /_config/. +export async function ensureConfigDir(bmadDir) { + const cfgDir = path.join(bmadDir, '_config'); + await fs.mkdir(cfgDir, { recursive: true }); + return cfgDir; +} diff --git a/src/core-skills/bmad-module/scripts/lib/cache.mjs b/src/core-skills/bmad-module/scripts/lib/cache.mjs new file mode 100644 index 000000000..e4b3c63ab --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/cache.mjs @@ -0,0 +1,161 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import os from 'node:os'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { EXIT, BmadModuleError } from './exit.mjs'; + +const execFileP = promisify(execFile); +const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' }; + +// Shared clone cache for community modules — mirrors +// tools/installer/modules/custom-module-manager.js (getCacheDir + cloneRepo) so +// a skill-driven install reuses the same on-disk cache the CLI installer +// maintains. node:-only (execFile, not execSync+fs-extra); npm deps are NOT +// installed here — the skill installs them in _bmad// after the copy. + +export function getCacheDir() { + return path.join(os.homedir(), '.bmad', 'cache', 'custom-modules'); +} + +// A ref must be a tag/branch name git can take as a positional argument. Reject +// option-like values so a crafted `--upload-pack=…` ref can't reach git. +function assertSafeRef(ref) { + if (!/^[\w.+/][\w.\-+/]*$/.test(ref)) { + throw new BmadModuleError(EXIT.USAGE, `unsafe git ref: ${ref}`); + } + return ref; +} + +async function pathExists(p) { + try { + await fs.access(p); + return true; + } catch { + return false; + } +} + +async function readJsonSafe(p) { + try { + return JSON.parse(await fs.readFile(p, 'utf8')); + } catch { + return null; + } +} + +async function git(args, cwd) { + return execFileP('git', args, { cwd, env: GIT_ENV, timeout: 120_000 }); +} + +async function revParseHead(cwd) { + try { + const { stdout } = await execFileP('git', ['rev-parse', 'HEAD'], { cwd }); + return stdout.trim(); + } catch { + return null; + } +} + +async function defaultBranch(cwd) { + try { + const { stdout } = await execFileP('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd }); + return stdout.trim().replace(/^origin\//, '') || 'main'; + } catch { + return 'main'; + } +} + +// Ensure the repo behind `descriptor` is cloned/refreshed in the shared cache at +// the requested `ref` (a tag/branch, or null for the default branch). Returns +// { repoDir, sha, ref }. Reuses an existing clone when its recorded version +// matches; re-clones on a version change; on a fetch failure against an existing +// clone, keeps the stale copy and warns (so installs work offline). +export async function ensureCachedRepo(descriptor, ref = null) { + if (descriptor.kind !== 'git') throw new BmadModuleError(EXIT.USAGE, `ensureCachedRepo requires a git source`); + if (!descriptor.cacheKey) throw new BmadModuleError(EXIT.USAGE, `git source has no cacheKey: ${descriptor.rawInput}`); + const effectiveRef = ref; + if (effectiveRef) assertSafeRef(effectiveRef); + + const repoDir = path.join(getCacheDir(), ...descriptor.cacheKey.split('/')); + await fs.mkdir(path.dirname(repoDir), { recursive: true }); + + // Existing cache at a different version → re-clone from scratch. + if (await pathExists(repoDir)) { + const meta = await readJsonSafe(path.join(repoDir, '.bmad-source.json')); + const cachedVersion = meta?.version || null; + if (effectiveRef !== cachedVersion) { + await fs.rm(repoDir, { recursive: true, force: true }); + } + } + + if (await pathExists(repoDir)) { + // Refresh the existing clone (same version as before). + try { + await git(['fetch', 'origin', '--depth', '1'], repoDir); + if (effectiveRef) { + await git(['fetch', '--depth', '1', 'origin', effectiveRef, '--no-tags'], repoDir); + await git(['checkout', '--quiet', 'FETCH_HEAD'], repoDir); + } else { + const branch = await defaultBranch(repoDir); + assertSafeRef(branch); + await git(['fetch', '--depth', '1', 'origin', branch], repoDir); + await git(['reset', '--hard', `origin/${branch}`], repoDir); + } + } catch (e) { + // Remote unreachable — keep the cached copy so the install still works. + process.stderr.write( + `[bmad-module] warning: could not refresh ${descriptor.displayName} (${e.stderr || e.message}). Using cached copy.\n`, + ); + } + } else { + // Fresh clone. + const args = ['clone', '--depth', '1']; + if (effectiveRef) args.push('--branch', effectiveRef); + args.push(descriptor.url, repoDir); + try { + await git(args); + } catch (e) { + await fs.rm(repoDir, { recursive: true, force: true }).catch(() => {}); + const refSuffix = effectiveRef ? `@${effectiveRef}` : ''; + throw new BmadModuleError(EXIT.NETWORK_FAILURE, `git clone failed for ${descriptor.url}${refSuffix}: ${e.stderr || e.message}`); + } + } + + const sha = await revParseHead(repoDir); + const branchForMeta = effectiveRef ? null : await defaultBranch(repoDir); + const now = new Date().toISOString(); + await fs.writeFile( + path.join(repoDir, '.bmad-source.json'), + JSON.stringify( + { + cloneUrl: descriptor.url, + cacheKey: descriptor.cacheKey, + displayName: descriptor.displayName, + version: effectiveRef || null, + rawInput: descriptor.rawInput, + sha, + clonedAt: now, + }, + null, + 2, + ), + 'utf8', + ); + await fs.writeFile( + path.join(repoDir, '.bmad-channel.json'), + JSON.stringify( + { + channel: effectiveRef ? 'pinned' : 'next', + version: effectiveRef || branchForMeta || 'main', + sha, + writtenAt: now, + }, + null, + 2, + ), + 'utf8', + ); + + return { repoDir, sha, ref: effectiveRef }; +} diff --git a/src/core-skills/bmad-module/scripts/lib/channel-resolver.mjs b/src/core-skills/bmad-module/scripts/lib/channel-resolver.mjs new file mode 100644 index 000000000..d89d37573 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/channel-resolver.mjs @@ -0,0 +1,135 @@ +import https from 'node:https'; +import { valid, prerelease, rcompare } from './semver-lite.mjs'; + +// Channel resolver for community modules — decides which ref of a module to +// clone when no explicit version is supplied: +// - stable: highest pure-semver git tag (excludes -alpha/-beta/-rc) +// - next: default-branch HEAD +// - pinned: an explicit user-supplied tag/branch +// +// node:-only port of tools/installer/modules/channel-resolver.js (which uses the +// `semver` package + node:https). Uses lib/semver-lite.mjs to stay registry-free +// — the skill ships without node_modules. Talks only to the GitHub tags API and +// does semver math; clone logic lives in source.mjs. + +const GITHUB_API_BASE = 'https://api.github.com'; +const DEFAULT_TIMEOUT_MS = 10_000; +const USER_AGENT = 'bmad-method-installer'; + +// Per-process cache: 'owner/repo' => [{ tag, version }] sorted newest-first. +const tagCache = new Map(); + +// Parse a GitHub repo URL into { owner, repo }, or null for non-GitHub URLs. +export function parseGitHubRepo(url) { + if (!url || typeof url !== 'string') return null; + const trimmed = url + .trim() + .replace(/\/+$/, '') + .replace(/\.git$/, ''); + const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?$/i); + if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] }; + const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/]+)$/i); + if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] }; + return null; +} + +function fetchJson(url, { timeout = DEFAULT_TIMEOUT_MS } = {}) { + const headers = { + 'User-Agent': USER_AGENT, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }; + if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + + return new Promise((resolve, reject) => { + const req = https.get(url, { headers, timeout }, (res) => { + let body = ''; + res.on('data', (chunk) => (body += chunk)); + res.on('end', () => { + if (res.statusCode < 200 || res.statusCode >= 300) { + const err = new Error(`GitHub API ${res.statusCode} for ${url}: ${body.slice(0, 200)}`); + err.statusCode = res.statusCode; + return reject(err); + } + try { + resolve(JSON.parse(body)); + } catch (error) { + reject(new Error(`Failed to parse GitHub response: ${error.message}`)); + } + }); + }); + req.on('error', reject); + req.on('timeout', () => { + req.destroy(); + reject(new Error(`GitHub API request timed out: ${url}`)); + }); + }); +} + +// Strip a leading 'v' and return a valid non-prerelease semver, else null. +export function normalizeStableTag(tagName) { + if (typeof tagName !== 'string') return null; + const stripped = tagName.startsWith('v') ? tagName.slice(1) : tagName; + const v = valid(stripped); + if (!v) return null; + if (prerelease(v)) return null; // exclude prereleases + return v; +} + +// Fetch pure-semver tags (highest first) from a GitHub repo, cached per-process. +// Returns [{ tag, version }]: tag is the original ref (e.g. "v1.7.0"), version +// the cleaned semver ("1.7.0"). +export async function fetchStableTags(owner, repo, { timeout } = {}) { + const cacheKey = `${owner}/${repo}`; + if (tagCache.has(cacheKey)) return tagCache.get(cacheKey); + + const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/tags?per_page=100`; + const raw = await fetchJson(url, { timeout }); + if (!Array.isArray(raw)) throw new TypeError(`Unexpected response from ${url}`); + + const stable = []; + for (const entry of raw) { + const version = normalizeStableTag(entry?.name); + if (version) stable.push({ tag: entry.name, version }); + } + stable.sort((a, b) => rcompare(a.version, b.version)); + tagCache.set(cacheKey, stable); + return stable; +} + +// Resolve a channel into a git-clonable ref. +// ref: ref for `git clone --branch`, or null for default-branch HEAD (next) +// version: tag name for stable/pinned, 'main' for next +// Falls back to next-channel semantics with resolvedFallback=true when stable +// turns up no tags or the URL isn't a GitHub repo. Throws on a pinned channel +// with no pin, or on a GitHub API error during stable resolution. +export async function resolveChannel({ channel, pin, repoUrl, timeout }) { + if (channel === 'pinned') { + if (!pin) throw new Error('resolveChannel: pinned channel requires a pin value'); + return { channel: 'pinned', ref: pin, version: pin, resolvedFallback: false }; + } + + if (channel === 'next') { + return { channel: 'next', ref: null, version: 'main', resolvedFallback: false }; + } + + if (channel === 'stable') { + const parsed = parseGitHubRepo(repoUrl); + if (!parsed) { + return { channel: 'next', ref: null, version: 'main', resolvedFallback: true, reason: 'not-a-github-url' }; + } + const tags = await fetchStableTags(parsed.owner, parsed.repo, { timeout }); + if (tags.length === 0) { + return { channel: 'next', ref: null, version: 'main', resolvedFallback: true, reason: 'no-stable-tags' }; + } + const top = tags[0]; + return { channel: 'stable', ref: top.tag, version: top.tag, resolvedFallback: false }; + } + + throw new Error(`resolveChannel: unknown channel '${channel}'`); +} + +// Test-only: clear the per-process tag cache. +export function _clearTagCache() { + tagCache.clear(); +} diff --git a/src/core-skills/bmad-module/scripts/lib/config-gen.mjs b/src/core-skills/bmad-module/scripts/lib/config-gen.mjs new file mode 100644 index 000000000..de52f3756 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/config-gen.mjs @@ -0,0 +1,354 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { parse as parseYaml } from './vendor/yaml.mjs'; + +// Generate/patch the central TOML config for a community module, mirroring the +// full installer's ManifestGenerator.writeCentralConfig + collectAgentsFromModuleYaml +// (tools/installer/core/manifest-generator.js). +// +// Adaptation for the self-contained skill: the installer regenerates the WHOLE +// config from the source tree (it has core/official module.yaml on disk). The +// skill has only `_bmad//module.yaml` for the module it just installed, and +// must NOT clobber [core] or sibling modules' interactively-collected answers it +// cannot reconstruct. So we do a TARGETED merge: upsert just this module's +// `[modules.]` (team→config.toml, user→config.user.toml) and its +// `[agents.]` blocks, leaving every other block byte-for-byte intact. +// +// Values are non-interactive: each prompt key resolves from its module.yaml +// `default` (overridable via `--set .=`), with the same +// `result:` template substitution the installer uses. A module's setup skill +// (postInstallSkill) can re-run this with collected answers for interactive refinement. + +const TEAM_FILE = 'config.toml'; +const USER_FILE = 'config.user.toml'; + +// ── TOML emit (port of formatTomlValue) ────────────────────────────────────── +export function formatTomlValue(value) { + if (value === null || value === undefined) return '""'; + if (typeof value === 'boolean') return value ? 'true' : 'false'; + if (typeof value === 'number' && Number.isFinite(value)) return String(value); + if (Array.isArray(value)) return `[${value.map((v) => formatTomlValue(v)).join(', ')}]`; + const str = String(value); + const escaped = str + .replaceAll('\\', '\\\\') + .replaceAll('"', '\\"') + .replaceAll('\n', '\\n') + .replaceAll('\r', '\\r') + .replaceAll('\t', '\\t'); + return `"${escaped}"`; +} + +// Minimal reverse of formatTomlValue for the scalars we read back (core values). +function parseTomlScalar(raw) { + const s = raw.trim(); + if (s === 'true') return true; + if (s === 'false') return false; + if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s); + if (s.startsWith('"') && s.endsWith('"')) { + return s + .slice(1, -1) + .replaceAll('\\n', '\n') + .replaceAll('\\r', '\r') + .replaceAll('\\t', '\t') + .replaceAll('\\"', '"') + .replaceAll('\\\\', '\\'); + } + return s; +} + +// ── TOML block model ────────────────────────────────────────────────────────── +// Split a config file into a leading preamble (the comment header before the +// first table) and an ordered list of `[header]` blocks. The file is our own +// controlled output, so a line scanner is safer than a full TOML parser. +function splitBlocks(content) { + const lines = content.split('\n'); + const preamble = []; + let i = 0; + while (i < lines.length && !/^\[[^\]]+]\s*$/.test(lines[i])) { + preamble.push(lines[i]); + i++; + } + const blocks = []; + let current = null; + for (; i < lines.length; i++) { + const m = lines[i].match(/^\[([^\]]+)]\s*$/); + if (m) { + if (current) blocks.push(current); + current = { header: m[1], lines: [lines[i]] }; + } else if (current) { + current.lines.push(lines[i]); + } + } + if (current) blocks.push(current); + return { preamble, blocks }; +} + +function blockToText(block) { + const lines = [...block.lines]; + while (lines.length > 1 && lines.at(-1).trim() === '') lines.pop(); + return lines.join('\n'); +} + +function joinFile(preamble, blocks) { + const parts = []; + const pre = [...preamble]; + while (pre.length && pre.at(-1).trim() === '') pre.pop(); + if (pre.length) parts.push(pre.join('\n')); + for (const b of blocks) parts.push(blockToText(b)); + return parts.join('\n\n').replace(/\n+$/, '') + '\n'; +} + +async function readFileOrNull(p) { + try { + return await fs.readFile(p, 'utf8'); + } catch { + return null; + } +} + +// Read the `[core]` table from config.toml as a flat {key: value} map. Used to +// resolve `{output_folder}`-style placeholders in module defaults. +function readCoreValues(teamContent) { + if (!teamContent) return {}; + const { blocks } = splitBlocks(teamContent); + const core = blocks.find((b) => b.header === 'core'); + if (!core) return {}; + const out = {}; + for (const line of core.lines.slice(1)) { + const m = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/); + if (m) out[m[1]] = parseTomlScalar(m[2]); + } + return out; +} + +// ── default/result resolution (port of processResultTemplate) ───────────────── +function applyResultTemplate(template, value, lookups) { + if (typeof template !== 'string') return value; + let result = template; + if (typeof value === 'string') { + result = result.replace('{value}', value); + } else if (typeof value === 'boolean' || typeof value === 'number') { + result = result === '{value}' ? value : result.replace('{value}', String(value)); + } else { + return value; + } + if (typeof result !== 'string') return result; + return result.replaceAll(/{([^}]+)}/g, (match, key) => { + if (key === 'project-root') return '{project-root}'; + if (key === 'value') return match; + let v = lookups[key]; + if (typeof v === 'string' && v.includes('{project-root}/')) v = v.replace('{project-root}/', ''); + return v === undefined || v === null ? match : String(v); + }); +} + +// Resolve a module.yaml into { values, scopes } where values are post-template +// strings and scopes maps each key to 'team' | 'user'. `overrides` supplies +// non-default values (from --set); `coreValues` feeds placeholder resolution. +function resolveModuleConfig(moduleYaml, coreValues, overrides) { + const values = {}; + const scopes = {}; + const lookups = { ...coreValues }; + for (const [key, entry] of Object.entries(moduleYaml || {})) { + if (!entry || typeof entry !== 'object' || !('prompt' in entry)) continue; + const raw = key in overrides ? overrides[key] : entry.default; + if (raw === undefined) continue; + const resolved = 'result' in entry ? applyResultTemplate(entry.result, raw, lookups) : raw; + values[key] = resolved; + scopes[key] = entry.scope === 'user' ? 'user' : 'team'; + // Make this key visible to later keys' placeholder resolution. + lookups[key] = resolved; + } + return { values, scopes }; +} + +function renderModuleBlock(sectionKey, kv) { + const lines = [`[modules.${sectionKey}]`]; + for (const [k, v] of Object.entries(kv)) lines.push(`${k} = ${formatTomlValue(v)}`); + return { header: `modules.${sectionKey}`, lines }; +} + +function renderAgentBlock(agent) { + const lines = [`[agents.${agent.code}]`, `module = ${formatTomlValue(agent.module)}`, `team = ${formatTomlValue(agent.team)}`]; + if (agent.name) lines.push(`name = ${formatTomlValue(agent.name)}`); + if (agent.title) lines.push(`title = ${formatTomlValue(agent.title)}`); + if (agent.icon) lines.push(`icon = ${formatTomlValue(agent.icon)}`); + if (agent.description) lines.push(`description = ${formatTomlValue(agent.description)}`); + return { header: `agents.${agent.code}`, lines }; +} + +const TEAM_HEADER = [ + '# ─────────────────────────────────────────────────────────────────', + '# Installer-managed. Regenerated on install — treat as read-only.', + '# To pin a value or add custom agents, use _bmad/custom/config.toml', + '# (team, committed) — never touched by the installer.', + '# ─────────────────────────────────────────────────────────────────', + '', +]; +const USER_HEADER = [ + '# ─────────────────────────────────────────────────────────────────', + '# Installer-managed. Regenerated on install — treat as read-only.', + '# Holds install answers scoped to YOU personally.', + '# For pinned overrides use _bmad/custom/config.user.toml.', + '# ─────────────────────────────────────────────────────────────────', + '', +]; + +// Upsert `[modules.]` and the module's `[agents.*]` blocks, dropping any +// prior copies (idempotent). When no team/user keys exist the module section is +// omitted from that file. Returns the resolved config values for downstream +// consumers (e.g. directory creation). +export async function regenerateCentralConfig(bmadDir, code, opts = {}) { + const overrides = opts.setOverrides?.[code] || {}; + const moduleYamlPath = path.join(bmadDir, code, 'module.yaml'); + const moduleYamlRaw = await readFileOrNull(moduleYamlPath); + + const teamPath = path.join(bmadDir, TEAM_FILE); + const userPath = path.join(bmadDir, USER_FILE); + const teamContent = await readFileOrNull(teamPath); + const userContent = await readFileOrNull(userPath); + + // No module.yaml → nothing module-specific to write, but still strip any stale + // blocks for this code so re-installs stay clean. + let moduleYaml = null; + if (moduleYamlRaw) { + try { + moduleYaml = parseYaml(moduleYamlRaw); + } catch (e) { + process.stderr.write(`[bmad-module] warn: could not parse ${code}/module.yaml: ${e.message}\n`); + } + } + + const sectionKey = (moduleYaml && moduleYaml.code) || code; + const coreValues = readCoreValues(teamContent); + const { values, scopes } = moduleYaml ? resolveModuleConfig(moduleYaml, coreValues, overrides) : { values: {}, scopes: {} }; + + const teamKv = {}; + const userKv = {}; + for (const [k, v] of Object.entries(values)) { + if (scopes[k] === 'user') userKv[k] = v; + else teamKv[k] = v; + } + + const agents = Array.isArray(moduleYaml?.agents) + ? moduleYaml.agents + .filter((a) => a && typeof a.code === 'string') + .map((a) => ({ + code: a.code, + name: a.name || '', + title: a.title || '', + icon: a.icon || '', + description: a.description || '', + module: code, + team: a.team || code, + })) + : []; + const agentCodes = new Set(agents.map((a) => a.code)); + + // ── config.toml (team) ── + { + const base = teamContent || TEAM_HEADER.join('\n') + '\n'; + const { preamble, blocks } = splitBlocks(base); + // Drop this module's prior [modules.] and its [agents.*] blocks. Match + // current agent codes AND, as a fallback for removed/renamed agents (or a + // missing module.yaml that leaves agentCodes empty), any [agents.*] block + // whose `module = ""` line marks it as owned by this module. + const kept = blocks.filter((b) => { + if (b.header === `modules.${sectionKey}` || b.header === `modules.${code}`) return false; + if (b.header.startsWith('agents.')) { + if (agentCodes.has(b.header.slice('agents.'.length))) return false; + if (b.lines.some((l) => /^module\s*=/.test(l) && parseTomlScalar(l.split('=').slice(1).join('=')) === code)) return false; + } + return true; + }); + if (Object.keys(teamKv).length) kept.push(renderModuleBlock(sectionKey, teamKv)); + for (const a of agents) kept.push(renderAgentBlock(a)); + await fs.writeFile(teamPath, joinFile(preamble, kept), 'utf8'); + } + + // ── config.user.toml (user) ── + { + const base = userContent || USER_HEADER.join('\n') + '\n'; + const { preamble, blocks } = splitBlocks(base); + const kept = blocks.filter((b) => b.header !== `modules.${sectionKey}`); + if (Object.keys(userKv).length) kept.push(renderModuleBlock(sectionKey, userKv)); + await fs.writeFile(userPath, joinFile(preamble, kept), 'utf8'); + } + + return { values, scopes, sectionKey }; +} + +// Read a module's currently-stored config values from config.toml + +// config.user.toml ([modules.]), merged into one {key: value} map. +// Used to detect changed directory paths across updates. +export async function readModuleConfigValues(bmadDir, sectionKey) { + const out = {}; + for (const file of [TEAM_FILE, USER_FILE]) { + const content = await readFileOrNull(path.join(bmadDir, file)); + if (!content) continue; + const { blocks } = splitBlocks(content); + const block = blocks.find((b) => b.header === `modules.${sectionKey}`); + if (!block) continue; + for (const line of block.lines.slice(1)) { + const m = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/); + if (m) out[m[1]] = parseTomlScalar(m[2]); + } + } + return out; +} + +// Resolve a module.yaml's `code` field (the TOML section key), falling back to +// the install code when module.yaml is absent/unparseable. +export async function resolveSectionKey(bmadDir, code) { + const raw = await readFileOrNull(path.join(bmadDir, code, 'module.yaml')); + if (!raw) return code; + try { + const y = parseYaml(raw); + return (y && y.code) || code; + } catch { + return code; + } +} + +// Strip a module's `[modules.]` (both files) and its `[agents.*]` blocks +// (team file) on removal. Agent codes come from the module's module.yaml if it +// still exists; otherwise we drop agent blocks whose `module = ""`. +export async function removeModuleFromConfig(bmadDir, code) { + const moduleYamlRaw = await readFileOrNull(path.join(bmadDir, code, 'module.yaml')); + let sectionKey = code; + const agentCodes = new Set(); + if (moduleYamlRaw) { + try { + const y = parseYaml(moduleYamlRaw); + if (y?.code) sectionKey = y.code; + if (Array.isArray(y?.agents)) for (const a of y.agents) if (a?.code) agentCodes.add(a.code); + } catch { + /* fall through to module= match */ + } + } + + const teamPath = path.join(bmadDir, TEAM_FILE); + const userPath = path.join(bmadDir, USER_FILE); + const teamContent = await readFileOrNull(teamPath); + const userContent = await readFileOrNull(userPath); + + if (teamContent) { + const { preamble, blocks } = splitBlocks(teamContent); + const kept = blocks.filter((b) => { + if (b.header === `modules.${sectionKey}` || b.header === `modules.${code}`) return false; + if (b.header.startsWith('agents.')) { + const ac = b.header.slice('agents.'.length); + if (agentCodes.has(ac)) return false; + // Fallback: drop blocks whose module line names this code. + if (b.lines.some((l) => /^module\s*=/.test(l) && parseTomlScalar(l.split('=').slice(1).join('=')) === code)) return false; + } + return true; + }); + await fs.writeFile(teamPath, joinFile(preamble, kept), 'utf8'); + } + if (userContent) { + const { preamble, blocks } = splitBlocks(userContent); + const kept = blocks.filter((b) => b.header !== `modules.${sectionKey}` && b.header !== `modules.${code}`); + await fs.writeFile(userPath, joinFile(preamble, kept), 'utf8'); + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/exit.mjs b/src/core-skills/bmad-module/scripts/lib/exit.mjs new file mode 100644 index 000000000..89f43d7d0 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/exit.mjs @@ -0,0 +1,33 @@ +// Exit codes for bmad-module verbs. Documented in SKILL.md and README.md so +// callers (Claude, CI, humans) can branch on them programmatically. +export const EXIT = { + OK: 0, + USAGE: 2, + // Setup/packaging problem — the skill's own bundled runtime files (e.g. the + // vendored yaml in lib/vendor/) are missing or corrupt, usually from an + // incomplete copy. Distinct from a module-rejection decision: it's fixable by + // reinstalling the skill. Emitted by the launcher in bmad-module.mjs, which + // duplicates the literal `5` so its guard can depend on nothing — keep in sync. + TOOLING: 5, + NO_BMAD_DIR: 10, + BAD_MANIFEST: 20, + RESERVED_PREFIX: 21, + PREFIX_COLLISION: 30, + COMMIT_FAILURE: 50, + NETWORK_FAILURE: 60, + PATH_TRAVERSAL: 70, + MODIFIED_FILES: 80, + NOT_INSTALLED: 90, +}; + +export class BmadModuleError extends Error { + constructor(code, message) { + super(message); + this.code = code; + } +} + +export function die(code, message) { + process.stderr.write(`[bmad-module] ${message}\n`); + process.exit(code); +} diff --git a/src/core-skills/bmad-module/scripts/lib/frontmatter.mjs b/src/core-skills/bmad-module/scripts/lib/frontmatter.mjs new file mode 100644 index 000000000..e3bb41800 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/frontmatter.mjs @@ -0,0 +1,13 @@ +import { parse as parseYaml } from './vendor/yaml.mjs'; + +// Parse YAML frontmatter from a markdown string. Returns the parsed object, +// or null if no frontmatter block is present / it failed to parse. +export function parseFrontmatter(content) { + const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!m) return null; + try { + return parseYaml(m[1]); + } catch { + return null; + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/fs-safe.mjs b/src/core-skills/bmad-module/scripts/lib/fs-safe.mjs new file mode 100644 index 000000000..2cd846d69 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/fs-safe.mjs @@ -0,0 +1,150 @@ +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import crypto from 'node:crypto'; + +// Resolve `declared` as a path inside `rootAbs`. Rejects absolute paths, +// `..` segments, and symlink escapes. Returns the absolute path or null. +export function safePathInsideRoot(rootAbs, declared) { + if (typeof declared !== 'string' || declared === '') return null; + if (path.isAbsolute(declared)) return null; + if (declared.split(/[\\/]/).includes('..')) return null; + const resolved = path.resolve(rootAbs, declared); + if (resolved !== rootAbs && !resolved.startsWith(rootAbs + path.sep)) return null; + if (fs.existsSync(resolved)) { + try { + const real = fs.realpathSync(resolved); + const realRoot = fs.realpathSync(rootAbs); + if (real !== realRoot && !real.startsWith(realRoot + path.sep)) return null; + } catch { + return null; + } + } + return resolved; +} + +// SHA-256 of a file's bytes, returned as a hex string. Returns null on +// I/O failure — callers should treat a null hash as "file unreadable". +export async function sha256File(filePath) { + try { + const buf = await fsp.readFile(filePath); + return crypto.createHash('sha256').update(buf).digest('hex'); + } catch { + return null; + } +} + +// Recursively copy `srcDir` into `destDir`, creating destDir first. +// Returns an array of relative file paths (POSIX-style) actually copied. +// Skips entries matched by `shouldSkip(relPath)` which receives a POSIX +// path relative to srcDir. +export async function copyDir(srcDir, destDir, shouldSkip = () => false) { + await fsp.mkdir(destDir, { recursive: true }); + const copied = []; + async function walk(rel) { + const absSrc = path.join(srcDir, rel); + const entries = await fsp.readdir(absSrc, { withFileTypes: true }); + for (const entry of entries) { + const childRel = rel ? `${rel}/${entry.name}` : entry.name; + if (shouldSkip(childRel)) continue; + const childSrc = path.join(srcDir, childRel); + const childDest = path.join(destDir, childRel); + if (entry.isDirectory()) { + await fsp.mkdir(childDest, { recursive: true }); + await walk(childRel); + } else if (entry.isFile()) { + await fsp.mkdir(path.dirname(childDest), { recursive: true }); + await fsp.copyFile(childSrc, childDest); + copied.push(childRel); + } + // Symlinks are skipped — install staging never preserves them. + } + } + await walk(''); + return copied; +} + +// Stage a copy plan into `destDir`: each plan entry copies one file from +// `srcRoot/srcRel` to `destDir/destRel`. `extras` is an optional map of +// `destRel → string content` for synthesized files (e.g. a rewritten plugin.json) +// that have no source-tree counterpart. Returns the union of destRels written. +export async function stageCopyPlan(srcRoot, destDir, plan, extras = {}) { + await fsp.mkdir(destDir, { recursive: true }); + const written = []; + for (const { srcRel, destRel } of plan) { + const absSrc = path.join(srcRoot, srcRel); + const absDest = path.join(destDir, destRel); + await fsp.mkdir(path.dirname(absDest), { recursive: true }); + await fsp.copyFile(absSrc, absDest); + written.push(destRel); + } + for (const [destRel, content] of Object.entries(extras)) { + const absDest = path.join(destDir, destRel); + await fsp.mkdir(path.dirname(absDest), { recursive: true }); + await fsp.writeFile(absDest, content, 'utf8'); + written.push(destRel); + } + return written; +} + +// Atomically replace `targetDir` with `stagedDir` contents. Best effort — +// not truly atomic, but minimizes the inconsistent window. +// +// `stagedDir` usually lives under the OS temp dir, which is frequently a +// separate filesystem (e.g. tmpfs on /tmp) from the target. rename() cannot +// move across filesystems and throws EXDEV there, so we first land the staged +// tree onto the target's own filesystem as a sibling — by rename when they +// already share a filesystem, by copy when they don't — and then rename that +// sibling into place, which is always an intra-filesystem atomic swap. +export async function atomicSwapDir(stagedDir, targetDir) { + const parent = path.dirname(targetDir); + await fsp.mkdir(parent, { recursive: true }); + const suffix = crypto.randomBytes(6).toString('hex'); + const sibling = path.join(parent, `.${path.basename(targetDir)}.bmad-tmp-${suffix}`); + const backup = path.join(parent, `.${path.basename(targetDir)}.bmad-old-${suffix}`); + try { + try { + await fsp.rename(stagedDir, sibling); + } catch (e) { + if (e.code !== 'EXDEV') throw e; + await copyDir(stagedDir, sibling); + await fsp.rm(stagedDir, { recursive: true, force: true }); + } + // Move any existing target aside as a backup rather than deleting it + // up front, so a failed swap can be rolled back to the old install. + const hadTarget = await fsp + .stat(targetDir) + .then(() => true) + .catch(() => false); + if (hadTarget) await fsp.rename(targetDir, backup); + try { + await fsp.rename(sibling, targetDir); + } catch (e) { + if (hadTarget) await fsp.rename(backup, targetDir).catch(() => {}); + throw e; + } + if (hadTarget) await fsp.rm(backup, { recursive: true, force: true }); + } catch (e) { + await fsp.rm(sibling, { recursive: true, force: true }); + await fsp.rm(backup, { recursive: true, force: true }); + throw e; + } +} + +// Remove empty parent directories upward until a non-empty one is hit, +// stopping at `stopAt` (exclusive). Used after file deletion. +export async function pruneEmptyDirs(startDir, stopAt) { + let dir = path.resolve(startDir); + const stop = path.resolve(stopAt); + while (dir !== stop && dir.startsWith(stop + path.sep)) { + let entries; + try { + entries = await fsp.readdir(dir); + } catch { + return; + } + if (entries.length > 0) return; + await fsp.rmdir(dir); + dir = path.dirname(dir); + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/help-catalog.mjs b/src/core-skills/bmad-module/scripts/lib/help-catalog.mjs new file mode 100644 index 000000000..60b74b1d3 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/help-catalog.mjs @@ -0,0 +1,133 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +// Regenerate the merged help catalog `_bmad/_config/bmad-help.csv` from every +// installed module's `module-help.csv`. This mirrors the full installer's +// `Installer.mergeModuleHelpCatalogs` (tools/installer/core/installer.js) so a +// module installed via this skill is visible to the `bmad-help` skill, which +// reads `_bmad/_config/bmad-help.csv` (see src/core-skills/bmad-help/SKILL.md). +// +// Self-contained note: the installer scans core from its source tree +// (`getSourcePath('core-skills')`); we instead scan `_bmad//` for every +// installed module — including core, whose `module-help.csv` is copied into +// `_bmad/core/` at `bmad install` time — so this needs no source checkout. + +// Canonical per-module CSV header. Must match +// tools/installer/modules/module-help-schema.js (MODULE_HELP_CSV_HEADER). A +// per-module file whose header differs is loaded positionally with a warning. +export const MODULE_HELP_CSV_HEADER = + 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs'; +const COLUMN_COUNT = 13; +const PHASE_INDEX = 7; + +// Top-level _bmad children that are not modules and must not be scanned. +const NON_MODULE_DIRS = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom']); + +// Parse a single CSV line into fields. Mirrors Installer.parseCSVLine: handles +// `""`-escaped quotes inside quoted fields and unquoted commas as separators. +function parseCsvLine(line) { + const result = []; + let current = ''; + let inQuotes = false; + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const next = line[i + 1]; + if (char === '"') { + if (inQuotes && next === '"') { + current += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + result.push(current); + current = ''; + } else { + current += char; + } + } + result.push(current); + return result; +} + +// Quote a field only when it contains a comma, quote, or newline. Mirrors +// Installer.escapeCSVField so the merged output is byte-compatible. +function escapeCsvField(field) { + if (field === null || field === undefined) return ''; + const str = String(field); + if (str.includes(',') || str.includes('"') || str.includes('\n')) { + return `"${str.replaceAll('"', '""')}"`; + } + return str; +} + +// Read every installed module's module-help.csv, merge into the canonical +// catalog, and write `_bmad/_config/bmad-help.csv`. Returns the data-row count. +// Re-scans the whole tree each call, so it is correct after install AND remove. +export async function regenerateHelpCatalog(bmadDir) { + let entries; + try { + entries = await fs.readdir(bmadDir, { withFileTypes: true }); + } catch { + return 0; + } + const moduleNames = entries + .filter((e) => e.isDirectory() && !NON_MODULE_DIRS.has(e.name) && !e.name.startsWith('.')) + .map((e) => e.name) + .sort(); + + const allRows = []; + for (const moduleName of moduleNames) { + const helpFilePath = path.join(bmadDir, moduleName, 'module-help.csv'); + let content; + try { + content = await fs.readFile(helpFilePath, 'utf8'); + } catch { + continue; // module ships no help catalog — fine + } + const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#')); + let headerWarned = false; + for (const line of lines) { + // Canonical header row: warn on drift, then skip. (A non-canonical header + // that doesn't start with `module,` falls through and is loaded as data, + // matching the installer — author CSVs should use the canonical header.) + if (line.startsWith('module,')) { + if (!headerWarned && line.trim() !== MODULE_HELP_CSV_HEADER) { + process.stderr.write( + `[bmad-module] warn: ${moduleName}/module-help.csv header differs from canonical schema — data loaded positionally.\n`, + ); + headerWarned = true; + } + continue; + } + const columns = parseCsvLine(line); + if (columns.length < COLUMN_COUNT - 1) continue; + const padded = columns.slice(0, COLUMN_COUNT); + while (padded.length < COLUMN_COUNT) padded.push(''); + // Empty module column → fill with the dir name (core stays empty so its + // rows render as universal tools), matching the installer. + if ((!padded[0] || padded[0].trim() === '') && moduleName !== 'core') { + padded[0] = moduleName; + } + allRows.push(padded.map((c) => escapeCsvField(c)).join(',')); + } + } + + // Sort by (module, phase); stable within a phase to preserve authored order. + const decorated = allRows.map((row, index) => ({ row, index, cols: parseCsvLine(row) })); + decorated.sort((a, b) => { + const moduleA = (a.cols[0] || '').toLowerCase(); + const moduleB = (b.cols[0] || '').toLowerCase(); + if (moduleA !== moduleB) return moduleA.localeCompare(moduleB); + const phaseA = a.cols[PHASE_INDEX] || ''; + const phaseB = b.cols[PHASE_INDEX] || ''; + if (phaseA !== phaseB) return phaseA.localeCompare(phaseB); + return a.index - b.index; + }); + const sortedRows = decorated.map((d) => d.row); + + const outputPath = path.join(bmadDir, '_config', 'bmad-help.csv'); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, [MODULE_HELP_CSV_HEADER, ...sortedRows].join('\n'), 'utf8'); + return sortedRows.length; +} diff --git a/src/core-skills/bmad-module/scripts/lib/ide-sync.mjs b/src/core-skills/bmad-module/scripts/lib/ide-sync.mjs new file mode 100644 index 000000000..9df4f192d --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/ide-sync.mjs @@ -0,0 +1,68 @@ +import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { readManifestYaml } from './manifest-ops.mjs'; + +// Distribute the project's installed skills to the coding assistants (IDEs) the +// user chose at `bmad install` time, by running the self-contained engine bundle +// shipped beside this file (vendor/ide-sync.mjs). Pure node: + a local bundle — +// no npx, no network, no node_modules. The bundle reads the chosen IDEs from +// _bmad/_config/manifest.yaml and the skills from _config/skill-manifest.csv. +// +// `prune` is the list of canonicalIds to remove from the IDE directories (the +// skills of a module being updated or removed); pass [] for a plain install. +// +// Returns one of: +// { skipped: true } — no IDEs configured; nothing to do +// { ok: true } — bundle ran and distributed successfully +// { ok: false, hint } — bundle missing or exited non-zero; caller +// reports the hint but does NOT fail the verb +// (the _bmad/ write already succeeded). +export async function distributeToIdes({ projectDir, bmadDir, prune = [] }) { + const manifest = await readManifestYaml(bmadDir); + // readManifestYaml returns null for BOTH a missing manifest and a parse + // failure. A present-but-unreadable manifest is real config corruption — don't + // silently skip distribution; surface a repair hint. A genuinely absent + // manifest falls through to the "nothing configured" skip below. + if (manifest === null && existsSync(path.join(bmadDir, '_config', 'manifest.yaml'))) { + return { + ok: false, + hint: + 'Could not read _bmad/_config/manifest.yaml (invalid YAML). Run `bmad install` to repair BMAD config, ' + + 'then `bmad ide-sync` to push skills to your coding assistants.', + }; + } + const ides = Array.isArray(manifest?.ides) ? manifest.ides.filter((i) => i && typeof i === 'string') : []; + if (ides.length === 0) { + return { skipped: true }; + } + + const bundlePath = fileURLToPath(new URL('vendor/ide-sync.mjs', import.meta.url)); + if (!existsSync(bundlePath)) { + return { + ok: false, + hint: + 'IDE distribution bundle is missing (older install). Run `bmad install` to refresh BMAD tooling, ' + + 'or `bmad ide-sync` to push skills to your coding assistants.', + }; + } + + const args = [bundlePath, '-d', projectDir]; + const pruneIds = (prune || []).filter(Boolean); + if (pruneIds.length) args.push('--prune', pruneIds.join(',')); + + const code = await new Promise((resolve) => { + const child = spawn(process.execPath, args, { stdio: 'inherit' }); + child.on('error', () => resolve(-1)); + child.on('close', (c) => resolve(c ?? -1)); + }); + + if (code === 0) return { ok: true }; + return { + ok: false, + hint: + `IDE distribution exited with code ${code}. Your module is installed under _bmad/, but skills may ` + + 'not be in every coding assistant yet — run `bmad ide-sync` to retry.', + }; +} diff --git a/src/core-skills/bmad-module/scripts/lib/install-plan.mjs b/src/core-skills/bmad-module/scripts/lib/install-plan.mjs new file mode 100644 index 000000000..27c4fd46d --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/install-plan.mjs @@ -0,0 +1,403 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { EXIT, BmadModuleError } from './exit.mjs'; +import { safePathInsideRoot } from './fs-safe.mjs'; + +// Default ignore patterns always applied on top of user ignores. +// `.claude-plugin/` is intentionally NOT ignored — the manifest is needed +// post-install for `update` and `list` to re-resolve the module. +const DEFAULT_IGNORES = ['.git/**', '.git', 'node_modules/**', 'node_modules', '.bmadignore', '.DS_Store', '**/.DS_Store']; + +// Compile one ignore pattern (gitignore-lite: supports `*`, `**`, `?`, and +// trailing `/`; no negation, no leading `/` anchoring) into a RegExp matched +// against a POSIX-style relative path. +function compilePattern(pattern) { + let p = pattern.trim(); + if (!p || p.startsWith('#')) return null; + // Treat trailing slash as "directory" — match the dir and its contents. + const dirOnly = p.endsWith('/'); + if (dirOnly) p = p.slice(0, -1); + // Anchor by default (gitignore semantics): if no slash in pattern, match + // basename anywhere; else anchor to root. + const anchored = p.includes('/'); + let body = p + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*\*/g, '\uFFFF') + .replace(/\*/g, '[^/]*') + .replace(/\uFFFF/g, '.*') + .replace(/\?/g, '[^/]'); + const re = anchored ? new RegExp(`^${body}(/.*)?$`) : new RegExp(`(^|/)${body}(/.*)?$`); + return re; +} + +export function buildIgnoreMatcher(userPatterns) { + const patterns = [...DEFAULT_IGNORES, ...(userPatterns || [])]; + const compiled = patterns.map(compilePattern).filter(Boolean); + return (relPath) => { + const posix = relPath.replaceAll('\\', '/'); + return compiled.some((re) => re.test(posix)); + }; +} + +// Load user ignore patterns from manifest first, then .bmadignore. Declaring +// both at once is disallowed — readUserIgnores enforces it. +export async function readUserIgnores(sourceDir, manifest) { + const fromManifest = manifest?.bmad?.install?.ignore; + const ignoreFilePath = path.join(sourceDir, '.bmadignore'); + let fromFile = null; + try { + const buf = await fs.readFile(ignoreFilePath, 'utf8'); + fromFile = buf + .split(/\r?\n/) + .map((s) => s.trim()) + .filter(Boolean); + } catch { + /* no .bmadignore — fine */ + } + if (Array.isArray(fromManifest) && fromFile) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `both .bmadignore and bmad.install.ignore are present — pick one`); + } + if (Array.isArray(fromManifest)) return fromManifest; + if (fromFile) return fromFile; + return []; +} + +// Validate that every declared path in the manifest exists inside the source +// tree and resolves safely (no traversal, no symlink escape). Declared paths +// double as documentation; they do NOT drive the copy list, but if they are +// broken the install would land a non-functional module. +export function validateDeclaredPaths(sourceDir, manifest) { + const declared = []; + const arr = (key, val) => Array.isArray(val) && val.forEach((v) => declared.push({ key, val: v })); + const str = (key, val) => typeof val === 'string' && declared.push({ key, val }); + arr('skills', manifest.skills); + arr('agents', manifest.agents); + arr('commands', manifest.commands); + str('hooks', manifest.hooks); + if (typeof manifest.mcpServers === 'string') str('mcpServers', manifest.mcpServers); + str('lspServers', manifest.lspServers); + str('settings', manifest.settings); + str('bmad.moduleDefinition', manifest.bmad?.moduleDefinition); + str('bmad.moduleHelpCsv', manifest.bmad?.moduleHelpCsv); + arr('bmad.customize.schemas', manifest.bmad?.customize?.schemas); + if (manifest.bmad?.docs) { + str('bmad.docs.readme', manifest.bmad.docs.readme); + str('bmad.docs.changelog', manifest.bmad.docs.changelog); + if (typeof manifest.bmad.docs.homepage === 'string' && !/^https?:/.test(manifest.bmad.docs.homepage)) { + str('bmad.docs.homepage', manifest.bmad.docs.homepage); + } + } + for (const { key, val } of declared) { + const safe = safePathInsideRoot(sourceDir, val); + if (safe === null) { + throw new BmadModuleError(EXIT.PATH_TRAVERSAL, `manifest ${key}: "${val}" escapes module root`); + } + } + // Existence is enforced by the validator pre-publish; at install time we + // surface missing declared paths as PATH_TRAVERSAL-class problems too, so + // the user gets a single failure mode for "manifest doesn't match tree". + return declared; +} + +// Walk the module source tree and return the list of POSIX-relative file +// paths that should be copied into `_bmad//`. Honors ignore patterns +// and skips symlinks (they're not preserved in the install). +export async function buildCopyList(sourceDir, ignoreMatch) { + const out = []; + async function walk(rel) { + const absDir = path.join(sourceDir, rel); + const entries = await fs.readdir(absDir, { withFileTypes: true }); + for (const entry of entries) { + const childRel = rel ? `${rel}/${entry.name}` : entry.name; + if (ignoreMatch(childRel)) continue; + if (entry.isSymbolicLink()) continue; + if (entry.isDirectory()) await walk(childRel); + else if (entry.isFile()) out.push(childRel); + } + } + await walk(''); + out.sort(); + return out; +} + +// Top-level files we always copy if present (and not ignored). Authors don't +// have to declare these — they're conventional repo metadata. package.json / +// package-lock.json are included so a module that ships JS runtime deps can have +// them installed in place post-copy (see npm-deps.mjs); node_modules itself is +// never copied (it's in DEFAULT_IGNORES) and is regenerated by npm install. +const ALWAYS_TOPLEVEL = new Set([ + 'README.md', + 'README', + 'README.rst', + 'CHANGELOG.md', + 'CHANGELOG', + 'LICENSE', + 'LICENSE.md', + 'LICENSE.txt', + 'LICENCE', + 'LICENCE.md', + 'NOTICE', + 'NOTICE.md', + 'package.json', + 'package-lock.json', +]); + +function stripDotSlash(p) { + if (typeof p !== 'string') return p; + let s = p.replaceAll('\\', '/'); + if (s.startsWith('./')) s = s.slice(2); + return s; +} + +// Recursively list files under `sourceDir/relDir`, returning POSIX paths +// relative to `sourceDir`. Skips symlinks and ignore-matched entries. +async function listFilesUnder(sourceDir, relDir, ignoreMatch) { + const out = []; + async function walk(rel) { + const absDir = path.join(sourceDir, rel); + let entries; + try { + entries = await fs.readdir(absDir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const childRel = rel ? `${rel}/${entry.name}` : entry.name; + if (ignoreMatch && ignoreMatch(childRel)) continue; + if (entry.isSymbolicLink()) continue; + if (entry.isDirectory()) await walk(childRel); + else if (entry.isFile()) out.push(childRel); + } + } + await walk(relDir); + return out; +} + +// Build a manifest-driven copy plan. Each entry is { srcRel, destRel } in +// POSIX form, relative to the module's source root / install root respectively. +// +// The plan: +// - Each declared `skills[]` / `agents[]` / `commands[]` dir is copied +// recursively into the canonical slot (`skills//...` etc.), +// regardless of where the author kept it in source (e.g. under `src/`). +// - `bmad.moduleDefinition` → `module.yaml` +// - `bmad.moduleHelpCsv` → `module-help.csv` +// - `bmad.docs.readme` / `bmad.docs.changelog` / `bmad.docs.homepage` +// (relative path) → preserved at module root with their basename. +// - `hooks` / `mcpServers` / `lspServers` / `settings` (when string paths) → +// canonical root slot (`hooks.json`, `.mcp.json`, etc.). +// - `.claude-plugin/plugin.json` is always kept (callers rewrite paths in it +// via `rewriteManifestPaths`). +// - `.claude-plugin/marketplace.json` is preserved if present. +// - Conventional top-level metadata files (README/CHANGELOG/LICENSE/NOTICE) +// are copied if present at source root. +// +// Anything NOT covered by the above is dropped. This means `tools/`, `website/`, +// `.github/`, `.trunk/`, etc. don't leak into the install even if the author +// forgot to list them in `bmad.install.ignore`. +// +// Returns: { plan, skillDestDirs } where skillDestDirs is the list of canonical +// skill paths (`skills/X`) for the skill-manifest writer. +export async function buildCopyPlan(sourceDir, manifest, ignoreMatch) { + const plan = []; + const claimedSrc = new Set(); + const claimedDest = new Set(); + + // `allowDupSrc` lets a single source file be copied to a second destination + // even if it was already claimed by an earlier (recursive) copy. Needed for + // moduleDefinition / moduleHelpCsv, which commonly live INSIDE a declared + // skill dir (e.g. `/assets/module.yaml`): they must also be + // flattened to the canonical `_bmad//module.yaml` root slot that the + // rewritten plugin.json points at and that config / help-catalog read. + const addFile = (srcRel, destRel, allowDupSrc = false) => { + if (!srcRel || !destRel) return; + if (!allowDupSrc && claimedSrc.has(srcRel)) return; + if (claimedDest.has(destRel)) return; + if (!allowDupSrc) claimedSrc.add(srcRel); + claimedDest.add(destRel); + plan.push({ srcRel, destRel }); + }; + + const addDirRecursive = async (srcRelDir, destRelDir) => { + const files = await listFilesUnder(sourceDir, srcRelDir, ignoreMatch); + for (const fileSrcRel of files) { + const rest = fileSrcRel.slice(srcRelDir.length).replace(/^\//, ''); + const destRel = rest ? `${destRelDir}/${rest}` : destRelDir; + addFile(fileSrcRel, destRel); + } + }; + + // Helper: if `srcRel` exists as a file in source, queue it. + const queueFileIfExists = async (srcRel, destRel, allowDupSrc = false) => { + if (!srcRel) return; + if (ignoreMatch && ignoreMatch(srcRel)) return; + try { + const stat = await fs.stat(path.join(sourceDir, srcRel)); + if (stat.isFile()) addFile(srcRel, destRel, allowDupSrc); + } catch { + /* missing — silently skip; validateDeclaredPaths surfaces declared misses */ + } + }; + + // Plugin manifest itself — always kept. Path is rewritten by the caller + // before staging; here we just reserve the slot so nothing else claims it. + claimedDest.add('.claude-plugin/plugin.json'); + + // Optional marketplace.json — copy verbatim if present. + await queueFileIfExists('.claude-plugin/marketplace.json', '.claude-plugin/marketplace.json'); + + // Skills / agents / commands. + const arrCategories = [ + ['skills', 'skills', manifest.skills], + ['agents', 'agents', manifest.agents], + ['commands', 'commands', manifest.commands], + ]; + const skillDestDirs = []; + for (const [, destPrefix, arr] of arrCategories) { + if (!Array.isArray(arr)) continue; + for (const declared of arr) { + const srcRel = stripDotSlash(declared); + if (!srcRel) continue; + const destRel = `${destPrefix}/${path.posix.basename(srcRel)}`; + // Entries may be directories (skills, agent packs) or single files + // (e.g. a subagent declared as `./agents/foo.md`). Stat to branch; + // rewriteManifestPaths() remaps both to `/`. + try { + const stat = await fs.stat(path.join(sourceDir, srcRel)); + if (stat.isDirectory()) { + await addDirRecursive(srcRel, destRel); + if (destPrefix === 'skills') skillDestDirs.push(destRel); + } else if (stat.isFile() && (!ignoreMatch || !ignoreMatch(srcRel))) addFile(srcRel, destRel); + } catch { + /* missing — validateDeclaredPaths surfaces declared misses */ + } + } + } + + // moduleDefinition / moduleHelpCsv — flatten to canonical names at root. + // allowDupSrc: these often live inside a declared skill dir, so the source + // file may already be claimed by that skill's recursive copy; we still want + // the canonical root copy that the rewritten manifest, config-gen, and the + // help catalog all rely on. + if (typeof manifest.bmad?.moduleDefinition === 'string') { + await queueFileIfExists(stripDotSlash(manifest.bmad.moduleDefinition), 'module.yaml', true); + } + if (typeof manifest.bmad?.moduleHelpCsv === 'string') { + await queueFileIfExists(stripDotSlash(manifest.bmad.moduleHelpCsv), 'module-help.csv', true); + } + + // Top-level docs declared in the manifest — keep at root by basename. + const docs = manifest.bmad?.docs; + if (docs && typeof docs === 'object') { + for (const key of ['readme', 'changelog', 'homepage']) { + const v = docs[key]; + if (typeof v !== 'string') continue; + if (/^https?:/i.test(v)) continue; + const srcRel = stripDotSlash(v); + await queueFileIfExists(srcRel, path.posix.basename(srcRel)); + } + } + + // String-typed Claude-Code surfaces — canonical root slot. + const stringSurfaces = [ + ['hooks', 'hooks.json'], + ['mcpServers', '.mcp.json'], + ['lspServers', 'lsp-servers.json'], + ['settings', 'settings.json'], + ]; + for (const [key, destName] of stringSurfaces) { + const v = manifest[key]; + if (typeof v !== 'string') continue; + const srcRel = stripDotSlash(v); + if (!srcRel) continue; + // These surfaces are single JSON files installed at a fixed canonical name + // (rewriteManifestPaths always points the manifest at `./hooks.json`, + // `./.mcp.json`, etc.). A directory source would be copied under its + // basename yet leave the manifest pointing at a file that was never written, + // so only file sources are honored here. + try { + const stat = await fs.stat(path.join(sourceDir, srcRel)); + if (stat.isFile()) addFile(srcRel, destName); + } catch { + /* missing — skip */ + } + } + + // Conventional top-level metadata files — copy if present. + for (const name of ALWAYS_TOPLEVEL) { + await queueFileIfExists(name, name); + } + + // Stable order — dest-relative sort makes diffs and dry-run output readable. + plan.sort((a, b) => a.destRel.localeCompare(b.destRel)); + skillDestDirs.sort(); + + return { plan, skillDestDirs }; +} + +// Produce a rewritten plugin.json where every declared path points at its +// canonical post-install location (so the on-disk manifest stays self-consistent +// inside `_bmad//`). Returns a JSON string. +export function rewriteManifestPaths(manifest) { + const out = structuredClone(manifest); + + const remapArr = (arr, destPrefix) => { + if (!Array.isArray(arr)) return arr; + return arr.map((entry) => { + if (typeof entry !== 'string') return entry; + const srcRel = stripDotSlash(entry); + return `./${destPrefix}/${path.posix.basename(srcRel)}`; + }); + }; + + if (Array.isArray(out.skills)) out.skills = remapArr(out.skills, 'skills'); + if (Array.isArray(out.agents)) out.agents = remapArr(out.agents, 'agents'); + if (Array.isArray(out.commands)) out.commands = remapArr(out.commands, 'commands'); + + if (typeof out.hooks === 'string') out.hooks = './hooks.json'; + if (typeof out.mcpServers === 'string') out.mcpServers = './.mcp.json'; + if (typeof out.lspServers === 'string') out.lspServers = './lsp-servers.json'; + if (typeof out.settings === 'string') out.settings = './settings.json'; + + if (out.bmad && typeof out.bmad === 'object') { + if (typeof out.bmad.moduleDefinition === 'string') out.bmad.moduleDefinition = './module.yaml'; + if (typeof out.bmad.moduleHelpCsv === 'string') out.bmad.moduleHelpCsv = './module-help.csv'; + + // customize.schemas — each entry lives inside a declared skill dir, which is + // remapped to `skills/`. Anchor on the owning skill dir and keep + // every segment after it, so nested schemas (e.g. `/schemas/x.yaml`) + // land under the right skill instead of being collapsed to the last two + // segments. + const skillDirs = Array.isArray(manifest.skills) + ? manifest.skills.filter((s) => typeof s === 'string').map((s) => stripDotSlash(s)) + : []; + const schemas = out.bmad.customize?.schemas; + if (Array.isArray(schemas)) { + out.bmad.customize.schemas = schemas.map((entry) => { + if (typeof entry !== 'string') return entry; + const srcRel = stripDotSlash(entry); + const owner = skillDirs.find((sd) => sd && (srcRel === sd || srcRel.startsWith(sd + '/'))); + if (owner) { + const remainder = srcRel.slice(owner.length + 1); + return `./skills/${path.posix.basename(owner)}/${remainder}`; + } + // Fallback when no declared skill owns the path: last two segments are + // assumed to be `/`. + const parts = srcRel.split('/'); + if (parts.length >= 2) return `./skills/${parts.at(-2)}/${parts.at(-1)}`; + return `./${srcRel}`; + }); + } + + if (out.bmad.docs && typeof out.bmad.docs === 'object') { + for (const key of ['readme', 'changelog', 'homepage']) { + const v = out.bmad.docs[key]; + if (typeof v !== 'string') continue; + if (/^https?:/i.test(v)) continue; + out.bmad.docs[key] = `./${path.posix.basename(stripDotSlash(v))}`; + } + } + } + + return JSON.stringify(out, null, 2) + '\n'; +} diff --git a/src/core-skills/bmad-module/scripts/lib/legacy-resolver.mjs b/src/core-skills/bmad-module/scripts/lib/legacy-resolver.mjs new file mode 100644 index 000000000..5ba061e22 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/legacy-resolver.mjs @@ -0,0 +1,384 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { parse as parseYaml } from './vendor/yaml.mjs'; +import { valid as semverValid } from './semver-lite.mjs'; +import { safePathInsideRoot } from './fs-safe.mjs'; +import { MODULE_HELP_CSV_HEADER } from './help-catalog.mjs'; +import { EXIT, BmadModuleError } from './exit.mjs'; + +// Resolve a LEGACY BMAD module (marketplace.json + module.yaml) into a synthetic +// manifest of the same shape readAndValidateManifest produces, so the install +// pipeline (validateDeclaredPaths → buildCopyPlan → rewriteManifestPaths → …) +// handles it with no special-casing. This is a self-contained port of the full +// installer's PluginResolver (tools/installer/modules/plugin-resolver.js) +// strategies 1–5; the skill must not import from tools/installer (it ships +// standalone under .claude/skills/). +// +// Strategies, tried per marketplace plugin in order: +// 1. Module files (module.yaml + module-help.csv) at the skills' common parent +// or any directory between there and the repo root. +// 2. A `*-setup` skill with assets/module.yaml + assets/module-help.csv. +// 3. A single standalone skill with both files in its assets/. +// 4. Multiple standalone skills, each with both files → one module each. +// 5. Fallback: synthesize module.yaml + module-help.csv from marketplace.json +// metadata and SKILL.md frontmatter. +// +// Returns null when there is no marketplace.json (caller emits the normal +// BAD_MANIFEST). Returns { manifest, synthesized } on success, where +// `synthesized` is { 'module.yaml': string|null, 'module-help.csv': string|null } +// (non-null only for strategy 5, which the caller writes into the temp source +// dir before buildCopyPlan reads it). Throws BmadModuleError(BAD_MANIFEST) on an +// unparseable marketplace.json, when nothing resolves, or on multi-module +// ambiguity that `selector` does not disambiguate. +export async function resolveLegacyModule(sourceDir, { selector = null } = {}) { + const mpPath = path.join(sourceDir, '.claude-plugin', 'marketplace.json'); + let raw; + try { + raw = await fs.readFile(mpPath, 'utf8'); + } catch { + return null; + } + let mp; + try { + mp = JSON.parse(raw); + } catch (e) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `.claude-plugin/marketplace.json failed to parse: ${e.message}`); + } + const plugins = Array.isArray(mp.plugins) ? mp.plugins : []; + if (plugins.length === 0) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `marketplace.json declares no plugins`); + } + + const candidates = []; + for (const plugin of plugins) { + if (!plugin || typeof plugin !== 'object') continue; + const skillPaths = await resolveSkillPaths(sourceDir, plugin.skills || []); + if (skillPaths.length === 0) continue; // plugin contributes no installable skills + const resolved = + (await tryRootModuleFiles(sourceDir, plugin, skillPaths)) || + (await trySetupSkill(sourceDir, plugin, skillPaths)) || + (await trySingleStandalone(sourceDir, plugin, skillPaths)) || + (await tryMultipleStandalone(sourceDir, plugin, skillPaths)) || + (await synthesizeFallback(sourceDir, plugin, skillPaths)); + candidates.push(...resolved); + } + + if (candidates.length === 0) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `marketplace.json resolved no installable module (no skills found on disk)`); + } + + const pick = selectModule(candidates, selector); + return toSyntheticManifest(pick, sourceDir); +} + +// ─── Skill-path resolution ─────────────────────────────────────────────────── + +// Map a plugin's skills[] (repo-relative, ./-prefixed) to source-relative POSIX +// paths that exist on disk and stay inside the source root. Mirrors +// plugin-resolver.js:60-71. +async function resolveSkillPaths(sourceDir, skillRel) { + const out = []; + for (const rel of skillRel) { + if (typeof rel !== 'string') continue; + const normalized = rel.replace(/^\.\//, ''); + const abs = safePathInsideRoot(sourceDir, normalized); + if (!abs) continue; // traversal / absolute path — skip + if (await exists(abs)) out.push(toRel(sourceDir, abs)); + } + return out; +} + +// ─── Strategy 1: root module files (walk up from skills' common parent) ─────── + +async function tryRootModuleFiles(sourceDir, plugin, skillRelPaths) { + const commonParentAbs = computeCommonParent(skillRelPaths.map((r) => path.resolve(sourceDir, r))); + const candidates = await findModuleFilesUpward(commonParentAbs, sourceDir); + if (candidates.length === 0) return null; + // Deepest candidate (closest to the skills) is the safe default; a CLI has no + // interactive picker so we don't prompt between chain candidates. + const { moduleYamlAbs, moduleHelpAbs } = candidates[0]; + const data = await readModuleYaml(moduleYamlAbs); + if (!data) return null; + return [ + makeCandidate(plugin, data, skillRelPaths, { + moduleYamlRel: toRel(sourceDir, moduleYamlAbs), + moduleHelpCsvRel: toRel(sourceDir, moduleHelpAbs), + }), + ]; +} + +// ─── Strategy 2: -setup skill with assets/module.yaml ───────────────────────── + +async function trySetupSkill(sourceDir, plugin, skillRelPaths) { + for (const skillRel of skillRelPaths) { + if (!path.posix.basename(skillRel).endsWith('-setup')) continue; + const found = await skillAssets(sourceDir, skillRel); + if (!found) continue; + const data = await readModuleYaml(path.resolve(sourceDir, found.moduleYamlRel)); + if (!data) continue; + return [makeCandidate(plugin, data, skillRelPaths, found)]; + } + return null; +} + +// ─── Strategy 3: single standalone skill ────────────────────────────────────── + +async function trySingleStandalone(sourceDir, plugin, skillRelPaths) { + if (skillRelPaths.length !== 1) return null; + const found = await skillAssets(sourceDir, skillRelPaths[0]); + if (!found) return null; + const data = await readModuleYaml(path.resolve(sourceDir, found.moduleYamlRel)); + if (!data) return null; + return [makeCandidate(plugin, data, skillRelPaths, found)]; +} + +// ─── Strategy 4: multiple standalone skills, each its own module ─────────────── + +async function tryMultipleStandalone(sourceDir, plugin, skillRelPaths) { + if (skillRelPaths.length < 2) return null; + const resolved = []; + for (const skillRel of skillRelPaths) { + const found = await skillAssets(sourceDir, skillRel); + if (!found) continue; + const data = await readModuleYaml(path.resolve(sourceDir, found.moduleYamlRel)); + if (!data) continue; + resolved.push( + makeCandidate({ ...plugin }, data, [skillRel], found, { + fallbackCode: path.posix.basename(skillRel), + }), + ); + } + // Only use strategy 4 if EVERY skill carries module files; otherwise fall + // through to the synthesizer (mirrors plugin-resolver.js:349-355). + return resolved.length === skillRelPaths.length ? resolved : null; +} + +// ─── Strategy 5: synthesize from marketplace.json + SKILL.md frontmatter ────── + +async function synthesizeFallback(sourceDir, plugin, skillRelPaths) { + const skillInfos = []; + for (const skillRel of skillRelPaths) { + const fm = await parseSkillFrontmatter(path.resolve(sourceDir, skillRel)); + skillInfos.push({ + dirName: path.posix.basename(skillRel), + name: fm.name || path.posix.basename(skillRel), + description: fm.description || '', + }); + } + const code = plugin.name || path.posix.basename(skillRelPaths[0]); + const moduleName = formatDisplayName(code); + const synthesizedYaml = + `code: ${code}\n` + + `name: ${JSON.stringify(moduleName)}\n` + + `description: ${JSON.stringify(plugin.description || '')}\n` + + `module_version: ${plugin.version || '1.0.0'}\n`; + const synthesizedCsv = buildSynthesizedHelpCsv(moduleName, skillInfos); + return [ + { + code, + name: moduleName, + version: plugin.version || null, + description: plugin.description || '', + pluginName: plugin.name, + skillRelPaths, + moduleYamlRel: 'module.yaml', + moduleHelpCsvRel: 'module-help.csv', + synthesizedYaml, + synthesizedCsv, + }, + ]; +} + +// ─── Candidate selection ────────────────────────────────────────────────────── + +function selectModule(candidates, selector) { + if (candidates.length === 1) return candidates[0]; + const codes = candidates.map((c) => c.code); + if (selector) { + const matches = candidates.filter((c) => c.code === selector); + if (matches.length === 1) return matches[0]; + throw new BmadModuleError(EXIT.BAD_MANIFEST, `no module with code "${selector}" in this repo. Available: ${codes.join(', ')}.`); + } + throw new BmadModuleError(EXIT.BAD_MANIFEST, `this repo defines multiple modules: ${codes.join(', ')}. Re-run with --module .`); +} + +// ─── Synthetic manifest builder ─────────────────────────────────────────────── + +function toSyntheticManifest(pick, _sourceDir) { + const name = sanitizeName(pick.pluginName || pick.code); + const version = semverValid(pick.version) ? pick.version : '0.0.0'; + const manifest = { + name, + version, + description: pick.description || '', + skills: pick.skillRelPaths.map((p) => `./${p}`), + bmad: { + specVersion: '1.0.0', + code: pick.code, + compatibility: { bmadMethod: '>=6.0.0' }, + moduleVersion: pick.version || version, + moduleDefinition: `./${pick.moduleYamlRel}`, + moduleHelpCsv: `./${pick.moduleHelpCsvRel}`, + }, + }; + return { + manifest, + synthesized: { + 'module.yaml': pick.synthesizedYaml || null, + 'module-help.csv': pick.synthesizedCsv || null, + }, + }; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +// Normalize a module.yaml + plugin pair into the intermediate candidate shape. +function makeCandidate(plugin, data, skillRelPaths, files, { fallbackCode } = {}) { + return { + code: data.code || fallbackCode || plugin.name, + name: data.name || plugin.name, + version: plugin.version || data.module_version || null, + description: data.description || plugin.description || '', + pluginName: plugin.name, + skillRelPaths, + moduleYamlRel: files.moduleYamlRel, + moduleHelpCsvRel: files.moduleHelpCsvRel, + synthesizedYaml: null, + synthesizedCsv: null, + }; +} + +// Return assets/module.yaml + assets/module-help.csv under a skill dir when both +// exist, as source-relative POSIX paths; else null. +async function skillAssets(sourceDir, skillRel) { + const moduleYamlRel = path.posix.join(skillRel, 'assets', 'module.yaml'); + const moduleHelpCsvRel = path.posix.join(skillRel, 'assets', 'module-help.csv'); + if (!(await exists(path.resolve(sourceDir, moduleYamlRel)))) return null; + if (!(await exists(path.resolve(sourceDir, moduleHelpCsvRel)))) return null; + return { moduleYamlRel, moduleHelpCsvRel }; +} + +// Walk from startDirAbs up to the source root, collecting dirs that contain BOTH +// module.yaml and module-help.csv. Deepest-first; bounded by sourceDir. +async function findModuleFilesUpward(startDirAbs, sourceDir) { + const root = path.resolve(sourceDir); + let dir = path.resolve(startDirAbs); + if (dir !== root && !dir.startsWith(root + path.sep)) dir = root; + const out = []; + while (true) { + const moduleYamlAbs = path.join(dir, 'module.yaml'); + const moduleHelpAbs = path.join(dir, 'module-help.csv'); + if ((await exists(moduleYamlAbs)) && (await exists(moduleHelpAbs))) { + out.push({ moduleYamlAbs, moduleHelpAbs }); + } + if (dir === root) break; + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return out; +} + +// Deepest common ancestor of absolute paths. Single path → its dirname. +function computeCommonParent(absPaths) { + if (absPaths.length === 0) return '/'; + if (absPaths.length === 1) return path.dirname(absPaths[0]); + const segments = absPaths.map((p) => p.split(path.sep)); + const minLen = Math.min(...segments.map((s) => s.length)); + const common = []; + for (let i = 0; i < minLen; i++) { + const seg = segments[0][i]; + if (segments.every((s) => s[i] === seg)) common.push(seg); + else break; + } + return common.join(path.sep) || '/'; +} + +async function readModuleYaml(yamlAbs) { + try { + return parseYaml(await fs.readFile(yamlAbs, 'utf8')); + } catch { + return null; + } +} + +async function parseSkillFrontmatter(skillDirAbs) { + try { + const content = await fs.readFile(path.join(skillDirAbs, 'SKILL.md'), 'utf8'); + const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/); + if (!match) return { name: '', description: '' }; + const parsed = parseYaml(match[1]) || {}; + return { name: parsed.name || '', description: parsed.description || '' }; + } catch { + return { name: '', description: '' }; + } +} + +function buildSynthesizedHelpCsv(moduleName, skillInfos) { + const rows = [MODULE_HELP_CSV_HEADER]; + for (const info of skillInfos) { + const displayName = formatDisplayName(info.name || info.dirName); + const menuCode = generateMenuCode(info.name || info.dirName); + const description = escapeCsvField(info.description); + rows.push(`${moduleName},${info.dirName},${displayName},${menuCode},${description},activate,,anytime,,,false,,`); + } + return rows.join('\n') + '\n'; +} + +function formatDisplayName(name) { + const cleaned = String(name || '') + .replace(/^bmad-agent-/, '') + .replace(/^bmad-/, ''); + return cleaned + .split(/[-_]/) + .filter(Boolean) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +function generateMenuCode(name) { + const cleaned = String(name || '') + .replace(/^bmad-agent-/, '') + .replace(/^bmad-/, ''); + return cleaned + .split(/[-_]/) + .filter((w) => w.length > 0) + .map((w) => w.charAt(0).toUpperCase()) + .join('') + .slice(0, 3); +} + +function escapeCsvField(value) { + if (!value) return ''; + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return `"${value.replaceAll('"', '""')}"`; + } + return value; +} + +// Coerce an arbitrary plugin/module name into a manifest `name` that passes +// NAME_REGEX (/^[a-z][a-z0-9-]+$/, 3–64 chars): lowercase, non-[a-z0-9-] → '-', +// collapse and trim dashes, ensure it starts with a letter and is ≥3 chars. +function sanitizeName(raw) { + let s = String(raw || '') + .toLowerCase() + .replace(/[^a-z0-9-]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, ''); + if (!/^[a-z]/.test(s)) s = `bmad-${s}`.replace(/-+/g, '-').replace(/^-+|-+$/g, ''); + if (s.length < 3) s = `bmad-module-${s}`.replace(/-+$/g, ''); + return s.slice(0, 64).replace(/-+$/g, ''); +} + +function toRel(sourceDir, abs) { + return path.relative(sourceDir, abs).split(path.sep).join('/'); +} + +async function exists(abs) { + try { + await fs.access(abs); + return true; + } catch { + return false; + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/manifest-ops.mjs b/src/core-skills/bmad-module/scripts/lib/manifest-ops.mjs new file mode 100644 index 000000000..e774aa748 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/manifest-ops.mjs @@ -0,0 +1,299 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { parse as parseYaml, stringify as stringifyYaml } from './vendor/yaml.mjs'; +import { parseFrontmatter } from './frontmatter.mjs'; +import { sha256File } from './fs-safe.mjs'; +import { EXIT, BmadModuleError } from './exit.mjs'; + +// ============================================================================= +// manifest.yaml — read/write/addModule/removeModule +// ============================================================================= + +const MANIFEST_YAML_OPTS = { indent: 2, lineWidth: 0, sortKeys: false }; + +export async function readManifestYaml(bmadDir) { + const yamlPath = path.join(bmadDir, '_config', 'manifest.yaml'); + try { + const content = await fs.readFile(yamlPath, 'utf8'); + return parseYaml(content); + } catch { + return null; + } +} + +async function writeManifestYaml(bmadDir, data) { + const yamlPath = path.join(bmadDir, '_config', 'manifest.yaml'); + await fs.mkdir(path.dirname(yamlPath), { recursive: true }); + const yamlContent = stringifyYaml(structuredClone(data), MANIFEST_YAML_OPTS); + const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n'; + await fs.writeFile(yamlPath, content, 'utf8'); +} + +// Add or update a community module entry in manifest.yaml. Mirrors BMAD-METHOD's +// Manifest.addModule() entry shape exactly (source: 'community') so the +// upstream installer recognizes community rows during regeneration. +export async function addModuleToManifest(bmadDir, code, options) { + let manifest = await readManifestYaml(bmadDir); + if (!manifest) { + throw new BmadModuleError(EXIT.NO_BMAD_DIR, `manifest.yaml not found — _bmad/_config/ missing. Run \`bmad install\` first.`); + } + if (!Array.isArray(manifest.modules)) manifest.modules = []; + + const now = new Date().toISOString(); + const idx = manifest.modules.findIndex((m) => m && m.name === code); + if (idx === -1) { + const entry = { + name: code, + version: options.version || null, + installDate: now, + lastUpdated: now, + source: 'community', + npmPackage: null, + repoUrl: options.repoUrl || null, + }; + if (options.channel) entry.channel = options.channel; + if (options.sha) entry.sha = options.sha; + if (options.ref) entry.ref = options.ref; + if (options.rawSource) entry.rawSource = options.rawSource; + if (options.moduleName) entry.moduleName = options.moduleName; + manifest.modules.push(entry); + } else { + const existing = manifest.modules[idx]; + manifest.modules[idx] = { + ...existing, + version: options.version ?? existing.version, + source: 'community', + repoUrl: options.repoUrl ?? existing.repoUrl, + channel: options.channel ?? existing.channel, + sha: options.sha ?? existing.sha, + ref: options.ref ?? existing.ref, + rawSource: options.rawSource ?? existing.rawSource, + moduleName: options.moduleName ?? existing.moduleName, + lastUpdated: now, + }; + } + + await writeManifestYaml(bmadDir, manifest); +} + +export async function removeModuleFromManifest(bmadDir, code) { + const manifest = await readManifestYaml(bmadDir); + if (!manifest || !Array.isArray(manifest.modules)) return false; + const before = manifest.modules.length; + manifest.modules = manifest.modules.filter((m) => !(m && m.name === code)); + if (manifest.modules.length === before) return false; + await writeManifestYaml(bmadDir, manifest); + return true; +} + +export async function listModuleEntries(bmadDir) { + const manifest = await readManifestYaml(bmadDir); + if (!manifest || !Array.isArray(manifest.modules)) return []; + return manifest.modules.filter((m) => m && m.source === 'community'); +} + +// ============================================================================= +// CSV helpers — used by both skill-manifest.csv and files-manifest.csv +// ============================================================================= + +function escapeCsv(value) { + return `"${String(value ?? '').replaceAll('"', '""')}"`; +} + +// Tiny CSV parser sufficient for the shapes BMAD-METHOD writes: header line + +// records with `"…"` fields, quotes escaped as `""`. No commas-in-fields +// outside quotes. Returns array of arrays. +function parseCsv(text) { + const rows = []; + let row = []; + let field = ''; + let i = 0; + let inQuotes = false; + while (i < text.length) { + const c = text[i]; + if (inQuotes) { + if (c === '"') { + if (text[i + 1] === '"') { + field += '"'; + i += 2; + continue; + } + inQuotes = false; + i++; + continue; + } + field += c; + i++; + } else { + if (c === '"') { + inQuotes = true; + i++; + continue; + } + if (c === ',') { + row.push(field); + field = ''; + i++; + continue; + } + if (c === '\n' || c === '\r') { + if (field !== '' || row.length > 0) { + row.push(field); + rows.push(row); + } + row = []; + field = ''; + if (c === '\r' && text[i + 1] === '\n') i += 2; + else i++; + continue; + } + field += c; + i++; + } + } + if (field !== '' || row.length > 0) { + row.push(field); + rows.push(row); + } + return rows; +} + +async function readCsvRows(filePath) { + try { + const text = await fs.readFile(filePath, 'utf8'); + return parseCsv(text); + } catch { + return null; + } +} + +function rowsToCsv(header, rows) { + let csv = header.join(',') + '\n'; + for (const r of rows) { + csv += r.map(escapeCsv).join(',') + '\n'; + } + return csv; +} + +// ============================================================================= +// skill-manifest.csv — header: canonicalId,name,description,module,path +// ============================================================================= + +const SKILL_HEADER = ['canonicalId', 'name', 'description', 'module', 'path']; + +// Append rows for a module's skills, parsed from each SKILL.md's frontmatter. +// `skillDirs` is an array of POSIX-relative dirs inside `_bmad//` (e.g. +// `["bmad-devlog-write", "bmad-devlog-summarize"]`). +export async function appendSkillManifestRows(bmadDir, code, skillDirs) { + const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv'); + const existingRaw = await readCsvRows(csvPath); + const rows = existingRaw && existingRaw.length > 0 ? existingRaw.slice(1) : []; + + for (const skillRel of skillDirs) { + const skillMdPath = path.join(bmadDir, code, skillRel, 'SKILL.md'); + let canonicalId = path.basename(skillRel); + let name = canonicalId; + let description = ''; + try { + const md = await fs.readFile(skillMdPath, 'utf8'); + const fm = parseFrontmatter(md); + if (fm) { + if (typeof fm.name === 'string') { + canonicalId = fm.name; + name = fm.name; + } + if (typeof fm.description === 'string') { + description = fm.description.replaceAll(/\s+/g, ' ').trim(); + } + } + } catch { + /* SKILL.md unreadable — degrade gracefully with basename */ + } + rows.push([canonicalId, name, description, code, `_bmad/${code}/${skillRel}/SKILL.md`]); + } + + // Sort by (module, canonicalId) for stable diffs. Don't sort the header. + rows.sort((a, b) => { + if (a[3] !== b[3]) return a[3].localeCompare(b[3]); + return a[0].localeCompare(b[0]); + }); + + await fs.mkdir(path.dirname(csvPath), { recursive: true }); + await fs.writeFile(csvPath, rowsToCsv(SKILL_HEADER, rows), 'utf8'); +} + +// Return the canonicalIds of a module's skills currently recorded in +// skill-manifest.csv. Used by update/remove to tell ide-sync which skill +// directories to prune from the IDE targets. +export async function readSkillCanonicalIdsForModule(bmadDir, code) { + const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv'); + const rows = await readCsvRows(csvPath); + if (!rows || rows.length < 2) return []; + return rows + .slice(1) + .filter((r) => r[3] === code) + .map((r) => r[0]) + .filter(Boolean); +} + +export async function removeSkillManifestRows(bmadDir, code) { + const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv'); + const existingRaw = await readCsvRows(csvPath); + if (!existingRaw || existingRaw.length < 1) return; + const rows = existingRaw.slice(1).filter((r) => r[3] !== code); + await fs.writeFile(csvPath, rowsToCsv(SKILL_HEADER, rows), 'utf8'); +} + +// ============================================================================= +// files-manifest.csv — header: type,name,module,path,hash +// ============================================================================= + +const FILES_HEADER = ['type', 'name', 'module', 'path', 'hash']; + +// Append rows for every file copied during install. `copiedRelPaths` is the +// POSIX-relative path list returned by buildCopyList, paths relative to the +// staged module root (which == _bmad// after commit). +export async function appendFilesManifestRows(bmadDir, code, copiedRelPaths) { + const csvPath = path.join(bmadDir, '_config', 'files-manifest.csv'); + const existingRaw = await readCsvRows(csvPath); + const rows = existingRaw && existingRaw.length > 0 ? existingRaw.slice(1) : []; + + const newRows = []; + for (const rel of copiedRelPaths) { + const absPath = path.join(bmadDir, code, rel); + const ext = path.extname(rel).slice(1).toLowerCase(); + const base = path.basename(rel, path.extname(rel)); + const hash = await sha256File(absPath); + newRows.push([ext || 'file', base, code, `${code}/${rel}`, hash || '']); + } + + const merged = [...rows, ...newRows]; + merged.sort((a, b) => { + if (a[2] !== b[2]) return a[2].localeCompare(b[2]); + if (a[0] !== b[0]) return a[0].localeCompare(b[0]); + return a[1].localeCompare(b[1]); + }); + + await fs.mkdir(path.dirname(csvPath), { recursive: true }); + await fs.writeFile(csvPath, rowsToCsv(FILES_HEADER, merged), 'utf8'); +} + +// Return the existing rows for this module code as { path, hash } pairs. +// Used by update to diff old-vs-new and by remove to know what to delete. +export async function readFileEntriesForModule(bmadDir, code) { + const csvPath = path.join(bmadDir, '_config', 'files-manifest.csv'); + const rows = await readCsvRows(csvPath); + if (!rows || rows.length < 2) return []; + return rows + .slice(1) + .filter((r) => r[2] === code) + .map((r) => ({ type: r[0], name: r[1], module: r[2], path: r[3], hash: r[4] })); +} + +export async function removeFilesManifestRows(bmadDir, code) { + const csvPath = path.join(bmadDir, '_config', 'files-manifest.csv'); + const rows = await readCsvRows(csvPath); + if (!rows || rows.length < 1) return; + const kept = rows.slice(1).filter((r) => r[2] !== code); + await fs.writeFile(csvPath, rowsToCsv(FILES_HEADER, kept), 'utf8'); +} diff --git a/src/core-skills/bmad-module/scripts/lib/module-dirs.mjs b/src/core-skills/bmad-module/scripts/lib/module-dirs.mjs new file mode 100644 index 000000000..dcd41a2a4 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/module-dirs.mjs @@ -0,0 +1,128 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { parse as parseYaml } from './vendor/yaml.mjs'; + +// Create the project working directories a module declares in its module.yaml +// `directories:` key, mirroring the full installer's +// OfficialModules.createModuleDirectories (tools/installer/modules/official-modules.js). +// +// Each entry is a `{config_key}` reference resolved against the module's config +// values (produced by config-gen). `{project-root}` is stripped to a project- +// relative path; the dir is created under the project root (the parent of +// `_bmad/`). On update, a changed path moves the old directory to the new one. +// All failures are non-fatal warnings — the module itself is already installed. + +const warn = (msg) => process.stderr.write(`[bmad-module] warn: ${msg}\n`); + +async function pathExists(p) { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + +// `moduleConfig` / `existingConfig`: {configKey: resolvedValue} maps, where a +// value may carry a leading `{project-root}/`. Returns a summary for display. +export async function createModuleDirectories(bmadDir, code, moduleConfig = {}, existingConfig = {}) { + const projectRoot = path.dirname(bmadDir); + const empty = { createdDirs: [], movedDirs: [], createdWdsFolders: [] }; + + let moduleYamlRaw; + try { + moduleYamlRaw = await fs.readFile(path.join(bmadDir, code, 'module.yaml'), 'utf8'); + } catch { + return empty; // no module.yaml flattened into the install — nothing to do + } + let moduleYaml; + try { + moduleYaml = parseYaml(moduleYamlRaw); + } catch (e) { + warn(`invalid ${code}/module.yaml: ${e.message}`); + return empty; + } + if (!moduleYaml || !Array.isArray(moduleYaml.directories)) return empty; + + const wdsFolders = Array.isArray(moduleYaml.wds_folders) ? moduleYaml.wds_folders : []; + const createdDirs = []; + const movedDirs = []; + const createdWdsFolders = []; + const normalizedRoot = path.normalize(projectRoot); + + const toRelPath = (value) => path.normalize(value.replace(/^\{project-root\}\/?/, '').replaceAll('{project-root}', '')); + + for (const dirRef of moduleYaml.directories) { + const varMatch = typeof dirRef === 'string' && dirRef.match(/^\{([^}]+)\}$/); + if (!varMatch) continue; // only variable references are honored + const configKey = varMatch[1]; + const dirValue = moduleConfig[configKey]; + if (!dirValue || typeof dirValue !== 'string') continue; + + const dirPath = toRelPath(dirValue); + const fullPath = path.join(projectRoot, dirPath); + const normalizedNewAbs = path.normalize(fullPath); + if (normalizedNewAbs !== normalizedRoot && !normalizedNewAbs.startsWith(normalizedRoot + path.sep)) { + warn(`${configKey} path escapes project root, skipping: ${dirPath}`); + continue; + } + + // Detect a changed path vs the previous install for a move. + let oldFullPath = null; + let oldDirPath = null; + const oldValue = existingConfig[configKey]; + if (oldValue && typeof oldValue === 'string') { + const normalizedOld = toRelPath(oldValue); + if (normalizedOld !== dirPath) { + oldDirPath = normalizedOld; + oldFullPath = path.join(projectRoot, oldDirPath); + const normalizedOldAbs = path.normalize(oldFullPath); + if (normalizedOldAbs !== normalizedRoot && !normalizedOldAbs.startsWith(normalizedRoot + path.sep)) { + oldFullPath = null; // old path escapes root — ignore + } else if (normalizedOldAbs.startsWith(normalizedNewAbs + path.sep) || normalizedNewAbs.startsWith(normalizedOldAbs + path.sep)) { + warn(`${configKey}: cannot move between parent/child paths (${oldDirPath} / ${dirPath}); creating new directory`); + oldFullPath = null; + } + } + } + + const dirName = configKey.replaceAll('_', ' '); + const newExists = await pathExists(fullPath); + if (oldFullPath && (await pathExists(oldFullPath)) && !newExists) { + try { + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.rename(oldFullPath, fullPath); + movedDirs.push(`${dirName}: ${oldDirPath} → ${dirPath}`); + } catch (moveErr) { + warn(`failed to move ${oldDirPath} → ${dirPath}: ${moveErr.message}. Creating new directory; move contents manually.`); + await fs.mkdir(fullPath, { recursive: true }); + createdDirs.push(`${dirName}: ${dirPath}`); + } + } else if (oldFullPath && (await pathExists(oldFullPath)) && newExists) { + warn(`${dirName}: path changed but both old (${oldDirPath}) and new (${dirPath}) exist — review/merge manually.`); + } else if (!newExists) { + await fs.mkdir(fullPath, { recursive: true }); + createdDirs.push(`${dirName}: ${dirPath}`); + } + + // WDS subfolders under design_artifacts. + if (configKey === 'design_artifacts' && wdsFolders.length) { + for (const sub of wdsFolders) { + if (typeof sub !== 'string' || sub === '') continue; + const subPath = path.normalize(path.join(fullPath, sub)); + // `sub` is untrusted module content; reject traversal/absolute escapes + // out of the design_artifacts directory. + if (subPath !== normalizedNewAbs && !subPath.startsWith(normalizedNewAbs + path.sep)) { + warn(`wds_folders entry escapes design_artifacts, skipping: ${sub}`); + continue; + } + if (!(await pathExists(subPath))) { + await fs.mkdir(subPath, { recursive: true }); + createdWdsFolders.push(sub); + } + } + } + } + + return { createdDirs, movedDirs, createdWdsFolders }; +} diff --git a/src/core-skills/bmad-module/scripts/lib/npm-deps.mjs b/src/core-skills/bmad-module/scripts/lib/npm-deps.mjs new file mode 100644 index 000000000..81e5546b5 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/npm-deps.mjs @@ -0,0 +1,41 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; + +const execFileP = promisify(execFile); + +// Install a module's runtime dependencies when it ships a package.json, mirroring +// the full installer's CustomModuleManager.cloneRepo npm step +// (tools/installer/modules/custom-module-manager.js). Unlike the installer — which +// installs into its ~/.bmad cache and copies node_modules across — the skill never +// copies node_modules (it's in DEFAULT_IGNORES), so we install in place inside the +// committed `_bmad//`. +// +// This relaxes the skill's "no npm in _bmad/" principle, but it is the only way a +// module whose skills shell out to JS deps works after a skill-driven install. +// Gated on package.json presence and opt-out via `bmad.install.skipNpm: true`. +// Always non-fatal: the module files are already committed; a failed dep install +// is a warning, not an install failure. + +const NPM_ARGS = ['install', '--omit=dev', '--no-audit', '--no-fund', '--no-progress', '--legacy-peer-deps']; +const TIMEOUT_MS = 120_000; + +export async function installModuleDeps(moduleDir, manifest) { + if (manifest?.bmad?.install?.skipNpm === true) return { ran: false, skipped: 'opted out (bmad.install.skipNpm)' }; + + const pkgPath = path.join(moduleDir, 'package.json'); + try { + const stat = await fs.stat(pkgPath); + if (!stat.isFile()) return { ran: false }; + } catch { + return { ran: false }; // no package.json — nothing to install + } + + try { + await execFileP('npm', NPM_ARGS, { cwd: moduleDir, timeout: TIMEOUT_MS }); + return { ran: true, ok: true }; + } catch (e) { + return { ran: true, ok: false, error: e.shortMessage || e.message }; + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/plugin-json.mjs b/src/core-skills/bmad-module/scripts/lib/plugin-json.mjs new file mode 100644 index 000000000..998ea53ce --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/plugin-json.mjs @@ -0,0 +1,113 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { valid as semverValid, validRange as semverValidRange } from './semver-lite.mjs'; +import { EXIT, BmadModuleError } from './exit.mjs'; + +// Reserved bmad.code values — must match the validator's RESERVED_CODES +// set. Single source of truth for the runtime. +export const RESERVED_CODES = new Set([ + 'core', + 'bmm', + 'bmb', + 'cis', + 'gds', + 'tea', + 'wds', + 'automator', + '_config', + '_memory', + 'custom', + 'agents', + 'hooks', + 'config', + 'commands', + 'skills', +]); + +export const CODE_REGEX = /^[a-z][a-z0-9-]{1,31}$/; +export const NAME_REGEX = /^[a-z][a-z0-9-]+$/; + +// Read and install-time-validate a module manifest. Install-time checks are +// intentionally narrower than the author validator (scripts/validate-module.mjs) +// — we only block things that would corrupt _bmad/ or cause data loss. +export async function readAndValidateManifest(sourceDir) { + const manifestPath = path.join(sourceDir, '.claude-plugin', 'plugin.json'); + let raw; + try { + raw = await fs.readFile(manifestPath, 'utf8'); + } catch { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `missing .claude-plugin/plugin.json at ${sourceDir}`); + } + let m; + try { + m = JSON.parse(raw); + } catch (e) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `plugin.json failed to parse: ${e.message}`); + } + return validateManifestObject(m); +} + +// Validate an already-parsed manifest object against the install-time rules. +// Shared by readAndValidateManifest (new-spec, from disk) and the legacy +// resolver (which synthesizes a manifest from marketplace.json + module.yaml). +// `allowReserved` lets the legacy path install first-party modules whose codes +// (gds, bmm, cis, …) are reserved against new-spec community authors. Returns +// the validated object. +export function validateManifestObject(m, { allowReserved = false } = {}) { + const missing = []; + if (typeof m.name !== 'string') missing.push('name'); + if (typeof m.version !== 'string') missing.push('version'); + if (typeof m.description !== 'string') missing.push('description'); + if (!m.bmad || typeof m.bmad !== 'object') { + missing.push('bmad'); + } else { + if (typeof m.bmad.specVersion !== 'string') missing.push('bmad.specVersion'); + if (typeof m.bmad.code !== 'string') missing.push('bmad.code'); + if (typeof m.bmad.compatibility?.bmadMethod !== 'string') missing.push('bmad.compatibility.bmadMethod'); + } + if (missing.length) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `plugin.json missing required fields: ${missing.join(', ')}`); + } + + if (!NAME_REGEX.test(m.name) || m.name.length < 3 || m.name.length > 64) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `plugin.json#name "${m.name}" must match ${NAME_REGEX} and be 3–64 chars`); + } + if (!semverValid(m.version)) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `plugin.json#version "${m.version}" is not valid semver`); + } + if (!CODE_REGEX.test(m.bmad.code)) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `plugin.json#bmad.code "${m.bmad.code}" must match ${CODE_REGEX}`); + } + if (!allowReserved && RESERVED_CODES.has(m.bmad.code)) { + throw new BmadModuleError(EXIT.RESERVED_PREFIX, `plugin.json#bmad.code "${m.bmad.code}" is reserved`); + } + if (!semverValidRange(m.bmad.compatibility.bmadMethod)) { + throw new BmadModuleError( + EXIT.BAD_MANIFEST, + `plugin.json#bmad.compatibility.bmadMethod "${m.bmad.compatibility.bmadMethod}" is not a valid semver range`, + ); + } + + return m; +} + +// Probe whether a source dir carries a new-spec manifest — a parseable +// `.claude-plugin/plugin.json` with a `bmad{}` block. Returns false when the +// file is absent or has no `bmad` object (→ caller tries the legacy resolver), +// and true on parse failure so a malformed new manifest surfaces via +// readAndValidateManifest rather than being silently treated as legacy. +export async function hasBmadPluginJson(sourceDir) { + const manifestPath = path.join(sourceDir, '.claude-plugin', 'plugin.json'); + let raw; + try { + raw = await fs.readFile(manifestPath, 'utf8'); + } catch { + return false; + } + try { + const m = JSON.parse(raw); + return !!(m && typeof m === 'object' && m.bmad && typeof m.bmad === 'object'); + } catch { + return true; + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/semver-lite.mjs b/src/core-skills/bmad-module/scripts/lib/semver-lite.mjs new file mode 100644 index 000000000..996a405c1 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/semver-lite.mjs @@ -0,0 +1,147 @@ +// semver-lite — zero-dependency stand-ins for the two semver functions this +// skill needs. We deliberately do NOT vendor the `semver` package: its only use +// here is install-time validation of an author's plugin.json (`valid` on +// #version, `validRange` on #bmad.compatibility.bmadMethod). That is bounded, +// low-severity input checking — unlike manifest.yaml round-tripping, which is +// co-owned with BMAD core and so keeps the REAL `yaml` library (see vendor/). +// +// `node:`-only, matching every other script BMAD installs. Both functions are +// generous: a wrong *reject* would fail a legitimate install, so where semver +// is lenient we are lenient too. Parity with the real `semver` is asserted by a +// battery in the repo's skill tests; keep that battery green if you edit this. + +// Official SemVer 2.0.0 grammar (semver.org), with an optional leading `v` to +// match semver's parser. Build metadata is accepted but dropped from the +// normalized return, exactly like `semver.valid()`. +const SEMVER_RE = + /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/; + +/** + * Mirror of `semver.valid()`: returns the normalized version string for a valid + * full semver, otherwise null. Build metadata is stripped from the result. + */ +export function valid(version) { + if (typeof version !== 'string') return null; + const m = SEMVER_RE.exec(version.trim()); + if (!m) return null; + return `${m[1]}.${m[2]}.${m[3]}${m[4] ? `-${m[4]}` : ''}`; +} + +/** + * Mirror of `semver.prerelease()`: returns the array of dot-separated + * prerelease identifiers (numeric ones coerced to Number) for a valid version, + * or null when the version is invalid or has no prerelease. Used by the channel + * resolver to exclude `-alpha`/`-rc` tags from the stable channel. + */ +export function prerelease(version) { + if (typeof version !== 'string') return null; + const m = SEMVER_RE.exec(version.trim()); + if (!m || !m[4]) return null; + return m[4].split('.').map((id) => (/^\d+$/.test(id) ? Number(id) : id)); +} + +// Numeric compare of the three core fields, then prerelease precedence per +// SemVer §11. Returns -1, 0, or 1. Both inputs must be valid semver. +function compareValid(a, b) { + const ma = SEMVER_RE.exec(a); + const mb = SEMVER_RE.exec(b); + for (let i = 1; i <= 3; i++) { + const d = Number(ma[i]) - Number(mb[i]); + if (d !== 0) return d < 0 ? -1 : 1; + } + // A version WITH a prerelease has lower precedence than one without. + const pa = ma[4]; + const pb = mb[4]; + if (!pa && !pb) return 0; + if (!pa) return 1; + if (!pb) return -1; + const ia = pa.split('.'); + const ib = pb.split('.'); + const len = Math.min(ia.length, ib.length); + for (let i = 0; i < len; i++) { + if (ia[i] === ib[i]) continue; + const na = /^\d+$/.test(ia[i]); + const nb = /^\d+$/.test(ib[i]); + if (na && nb) return Number(ia[i]) < Number(ib[i]) ? -1 : 1; + // Numeric identifiers always have lower precedence than alphanumeric ones. + if (na) return -1; + if (nb) return 1; + return ia[i] < ib[i] ? -1 : 1; + } + // A larger set of prerelease fields wins when all preceding are equal. + return ia.length === ib.length ? 0 : ia.length < ib.length ? -1 : 1; +} + +/** + * Mirror of `semver.compare()`: -1/0/1 for ab. Returns null when + * either side is not valid semver (semver throws; callers here treat unknown as + * "don't reorder"). + */ +export function compare(a, b) { + const va = valid(a); + const vb = valid(b); + if (va === null || vb === null) return null; + return compareValid(va, vb); +} + +/** + * Mirror of `semver.rcompare()`: reverse of compare(), for sorting newest-first. + */ +export function rcompare(a, b) { + const c = compare(a, b); + return c === null ? 0 : -c; +} + +// ---- range grammar ------------------------------------------------------- +// A "partial" is a 1–3 segment version where each segment may be a number or an +// x-range wildcard (x/X/*), with optional prerelease/build on the full form. +const XID = '(?:0|[1-9]\\d*|\\d+|[xX*])'; +const PRE = '(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?'; +const BUILD = '(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?'; +const PARTIAL_RE = new RegExp(`^v?${XID}(?:\\.${XID}(?:\\.${XID}${PRE}${BUILD})?)?$`); + +function isPartial(v) { + return v !== '' && PARTIAL_RE.test(v); +} + +// One comparator: an optional operator (>=, <=, >, <, =, ~, ^) glued to a +// partial version. Bare `*`/`x`/`X` (or empty) means "any". +function isComparator(tok) { + if (tok === '' || tok === '*' || tok === 'x' || tok === 'X') return true; + // `~>` is a semver-supported alias for `~`; match it before `~`/`>`. + const m = /^(~>|>=|<=|>|<|=|~|\^)?(.*)$/.exec(tok); + return isPartial(m[2]); +} + +// One comparator set (the AND-joined part of a `||` union). +function isComparatorSet(set) { + if (set === '' || set === '*') return true; + // Hyphen range: " - " (spaces required around the dash). + const hy = /^(.+?)\s+-\s+(.+)$/.exec(set); + if (hy) return isPartial(hy[1].trim()) && isPartial(hy[2].trim()); + // Collapse whitespace after an operator so ">= 1.2.3" is a single comparator, + // then split the remaining intersection on whitespace. + const tokens = set + .replace(/(~>|>=|<=|>|<|=|~|\^)\s+/g, '$1') + .split(/\s+/) + .filter(Boolean); + // `[].every()` is already true, so an empty intersection means "any". + return tokens.every((t) => isComparator(t)); +} + +/** + * Mirror of `semver.validRange()` as a validator: returns the input range when + * it is a syntactically valid semver range, otherwise null. (We return the + * original string rather than semver's normalized form — callers here only test + * truthiness.) + */ +export function validRange(range) { + if (typeof range !== 'string') return null; + const r = range.trim(); + if (r === '') return '*'; + const groups = r.split(/\s*\|\|\s*/); + for (const g of groups) { + if (!isComparatorSet(g.trim())) return null; + } + return range; +} diff --git a/src/core-skills/bmad-module/scripts/lib/source.mjs b/src/core-skills/bmad-module/scripts/lib/source.mjs new file mode 100644 index 000000000..6a4b143d3 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/source.mjs @@ -0,0 +1,256 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import os from 'node:os'; +import { copyDir, safePathInsideRoot } from './fs-safe.mjs'; +import { ensureCachedRepo } from './cache.mjs'; +import { EXIT, BmadModuleError } from './exit.mjs'; + +const GH_SHORT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9._-]+$/; +// A `@` tail is ref-shaped (no `:` so we never eat the auth +// segment of `git@host:…`). Raw commit SHAs are not supported — `git clone +// --branch` can't take them; pass a tag/branch or check the SHA out manually. +const REF_TAIL_RE = /^[\w.\-+/]+$/; + +// Normalize a `` argument from the CLI into a descriptor: +// { kind: 'local' | 'git', path?, url?, subdir, ref, cacheKey, displayName, rawInput } +// `ref` is a branch/tag extracted from an explicit `@` suffix or an +// embedded browser-URL path (`…/tree/`); `subdir` is a module location +// inside the repo extracted from a deep-path / `?path=` URL. Accepts: +// - owner/repo[@ref] → GitHub HTTPS +// - https://…, http://…, git@…, ssh://, git:// → as given (ref/subdir parsed) +// - file://path → local +// - relative or absolute path → local (if it exists on disk) +// Mirrors tools/installer/modules/custom-module-manager.js#parseSource, adapted +// to the skill's throw-on-invalid contract and node:-only deps; keeps the skill's +// `owner/repo` shorthand, which the installer (URL/marketplace-driven) lacks. +export function parseSource(input) { + if (typeof input !== 'string' || !input.trim()) { + throw new BmadModuleError(EXIT.USAGE, `source is required`); + } + const rawInput = input.trim(); + + // Split off an optional @ suffix, but only when the part before the `@` + // looks like a complete repo reference — so we don't disturb `git@host:…` or + // an `@` that's part of the path. + let body = rawInput; + let ref = null; + const lastAt = rawInput.lastIndexOf('@'); + if (lastAt > 0) { + const candidate = rawInput.slice(lastAt + 1); + const before = rawInput.slice(0, lastAt); + if (REF_TAIL_RE.test(candidate) && !candidate.includes(':')) { + const beforeLooksLikeRepo = + before.startsWith('/') || + before.startsWith('./') || + before.startsWith('../') || + before.startsWith('~') || + before.startsWith('file://') || + /^(?:https?|ssh|git):\/\//i.test(before) || + /^git@[^:]+:.+/.test(before) || + GH_SHORT_RE.test(before); + if (beforeLooksLikeRepo) { + ref = candidate; + body = before; + } + } + } + + if (body.startsWith('file://')) { + if (ref) throw new BmadModuleError(EXIT.USAGE, `local paths do not support @ref suffixes`); + const p = decodeURI(body.slice('file://'.length)); + return localDescriptor(path.resolve(p), p, rawInput); + } + + // Local path: starts with /, ./, ../, or ~. + if (body.startsWith('/') || body.startsWith('./') || body.startsWith('../') || body.startsWith('~')) { + if (ref) throw new BmadModuleError(EXIT.USAGE, `local paths do not support @ref suffixes`); + const expanded = body.startsWith('~') ? path.join(os.homedir(), body.slice(1)) : body; + return localDescriptor(path.resolve(expanded), body, rawInput); + } + + // SSH: git@host:owner/repo[.git] + const sshMatch = body.match(/^git@([^:]+):(.+?)\/([^/.]+?)(?:\.git)?$/); + if (sshMatch) { + const [, host, owner, repo] = sshMatch; + return { + kind: 'git', + url: body, + subdir: null, + ref, + cacheKey: `${host}/${owner}/${repo}`, + displayName: `${owner}/${repo}`, + rawInput, + }; + } + + // HTTP(S) / ssh:// / git:// URLs — parse with the URL API so any host, nested + // group, dotted repo name, or browse-link shape is handled host-agnostically. + if (/^(?:https?|ssh|git):\/\//i.test(body)) { + return parseUrlDescriptor(body, ref, rawInput); + } + + // owner/repo shorthand → GitHub HTTPS. + if (GH_SHORT_RE.test(body)) { + return { + kind: 'git', + url: `https://github.com/${body}`, + subdir: null, + ref, + cacheKey: `github.com/${body}`, + displayName: body, + rawInput, + }; + } + + throw new BmadModuleError(EXIT.USAGE, `not a valid module source (owner/repo, git URL, or local path): ${rawInput}`); +} + +function localDescriptor(absPath, displayName, rawInput) { + return { kind: 'local', path: absPath, subdir: null, ref: null, cacheKey: null, displayName, rawInput }; +} + +// Browser-style deep paths that embed a ref (branch/tag/commit) and optional +// subdirectory, across hosts: +// GitHub //tree|blob/[/] +// GitLab //-/tree|blob/[/] +// Gitea //src/[branch|commit|tag/][/] +// Group 1 = repo path prefix, 2 = ref, 3 = subdir (optional). +const DEEP_PATH_PATTERNS = [ + /^(.+?)\/(?:-\/)?(?:tree|blob)\/([^/]+)(?:\/(.+))?$/, + /^(.+?)\/src\/(?:branch\/|commit\/|tag\/)?([^/]+)(?:\/(.+))?$/, +]; + +function parseUrlDescriptor(body, refFromSuffix, rawInput) { + let url; + try { + url = new URL(body); + } catch { + url = null; + } + if (!url || !url.host) { + throw new BmadModuleError(EXIT.USAGE, `not a valid Git URL: ${rawInput}`); + } + + const host = url.host; + let repoPath = url.pathname.replace(/^\/+/, '').replace(/\/+$/, ''); + let subdir = null; + let urlRef = null; + + for (const pattern of DEEP_PATH_PATTERNS) { + const m = repoPath.match(pattern); + if (m) { + repoPath = m[1]; + if (m[2]) urlRef = m[2]; + if (m[3]) { + const cleaned = m[3].replace(/\/+$/, ''); + if (cleaned) subdir = cleaned; + } + break; + } + } + + // Some hosts use ?path=/subdir on browse links. + if (!subdir) { + const pathParam = url.searchParams.get('path'); + if (pathParam) { + const cleaned = pathParam.replace(/^\/+/, '').replace(/\/+$/, ''); + if (cleaned) subdir = cleaned; + } + } + + const repoPathClean = repoPath.replace(/\.git$/i, ''); + if (!repoPathClean) { + throw new BmadModuleError(EXIT.USAGE, `not a valid Git URL: ${rawInput}`); + } + + const segments = repoPathClean.split('/').filter(Boolean); + const displayName = segments.length >= 2 ? `${segments.at(-2)}/${segments.at(-1)}` : segments.at(-1); + + return { + kind: 'git', + url: `${url.protocol}//${host}/${repoPathClean}`, + subdir, + // Explicit @ref suffix wins over an embedded /tree/ path segment. + ref: refFromSuffix || urlRef || null, + cacheKey: `${host}/${repoPathClean}`, + displayName, + rawInput, + }; +} + +// Files that should never be staged into _bmad// from a source tree. +const STAGE_IGNORE = (rel) => + rel === '.git' || + rel.startsWith('.git/') || + rel === 'node_modules' || + rel.startsWith('node_modules/') || + rel === '.bmad-source.json' || + rel === '.bmad-channel.json'; + +// Resolve a parsed descriptor into a usable source directory on disk. +// +// Always returns a throwaway temp working copy so the install pipeline can write +// into it (e.g. synthesized module.yaml for legacy strategy 5) and stage from it +// without mutating the user's tree or the shared clone cache. +// - local: copies the directory into the temp working copy. +// - git: ensures the repo is in the shared cache (~/.bmad/cache/custom-modules), +// then copies the module root (the subdir if the source URL named one) out of +// the cache into the temp working copy. +// +// Returns { dir, sha, ref, cleanup } where `sha`/`ref` are null for local +// sources and `cleanup()` removes the temp working copy (never the cache). +export async function materializeSource(descriptor, opts = {}) { + const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-module-')); + const dir = path.join(tmpRoot, 'src'); + const cleanup = () => fs.rm(tmpRoot, { recursive: true, force: true }); + + if (descriptor.kind === 'local') { + const srcStat = await fs.stat(descriptor.path).catch(() => null); + if (!srcStat || !srcStat.isDirectory()) { + await cleanup(); + throw new BmadModuleError(EXIT.USAGE, `local source not a directory: ${descriptor.path}`); + } + try { + await copyDir(descriptor.path, dir, STAGE_IGNORE); + } catch (e) { + await cleanup(); + throw e; + } + return { dir, sha: null, ref: null, cleanup }; + } + + // git — explicit --ref/resolved channel wins over a ref parsed from the source. + const ref = opts.ref ?? descriptor.ref ?? null; + let cached; + try { + cached = await ensureCachedRepo(descriptor, ref); + } catch (e) { + await cleanup(); + throw e; + } + + // A subdir parsed from the source URL (/tree// or ?path=) is + // untrusted: reject `..`/absolute/symlink escapes so it can't copy out of the + // shared clone cache. safePathInsideRoot returns null on any escape. + let moduleRoot = cached.repoDir; + if (descriptor.subdir) { + moduleRoot = safePathInsideRoot(cached.repoDir, descriptor.subdir); + if (!moduleRoot) { + await cleanup(); + throw new BmadModuleError(EXIT.PATH_TRAVERSAL, `subdirectory "${descriptor.subdir}" escapes the repository root`); + } + } + const rootStat = await fs.stat(moduleRoot).catch(() => null); + if (!rootStat || !rootStat.isDirectory()) { + await cleanup(); + throw new BmadModuleError(EXIT.USAGE, `subdirectory "${descriptor.subdir}" not found in ${descriptor.displayName}`); + } + + try { + await copyDir(moduleRoot, dir, STAGE_IGNORE); + } catch (e) { + await cleanup(); + throw e; + } + return { dir, sha: cached.sha, ref: cached.ref, cleanup }; +} diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/README.md b/src/core-skills/bmad-module/scripts/lib/vendor/README.md new file mode 100644 index 000000000..6d63dfd56 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/README.md @@ -0,0 +1,55 @@ +# `lib/vendor/` — vendored runtime dependencies + +This directory holds **self-contained, generated** copies of third-party libraries the skill needs at runtime. They are committed on purpose. + +## Why vendor at all? + +The `bmad-module` skill is **copied into a user's project** — into the IDE skills directories chosen at `bmad install` time (e.g. `.claude/skills/bmad-module/`, `.agents/skills/bmad-module/`) — by `npx bmad-method install`. The installer: + +- strips `node_modules` while copying (`tools/installer/core/installer.js`), +- ships **no** `package.json` under the skill, and +- never runs `npm install` inside `_bmad/`. + +So a bare `import 'yaml'` cannot resolve at runtime — it throws `ERR_MODULE_NOT_FOUND` before any of the skill's exit codes can fire. Every other script BMAD installs is zero-third-party-dependency; vendoring keeps this skill self-sufficient the same way, without setup. + +Files here are imported by **relative path** (`./vendor/yaml.mjs`), which resolves regardless of cwd, install location, or `node_modules` presence. + +## What's vendored — and what's NOT + +| Need | Strategy | Where | +|---|---|---| +| `yaml` (parse/stringify `_bmad/_config/manifest.yaml`) | **vendored, real library** | `vendor/yaml.mjs` | +| BMAD's IDE-distribution engine (push skills to `.claude/skills/` etc.) | **bundled, real engine** | `vendor/ide-sync.mjs` (+ `vendor/platform-codes.yaml`) | +| `semver` (`valid` + `validRange` on `plugin.json`) | **dropped** — hand-rolled, `node:` only | `../semver-lite.mjs` | + +`manifest.yaml` is **co-owned** with BMAD core, which reads/writes it with the same `yaml` package and the same `{indent:2, lineWidth:0}` options (`tools/installer/core/manifest.js`). Hand-rolling a YAML emitter risks diverging from that on the user's live install state, so we ship the **real** library and verify byte-identical output in `build-vendor.mjs`. `semver` is only input-validation of an author's manifest, so it is safe to hand-roll. + +## `yaml.mjs` + +- **GENERATED — do not edit by hand.** An esbuild single-file bundle of the `yaml` npm package (eemeli/yaml), tree-shaken to just `parse` + `stringify`. +- The exact pinned version and build provenance are in the file's header. +- Upstream license is retained inline (`legalComments: 'inline'`). + +### Regenerating + +After bumping `yaml` (or esbuild) in the repo's **root** `package.json` + `npm install`: + +```bash +npm run vendor:build # regenerate this yaml.mjs +npm run vendor:check # verify it's in sync (what CI runs) +``` + +## `ide-sync.mjs` (+ `platform-codes.yaml`) + +- **GENERATED — do not edit by hand.** An esbuild bundle of BMAD's real IDE-distribution engine (`tools/installer/ide/*` reached via `tools/installer/core/ide-sync.js`), tree-shaken to a single dependency-free ESM file. Built by `build-ide-sync.mjs`; `../prompts` and `../project-root` are aliased to the small shims in `shims/` so the interactive `@clack/prompts` and installer-only graphs are dropped. +- After `bmad-module` stages a module under `_bmad/`, it execs this bundle to copy the module's skills into the IDE directories the user chose (read from `_bmad/_config/manifest.yaml`). Reasons it's bundled rather than imported are the same as `yaml.mjs`: the skill ships into projects without `node_modules`, and shelling out to `npx bmad-method` would reintroduce a network/npm dependency. +- `platform-codes.yaml` is copied verbatim beside the bundle (the engine reads it at runtime via `$BMAD_IDE_PLATFORM_CODES`, set by the bundle entry). +- Same engine backs `bmad ide-sync` and the full installer's IDE setup, so the three stay in lockstep; `vendor:check` byte-verifies the bundle against source and `test/test-ide-sync.js` checks engine/bundle behavior parity. + +Regenerated by the same commands as above (`vendor:build` / `vendor:check` run both bundles). + +The build is **deterministic** for a given `yaml` + `esbuild` version (both pinned in the lockfile) and self-checks a parse→stringify round-trip. + +**You don't have to remember to do this.** `vendor:check` is wired into `npm test` (husky pre-commit) and `npm run quality` (the `validate` job in `.github/workflows/quality.yaml`). If the committed bundle drifts from the installed `yaml`/`esbuild` version, those gates fail with a message telling you to run `npm run vendor:build` — so a bump can't land with a stale bundle, and manifest writes stay byte-identical between BMAD core and this skill. + +Lint/format intentionally ignore this directory (see `eslint.config.mjs` global ignores and `.prettierignore`) — it is generated, not authored. diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/build-ide-sync.mjs b/src/core-skills/bmad-module/scripts/lib/vendor/build-ide-sync.mjs new file mode 100644 index 000000000..f3b2c2e80 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/build-ide-sync.mjs @@ -0,0 +1,192 @@ +#!/usr/bin/env node +// build-ide-sync — regenerates (and, with --check, verifies) the self-contained +// `ide-sync.mjs` bundle this skill ships, plus its sidecar `platform-codes.yaml`. +// +// Why this exists: after the bmad-module skill installs/updates/removes a +// community module under `_bmad/`, it must distribute that module's skills to +// exactly the coding assistants the user chose at `bmad install` time. The real +// distribution engine lives in `tools/installer/ide/*` (IdeManager / +// ConfigDrivenIdeSetup / platform-codes.yaml), but that code — and its +// dependencies (csv-parse, yaml, @clack/prompts) — is NOT present in a user's +// project (the installer ships the skill without node_modules). So we bundle the +// REAL engine into one dependency-free ESM file with esbuild (the same toolchain +// and vendoring philosophy as yaml.mjs), aliasing `../prompts` and +// `../project-root` to tiny shims so the interactive/heavy bits are dropped. +// +// The skill execs `vendor/ide-sync.mjs` from inside the user's project — no npx, +// no network, no node_modules. Because it is built from `tools/installer/ide/*` +// (not hand-forked) and verified by `vendor:check`, it can never silently drift +// from the engine the interactive installer uses. +// +// Usage (via root package.json): +// npm run vendor:build # regenerate ide-sync.mjs + platform-codes.yaml +// npm run vendor:check # fail if the committed bundle is stale (CI gate) + +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const require = createRequire(import.meta.url); +const vendorDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(vendorDir, '../../../../../..'); // -> repo root +const installerDir = path.join(repoRoot, 'tools', 'installer'); +const ideDir = path.join(installerDir, 'ide'); + +const outfile = path.join(vendorDir, 'ide-sync.mjs'); +const sidecarOut = path.join(vendorDir, 'platform-codes.yaml'); +const sidecarSrc = path.join(ideDir, 'platform-codes.yaml'); + +const promptsShim = path.join(vendorDir, 'shims', 'prompts.cjs'); +const projectRootShim = path.join(vendorDir, 'shims', 'project-root.cjs'); + +const checkMode = process.argv.includes('--check'); + +const esbuild = await import('esbuild'); +const esbuildVersion = require('esbuild/package.json').version; +const yamlVersion = require('yaml/package.json').version; +// csv-parse restricts ./package.json via its "exports" map, so read it directly. +const csvVersion = JSON.parse(await fs.readFile(path.join(repoRoot, 'node_modules', 'csv-parse', 'package.json'), 'utf8')).version; + +// Redirect the engine's interactive/installer-only deps to lightweight shims so +// @clack/prompts and the custom-module-manager graph never enter the bundle. +const aliasPlugin = { + name: 'ide-sync-aliases', + setup(build) { + build.onResolve({ filter: /^\.\.\/prompts$/ }, () => ({ path: promptsShim })); + build.onResolve({ filter: /^\.\.\/project-root$/ }, () => ({ path: projectRootShim })); + }, +}; + +// NOTE: no builder-specific data (node version, timestamp) in the banner — the +// output must be reproducible so --check can byte-compare. +const banner = `// ============================================================================ +// GENERATED — DO NOT EDIT BY HAND. Run \`npm run vendor:build\` to regenerate. +// Self-contained bundle of BMAD's IDE-distribution engine +// (tools/installer/ide/* via tools/installer/core/ide-sync.js). +// +// bundler : esbuild ${esbuildVersion} +// yaml : ${yamlVersion} +// csv-parse : ${csvVersion} +// +// Shipped because the bmad-module skill is copied into projects without +// node_modules; see build-ide-sync.mjs and vendor/README.md for the rationale. +// Reads platform-codes.yaml from beside this file (or $BMAD_IDE_PLATFORM_CODES). +// ============================================================================ +// Provide a real \`require\` so esbuild's CJS interop can load node: builtins +// (node:path/fs/os/crypto) from this ESM bundle. All third-party deps are +// inlined, so only builtins ever reach this require. +import { createRequire as __createRequire } from 'node:module'; +const require = __createRequire(import.meta.url); +`; + +// The entry sets the platform-codes path to the sidecar beside the bundle, then +// runs the CLI. Imports are hoisted/initialised first; the engine reads the env +// var lazily (loadPlatformCodes), by which point the body below has set it. +const entryContents = ` +import { runIdeSyncCli } from './core/ide-sync.js'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dir = dirname(fileURLToPath(import.meta.url)); +if (!process.env.BMAD_IDE_PLATFORM_CODES) { + process.env.BMAD_IDE_PLATFORM_CODES = join(__dir, 'platform-codes.yaml'); +} + +runIdeSyncCli(process.argv.slice(2)) + .then((code) => process.exit(code)) + .catch((err) => { + process.stderr.write('[ide-sync] ' + ((err && err.stack) || err) + '\\n'); + process.exit(1); + }); +`; + +const result = await esbuild.build({ + stdin: { + contents: entryContents, + resolveDir: installerDir, // so './core/ide-sync.js' resolves + sourcefile: 'ide-sync-entry.mjs', + loader: 'js', + }, + bundle: true, + format: 'esm', + platform: 'node', + target: 'node20', + minify: false, + charset: 'utf8', + legalComments: 'inline', + banner: { js: banner }, + plugins: [aliasPlugin], + write: false, +}); +const built = result.outputFiles[0].text; +const sidecar = await fs.readFile(sidecarSrc, 'utf8'); + +// Self-check: the freshly built bundle must distribute a fixture skill to a +// selected IDE with no node_modules on its resolution path (the runtime +// condition). Build a throwaway project, run the bundle, assert the output. +await selfCheck(built, sidecar); + +if (checkMode) { + const currentBundle = await fs.readFile(outfile, 'utf8').catch(() => null); + const currentSidecar = await fs.readFile(sidecarOut, 'utf8').catch(() => null); + if (currentBundle === built && currentSidecar === sidecar) { + process.stdout.write(`vendor:check OK — ide-sync.mjs matches engine (esbuild ${esbuildVersion})\n`); + process.exit(0); + } + process.stderr.write( + `vendor:check FAILED — vendor/ide-sync.mjs or platform-codes.yaml is stale or hand-edited.\n` + + ` The committed bundle no longer matches tools/installer/ide/*.\n` + + ` Fix: run \`npm run vendor:build\` and commit the regenerated files.\n`, + ); + process.exit(1); +} + +await fs.writeFile(outfile, built, 'utf8'); +await fs.writeFile(sidecarOut, sidecar, 'utf8'); +process.stdout.write(`built ide-sync.mjs + platform-codes.yaml (self-check OK, esbuild ${esbuildVersion})\n`); + +// --------------------------------------------------------------------------- + +async function selfCheck(bundleText, sidecarText) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-ide-sync-check-')); + try { + // Lay down the bundle + sidecar together. + await fs.writeFile(path.join(dir, 'ide-sync.mjs'), bundleText, 'utf8'); + await fs.writeFile(path.join(dir, 'platform-codes.yaml'), sidecarText, 'utf8'); + + // Minimal project: one community skill recorded in the manifests. + const proj = path.join(dir, 'proj'); + const skillDir = path.join(proj, '_bmad', 'demo', 'skills', 'bmad-demo-skill'); + await fs.mkdir(skillDir, { recursive: true }); + await fs.mkdir(path.join(proj, '_bmad', '_config'), { recursive: true }); + await fs.writeFile(path.join(skillDir, 'SKILL.md'), '---\nname: bmad-demo-skill\ndescription: demo\n---\nbody\n', 'utf8'); + await fs.writeFile( + path.join(proj, '_bmad', '_config', 'manifest.yaml'), + 'installation:\n version: "0.0.0"\nmodules:\n - name: demo\n source: community\nides:\n - claude-code\n', + 'utf8', + ); + await fs.writeFile( + path.join(proj, '_bmad', '_config', 'skill-manifest.csv'), + 'canonicalId,name,description,module,path\n"bmad-demo-skill","bmad-demo-skill","demo","demo","_bmad/demo/skills/bmad-demo-skill/SKILL.md"\n', + 'utf8', + ); + + const { spawn } = await import('node:child_process'); + const code = await new Promise((resolve) => { + const child = spawn(process.execPath, [path.join(dir, 'ide-sync.mjs'), '-d', proj], { + stdio: process.env.BMAD_IDE_SYNC_DEBUG ? 'inherit' : 'ignore', + }); + child.on('close', resolve); + }); + if (code !== 0) throw new Error(`ide-sync self-check: bundle exited ${code}`); + + const distributed = path.join(proj, '.claude', 'skills', 'bmad-demo-skill', 'SKILL.md'); + await fs.access(distributed).catch(() => { + throw new Error('ide-sync self-check FAILED: skill was not distributed to .claude/skills'); + }); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } +} diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/build-vendor.mjs b/src/core-skills/bmad-module/scripts/lib/vendor/build-vendor.mjs new file mode 100644 index 000000000..5f2560f12 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/build-vendor.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +// build-vendor — regenerates (and, with --check, verifies) the vendored, +// self-contained copy of the `yaml` library this skill ships. +// +// Why this exists: the bmad-module skill is COPIED into a user's project at +// `_bmad/core/skills/bmad-module/` by `npx bmad-method install`. The installer +// strips `node_modules` (tools/installer/core/installer.js), ships no +// package.json under the skill, and never runs `npm install` in `_bmad/`. So a +// bare `import 'yaml'` cannot resolve at runtime. Every other script BMAD +// installs is zero-third-party-dep; this is the one library we cannot safely +// hand-roll, because `_bmad/_config/manifest.yaml` is CO-OWNED with BMAD core +// (tools/installer/core/manifest.js writes it with the same `yaml` package and +// the same {indent:2,lineWidth:0} options). Vendoring the REAL library is the +// only way to guarantee byte-identical round-trips. +// +// The output `yaml.mjs` is imported by RELATIVE path from manifest-ops.mjs and +// frontmatter.mjs, so it resolves regardless of cwd, install location, or +// node_modules presence. +// +// Usage (via root package.json): +// npm run vendor:build # regenerate scripts/lib/vendor/yaml.mjs +// npm run vendor:check # fail if the committed bundle is stale (CI gate) +// +// The output is DETERMINISTIC for a given yaml + esbuild version (both pinned in +// the lockfile), which is what lets `--check` byte-compare. Bumping `yaml` (or +// esbuild) makes the check fail until you re-run `npm run vendor:build` and +// commit — so the vendored copy can never silently drift from BMAD core's. + +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const require = createRequire(import.meta.url); +const vendorDir = path.dirname(fileURLToPath(import.meta.url)); +const outfile = path.join(vendorDir, 'yaml.mjs'); +const checkMode = process.argv.includes('--check'); + +const esbuild = await import('esbuild'); +const yamlVersion = require('yaml/package.json').version; +const esbuildVersion = require('esbuild/package.json').version; + +// NOTE: intentionally no builder-specific data (node version, timestamp) in the +// banner — the output must be reproducible so --check can byte-compare. +const banner = `// ============================================================================ +// GENERATED — DO NOT EDIT BY HAND. Run \`npm run vendor:build\` to regenerate. +// Vendored, self-contained bundle of the \`yaml\` npm package (eemeli/yaml). +// +// yaml : ${yamlVersion} +// bundler : esbuild ${esbuildVersion} +// +// 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 +// \`stringify\` are re-exported (tree-shaken). Upstream license retained below. +// ============================================================================ +import { createRequire as __createRequire } from 'node:module'; +const require = __createRequire(import.meta.url); +`; + +const result = await esbuild.build({ + stdin: { + contents: "export { parse, stringify } from 'yaml';", + resolveDir: vendorDir, + sourcefile: 'vendor-entry.mjs', + loader: 'js', + }, + bundle: true, + format: 'esm', + platform: 'node', + target: 'node20', + minify: false, + legalComments: 'inline', + charset: 'utf8', + banner: { js: banner }, + write: false, +}); +const built = result.outputFiles[0].text; + +// Self-check: the freshly built bundle must import and round-trip without any +// node_modules on its resolution path (the runtime condition). Import it from a +// temp file so we never need to write the real output to do this. +const tmp = path.join(os.tmpdir(), `bmad-vendor-yaml-${process.pid}.mjs`); +await fs.writeFile(tmp, built, 'utf8'); +try { + const { parse, stringify } = await import(pathToFileURL(tmp).href); + const sample = { modules: [{ name: 'demo', version: '1.2.3', repoUrl: 'https://example.com/x', when: '2026-05-23T00:00:00.000Z' }] }; + const round = parse(stringify(sample, { indent: 2, lineWidth: 0 })); + if (JSON.stringify(round) !== JSON.stringify(sample)) { + throw new Error('vendor self-check FAILED: round-trip mismatch'); + } +} finally { + await fs.rm(tmp, { force: true }); +} + +if (checkMode) { + const current = await fs.readFile(outfile, 'utf8').catch(() => null); + if (current === built) { + process.stdout.write(`vendor:check OK — yaml.mjs matches yaml@${yamlVersion} (esbuild ${esbuildVersion})\n`); + process.exit(0); + } + process.stderr.write( + `vendor:check FAILED — scripts/lib/vendor/yaml.mjs is stale or hand-edited.\n` + + ` Expected the bundle for yaml@${yamlVersion} (esbuild ${esbuildVersion}).\n` + + ` Fix: run \`npm run vendor:build\` and commit the regenerated yaml.mjs.\n` + + ` (This guards manifest.yaml fidelity: the vendored yaml must match the\n` + + ` one BMAD core writes manifests with — see vendor/README.md.)\n`, + ); + process.exit(1); +} + +await fs.writeFile(outfile, built, 'utf8'); +process.stdout.write(`vendored yaml@${yamlVersion} -> ${path.relative(process.cwd(), outfile)} (self-check OK)\n`); diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/ide-sync.mjs b/src/core-skills/bmad-module/scripts/lib/vendor/ide-sync.mjs new file mode 100644 index 000000000..5df8ea9a5 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/ide-sync.mjs @@ -0,0 +1,10383 @@ +// ============================================================================ +// GENERATED — DO NOT EDIT BY HAND. Run `npm run vendor:build` to regenerate. +// Self-contained bundle of BMAD's IDE-distribution engine +// (tools/installer/ide/* via tools/installer/core/ide-sync.js). +// +// bundler : esbuild 0.25.12 +// yaml : 2.8.4 +// csv-parse : 6.1.0 +// +// Shipped because the bmad-module skill is copied into projects without +// node_modules; see build-ide-sync.mjs and vendor/README.md for the rationale. +// Reads platform-codes.yaml from beside this file (or $BMAD_IDE_PLATFORM_CODES). +// ============================================================================ +// Provide a real `require` so esbuild's CJS interop can load node: builtins +// (node:path/fs/os/crypto) from this ESM bundle. All third-party deps are +// inlined, so only builtins ever reach this require. +import { createRequire as __createRequire } from 'node:module'; +const require = __createRequire(import.meta.url); + +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] +}) : x)(function(x) { + if (typeof require !== "undefined") return require.apply(this, arguments); + throw Error('Dynamic require of "' + x + '" is not supported'); +}); +var __commonJS = (cb, mod) => function __require2() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// tools/installer/fs-native.js +var require_fs_native = __commonJS({ + "tools/installer/fs-native.js"(exports, module) { + var fsp = __require("node:fs/promises"); + var fs = __require("node:fs"); + var path = __require("node:path"); + async function pathExists(p) { + try { + await fsp.access(p); + return true; + } catch { + return false; + } + } + async function ensureDir(dir) { + await fsp.mkdir(dir, { recursive: true }); + } + async function remove(p) { + await fsp.rm(p, { recursive: true, force: true }); + } + async function copy(src, dest, options = {}) { + const filterFn = options.filter; + const overwrite = options.overwrite !== false; + const srcStat = await fsp.stat(src); + if (srcStat.isFile()) { + if (filterFn && !await filterFn(src, dest)) return; + await fsp.mkdir(path.dirname(dest), { recursive: true }); + if (!overwrite) { + try { + await fsp.access(dest); + if (options.errorOnExist) throw new Error(`${dest} already exists`); + return; + } catch (error) { + if (error.message.includes("already exists")) throw error; + } + } + await fsp.copyFile(src, dest); + return; + } + if (srcStat.isDirectory()) { + if (filterFn && !await filterFn(src, dest)) return; + await fsp.mkdir(dest, { recursive: true }); + const entries = await fsp.readdir(src, { withFileTypes: true }); + for (const entry of entries) { + await copy(path.join(src, entry.name), path.join(dest, entry.name), options); + } + } + } + async function move(src, dest) { + try { + await fsp.rename(src, dest); + } catch (error) { + if (error.code === "EXDEV") { + await copy(src, dest); + await fsp.rm(src, { recursive: true, force: true }); + } else { + throw error; + } + } + } + function readJsonSync(p) { + return JSON.parse(fs.readFileSync(p, "utf8")); + } + async function writeJson(p, data, options = {}) { + const spaces = options.spaces ?? 2; + await fsp.writeFile(p, JSON.stringify(data, null, spaces) + "\n", "utf8"); + } + module.exports = { + // Native async (node:fs/promises) + readFile: fsp.readFile, + writeFile: fsp.writeFile, + stat: fsp.stat, + readdir: fsp.readdir, + access: fsp.access, + realpath: fsp.realpath, + rename: fsp.rename, + rmdir: fsp.rmdir, + unlink: fsp.unlink, + chmod: fsp.chmod, + mkdir: fsp.mkdir, + mkdtemp: fsp.mkdtemp, + copyFile: fsp.copyFile, + rm: fsp.rm, + // fs-extra compatible helpers (native implementations) + pathExists, + ensureDir, + remove, + copy, + move, + readJsonSync, + writeJson, + // Sync methods from core node:fs + existsSync: fs.existsSync.bind(fs), + readFileSync: fs.readFileSync.bind(fs), + writeFileSync: fs.writeFileSync.bind(fs), + statSync: fs.statSync.bind(fs), + accessSync: fs.accessSync.bind(fs), + readdirSync: fs.readdirSync.bind(fs), + createReadStream: fs.createReadStream.bind(fs), + pathExistsSync: fs.existsSync.bind(fs), + // Constants + constants: fs.constants + }; + } +}); + +// tools/installer/ide/shared/path-utils.js +var require_path_utils = __commonJS({ + "tools/installer/ide/shared/path-utils.js"(exports, module) { + var AGENT_SEGMENT = "agents"; + var BMAD_FOLDER_NAME = "_bmad"; + function toDashName(module2, type, name) { + const isAgent = type === AGENT_SEGMENT; + if (module2 === "core") { + return isAgent ? `bmad-agent-${name}.md` : `bmad-${name}.md`; + } + if (module2 === "standalone") { + return isAgent ? `bmad-agent-standalone-${name}.md` : `bmad-standalone-${name}.md`; + } + const dashName = name.replace(/\//g, "-"); + return isAgent ? `bmad-agent-${module2}-${dashName}.md` : `bmad-${module2}-${dashName}.md`; + } + function toDashPath(relativePath) { + if (!relativePath || typeof relativePath !== "string") { + return "bmad-unknown.md"; + } + const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ""); + const parts = withoutExt.split(/[/\\]/); + const module2 = parts[0]; + const type = parts[1]; + let name; + if (type === "agents" && parts.length > 3) { + name = parts[2]; + } else { + name = parts.slice(2).join("-"); + } + return toDashName(module2, type, name); + } + function customAgentDashName(agentName) { + return `bmad-custom-agent-${agentName}.md`; + } + function isDashFormat(filename) { + return filename.startsWith("bmad-") && filename.includes("-"); + } + function parseDashName(filename) { + const withoutExt = filename.replace(".md", ""); + const parts = withoutExt.split("-"); + if (parts.length < 2 || parts[0] !== "bmad") { + return null; + } + const isAgent = parts[1] === "agent"; + if (isAgent) { + if (parts.length >= 4 && parts[2] === "standalone") { + return { + prefix: parts[0], + module: "standalone", + type: "agents", + name: parts.slice(3).join("-") + }; + } + if (parts.length === 3) { + return { + prefix: parts[0], + module: "core", + type: "agents", + name: parts[2] + }; + } else { + return { + prefix: parts[0], + module: parts[2], + type: "agents", + name: parts.slice(3).join("-") + }; + } + } + if (parts.length === 2) { + return { + prefix: parts[0], + module: "core", + type: "workflows", + // Default to workflows for non-agent core items + name: parts[1] + }; + } + if (parts[1] === "standalone") { + return { + prefix: parts[0], + module: "standalone", + type: "workflows", + // Default to workflows for non-agent standalone items + name: parts.slice(2).join("-") + }; + } + return { + prefix: parts[0], + module: parts[1], + type: "workflows", + // Default to workflows for non-agent module items + name: parts.slice(2).join("-") + }; + } + function resolveSkillName(artifact) { + if (artifact.canonicalId) { + return `${artifact.canonicalId}.md`; + } + return toDashPath(artifact.relativePath); + } + module.exports = { + toDashName, + toDashPath, + resolveSkillName, + customAgentDashName, + isDashFormat, + parseDashName, + AGENT_SEGMENT, + BMAD_FOLDER_NAME + }; + } +}); + +// src/core-skills/bmad-module/scripts/lib/vendor/shims/prompts.cjs +var require_prompts = __commonJS({ + "src/core-skills/bmad-module/scripts/lib/vendor/shims/prompts.cjs"(exports, module) { + "use strict"; + var out = (m) => process.stdout.write(`${m} +`); + var err = (m) => process.stderr.write(`${m} +`); + var log = { + info: async (m) => out(m), + success: async (m) => out(m), + message: async (m) => out(m), + step: async (m) => out(m), + warn: async (m) => err(m), + error: async (m) => err(m) + }; + var notInteractive = () => { + throw new Error("interactive prompt is not available in the ide-sync bundle"); + }; + var identityColor = new Proxy( + {}, + { + get: () => (s) => s + } + ); + module.exports = { + log, + getColor: async () => identityColor, + spinner: () => ({ start() { + }, stop() { + }, message() { + } }), + tasks: async () => { + }, + note: async (m) => out(m), + box: async (m) => out(m), + intro: async () => { + }, + outro: async () => { + }, + cancel: async () => { + }, + handleCancel: async () => { + }, + getClack: notInteractive, + select: notInteractive, + multiselect: notInteractive, + autocomplete: notInteractive, + autocompleteMultiselect: notInteractive, + directory: notInteractive, + confirm: notInteractive, + text: notInteractive, + password: notInteractive, + prompt: notInteractive + }; + } +}); + +// node_modules/yaml/dist/nodes/identity.js +var require_identity = __commonJS({ + "node_modules/yaml/dist/nodes/identity.js"(exports) { + "use strict"; + var ALIAS = Symbol.for("yaml.alias"); + var DOC = Symbol.for("yaml.document"); + var MAP = Symbol.for("yaml.map"); + var PAIR = Symbol.for("yaml.pair"); + var SCALAR = Symbol.for("yaml.scalar"); + var SEQ = Symbol.for("yaml.seq"); + var NODE_TYPE = Symbol.for("yaml.node.type"); + var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS; + var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC; + var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP; + var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR; + var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR; + var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ; + function isCollection(node) { + if (node && typeof node === "object") + switch (node[NODE_TYPE]) { + case MAP: + case SEQ: + return true; + } + return false; + } + function isNode(node) { + if (node && typeof node === "object") + switch (node[NODE_TYPE]) { + case ALIAS: + case MAP: + case SCALAR: + case SEQ: + return true; + } + return false; + } + var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor; + exports.ALIAS = ALIAS; + exports.DOC = DOC; + exports.MAP = MAP; + exports.NODE_TYPE = NODE_TYPE; + exports.PAIR = PAIR; + exports.SCALAR = SCALAR; + exports.SEQ = SEQ; + exports.hasAnchor = hasAnchor; + exports.isAlias = isAlias; + exports.isCollection = isCollection; + exports.isDocument = isDocument; + exports.isMap = isMap; + exports.isNode = isNode; + exports.isPair = isPair; + exports.isScalar = isScalar; + exports.isSeq = isSeq; + } +}); + +// node_modules/yaml/dist/visit.js +var require_visit = __commonJS({ + "node_modules/yaml/dist/visit.js"(exports) { + "use strict"; + var identity = require_identity(); + var BREAK = Symbol("break visit"); + var SKIP = Symbol("skip children"); + var REMOVE = Symbol("remove node"); + function visit(node, visitor) { + const visitor_ = initVisitor(visitor); + if (identity.isDocument(node)) { + const cd = visit_(null, node.contents, visitor_, Object.freeze([node])); + if (cd === REMOVE) + node.contents = null; + } else + visit_(null, node, visitor_, Object.freeze([])); + } + visit.BREAK = BREAK; + visit.SKIP = SKIP; + visit.REMOVE = REMOVE; + function visit_(key, node, visitor, path) { + const ctrl = callVisitor(key, node, visitor, path); + if (identity.isNode(ctrl) || identity.isPair(ctrl)) { + replaceNode(key, path, ctrl); + return visit_(key, ctrl, visitor, path); + } + if (typeof ctrl !== "symbol") { + if (identity.isCollection(node)) { + path = Object.freeze(path.concat(node)); + for (let i = 0; i < node.items.length; ++i) { + const ci = visit_(i, node.items[i], visitor, path); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + node.items.splice(i, 1); + i -= 1; + } + } + } else if (identity.isPair(node)) { + path = Object.freeze(path.concat(node)); + const ck = visit_("key", node.key, visitor, path); + if (ck === BREAK) + return BREAK; + else if (ck === REMOVE) + node.key = null; + const cv = visit_("value", node.value, visitor, path); + if (cv === BREAK) + return BREAK; + else if (cv === REMOVE) + node.value = null; + } + } + return ctrl; + } + async function visitAsync(node, visitor) { + const visitor_ = initVisitor(visitor); + if (identity.isDocument(node)) { + const cd = await visitAsync_(null, node.contents, visitor_, Object.freeze([node])); + if (cd === REMOVE) + node.contents = null; + } else + await visitAsync_(null, node, visitor_, Object.freeze([])); + } + visitAsync.BREAK = BREAK; + visitAsync.SKIP = SKIP; + visitAsync.REMOVE = REMOVE; + async function visitAsync_(key, node, visitor, path) { + const ctrl = await callVisitor(key, node, visitor, path); + if (identity.isNode(ctrl) || identity.isPair(ctrl)) { + replaceNode(key, path, ctrl); + return visitAsync_(key, ctrl, visitor, path); + } + if (typeof ctrl !== "symbol") { + if (identity.isCollection(node)) { + path = Object.freeze(path.concat(node)); + for (let i = 0; i < node.items.length; ++i) { + const ci = await visitAsync_(i, node.items[i], visitor, path); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + node.items.splice(i, 1); + i -= 1; + } + } + } else if (identity.isPair(node)) { + path = Object.freeze(path.concat(node)); + const ck = await visitAsync_("key", node.key, visitor, path); + if (ck === BREAK) + return BREAK; + else if (ck === REMOVE) + node.key = null; + const cv = await visitAsync_("value", node.value, visitor, path); + if (cv === BREAK) + return BREAK; + else if (cv === REMOVE) + node.value = null; + } + } + return ctrl; + } + function initVisitor(visitor) { + if (typeof visitor === "object" && (visitor.Collection || visitor.Node || visitor.Value)) { + return Object.assign({ + Alias: visitor.Node, + Map: visitor.Node, + Scalar: visitor.Node, + Seq: visitor.Node + }, visitor.Value && { + Map: visitor.Value, + Scalar: visitor.Value, + Seq: visitor.Value + }, visitor.Collection && { + Map: visitor.Collection, + Seq: visitor.Collection + }, visitor); + } + return visitor; + } + function callVisitor(key, node, visitor, path) { + if (typeof visitor === "function") + return visitor(key, node, path); + if (identity.isMap(node)) + return visitor.Map?.(key, node, path); + if (identity.isSeq(node)) + return visitor.Seq?.(key, node, path); + if (identity.isPair(node)) + return visitor.Pair?.(key, node, path); + if (identity.isScalar(node)) + return visitor.Scalar?.(key, node, path); + if (identity.isAlias(node)) + return visitor.Alias?.(key, node, path); + return void 0; + } + function replaceNode(key, path, node) { + const parent = path[path.length - 1]; + if (identity.isCollection(parent)) { + parent.items[key] = node; + } else if (identity.isPair(parent)) { + if (key === "key") + parent.key = node; + else + parent.value = node; + } else if (identity.isDocument(parent)) { + parent.contents = node; + } else { + const pt = identity.isAlias(parent) ? "alias" : "scalar"; + throw new Error(`Cannot replace node with ${pt} parent`); + } + } + exports.visit = visit; + exports.visitAsync = visitAsync; + } +}); + +// node_modules/yaml/dist/doc/directives.js +var require_directives = __commonJS({ + "node_modules/yaml/dist/doc/directives.js"(exports) { + "use strict"; + var identity = require_identity(); + var visit = require_visit(); + var escapeChars = { + "!": "%21", + ",": "%2C", + "[": "%5B", + "]": "%5D", + "{": "%7B", + "}": "%7D" + }; + var escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, (ch) => escapeChars[ch]); + var Directives = class _Directives { + constructor(yaml, tags) { + this.docStart = null; + this.docEnd = false; + this.yaml = Object.assign({}, _Directives.defaultYaml, yaml); + this.tags = Object.assign({}, _Directives.defaultTags, tags); + } + clone() { + const copy = new _Directives(this.yaml, this.tags); + copy.docStart = this.docStart; + return copy; + } + /** + * During parsing, get a Directives instance for the current document and + * update the stream state according to the current version's spec. + */ + atDocument() { + const res = new _Directives(this.yaml, this.tags); + switch (this.yaml.version) { + case "1.1": + this.atNextDocument = true; + break; + case "1.2": + this.atNextDocument = false; + this.yaml = { + explicit: _Directives.defaultYaml.explicit, + version: "1.2" + }; + this.tags = Object.assign({}, _Directives.defaultTags); + break; + } + return res; + } + /** + * @param onError - May be called even if the action was successful + * @returns `true` on success + */ + add(line, onError) { + if (this.atNextDocument) { + this.yaml = { explicit: _Directives.defaultYaml.explicit, version: "1.1" }; + this.tags = Object.assign({}, _Directives.defaultTags); + this.atNextDocument = false; + } + const parts = line.trim().split(/[ \t]+/); + const name = parts.shift(); + switch (name) { + case "%TAG": { + if (parts.length !== 2) { + onError(0, "%TAG directive should contain exactly two parts"); + if (parts.length < 2) + return false; + } + const [handle, prefix] = parts; + this.tags[handle] = prefix; + return true; + } + case "%YAML": { + this.yaml.explicit = true; + if (parts.length !== 1) { + onError(0, "%YAML directive should contain exactly one part"); + return false; + } + const [version] = parts; + if (version === "1.1" || version === "1.2") { + this.yaml.version = version; + return true; + } else { + const isValid = /^\d+\.\d+$/.test(version); + onError(6, `Unsupported YAML version ${version}`, isValid); + return false; + } + } + default: + onError(0, `Unknown directive ${name}`, true); + return false; + } + } + /** + * Resolves a tag, matching handles to those defined in %TAG directives. + * + * @returns Resolved tag, which may also be the non-specific tag `'!'` or a + * `'!local'` tag, or `null` if unresolvable. + */ + tagName(source, onError) { + if (source === "!") + return "!"; + if (source[0] !== "!") { + onError(`Not a valid tag: ${source}`); + return null; + } + if (source[1] === "<") { + const verbatim = source.slice(2, -1); + if (verbatim === "!" || verbatim === "!!") { + onError(`Verbatim tags aren't resolved, so ${source} is invalid.`); + return null; + } + if (source[source.length - 1] !== ">") + onError("Verbatim tags must end with a >"); + return verbatim; + } + const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s); + if (!suffix) + onError(`The ${source} tag has no suffix`); + const prefix = this.tags[handle]; + if (prefix) { + try { + return prefix + decodeURIComponent(suffix); + } catch (error) { + onError(String(error)); + return null; + } + } + if (handle === "!") + return source; + onError(`Could not resolve tag: ${source}`); + return null; + } + /** + * Given a fully resolved tag, returns its printable string form, + * taking into account current tag prefixes and defaults. + */ + tagString(tag) { + for (const [handle, prefix] of Object.entries(this.tags)) { + if (tag.startsWith(prefix)) + return handle + escapeTagName(tag.substring(prefix.length)); + } + return tag[0] === "!" ? tag : `!<${tag}>`; + } + toString(doc) { + const lines = this.yaml.explicit ? [`%YAML ${this.yaml.version || "1.2"}`] : []; + const tagEntries = Object.entries(this.tags); + let tagNames; + if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) { + const tags = {}; + visit.visit(doc.contents, (_key, node) => { + if (identity.isNode(node) && node.tag) + tags[node.tag] = true; + }); + tagNames = Object.keys(tags); + } else + tagNames = []; + for (const [handle, prefix] of tagEntries) { + if (handle === "!!" && prefix === "tag:yaml.org,2002:") + continue; + if (!doc || tagNames.some((tn) => tn.startsWith(prefix))) + lines.push(`%TAG ${handle} ${prefix}`); + } + return lines.join("\n"); + } + }; + Directives.defaultYaml = { explicit: false, version: "1.2" }; + Directives.defaultTags = { "!!": "tag:yaml.org,2002:" }; + exports.Directives = Directives; + } +}); + +// node_modules/yaml/dist/doc/anchors.js +var require_anchors = __commonJS({ + "node_modules/yaml/dist/doc/anchors.js"(exports) { + "use strict"; + var identity = require_identity(); + var visit = require_visit(); + function anchorIsValid(anchor) { + if (/[\x00-\x19\s,[\]{}]/.test(anchor)) { + const sa = JSON.stringify(anchor); + const msg = `Anchor must not contain whitespace or control characters: ${sa}`; + throw new Error(msg); + } + return true; + } + function anchorNames(root) { + const anchors = /* @__PURE__ */ new Set(); + visit.visit(root, { + Value(_key, node) { + if (node.anchor) + anchors.add(node.anchor); + } + }); + return anchors; + } + function findNewAnchor(prefix, exclude) { + for (let i = 1; true; ++i) { + const name = `${prefix}${i}`; + if (!exclude.has(name)) + return name; + } + } + function createNodeAnchors(doc, prefix) { + const aliasObjects = []; + const sourceObjects = /* @__PURE__ */ new Map(); + let prevAnchors = null; + return { + onAnchor: (source) => { + aliasObjects.push(source); + prevAnchors ?? (prevAnchors = anchorNames(doc)); + const anchor = findNewAnchor(prefix, prevAnchors); + prevAnchors.add(anchor); + return anchor; + }, + /** + * With circular references, the source node is only resolved after all + * of its child nodes are. This is why anchors are set only after all of + * the nodes have been created. + */ + setAnchors: () => { + for (const source of aliasObjects) { + const ref = sourceObjects.get(source); + if (typeof ref === "object" && ref.anchor && (identity.isScalar(ref.node) || identity.isCollection(ref.node))) { + ref.node.anchor = ref.anchor; + } else { + const error = new Error("Failed to resolve repeated object (this should not happen)"); + error.source = source; + throw error; + } + } + }, + sourceObjects + }; + } + exports.anchorIsValid = anchorIsValid; + exports.anchorNames = anchorNames; + exports.createNodeAnchors = createNodeAnchors; + exports.findNewAnchor = findNewAnchor; + } +}); + +// node_modules/yaml/dist/doc/applyReviver.js +var require_applyReviver = __commonJS({ + "node_modules/yaml/dist/doc/applyReviver.js"(exports) { + "use strict"; + function applyReviver(reviver, obj, key, val) { + if (val && typeof val === "object") { + if (Array.isArray(val)) { + for (let i = 0, len = val.length; i < len; ++i) { + const v0 = val[i]; + const v1 = applyReviver(reviver, val, String(i), v0); + if (v1 === void 0) + delete val[i]; + else if (v1 !== v0) + val[i] = v1; + } + } else if (val instanceof Map) { + for (const k of Array.from(val.keys())) { + const v0 = val.get(k); + const v1 = applyReviver(reviver, val, k, v0); + if (v1 === void 0) + val.delete(k); + else if (v1 !== v0) + val.set(k, v1); + } + } else if (val instanceof Set) { + for (const v0 of Array.from(val)) { + const v1 = applyReviver(reviver, val, v0, v0); + if (v1 === void 0) + val.delete(v0); + else if (v1 !== v0) { + val.delete(v0); + val.add(v1); + } + } + } else { + for (const [k, v0] of Object.entries(val)) { + const v1 = applyReviver(reviver, val, k, v0); + if (v1 === void 0) + delete val[k]; + else if (v1 !== v0) + val[k] = v1; + } + } + } + return reviver.call(obj, key, val); + } + exports.applyReviver = applyReviver; + } +}); + +// node_modules/yaml/dist/nodes/toJS.js +var require_toJS = __commonJS({ + "node_modules/yaml/dist/nodes/toJS.js"(exports) { + "use strict"; + var identity = require_identity(); + function toJS(value, arg, ctx) { + if (Array.isArray(value)) + return value.map((v, i) => toJS(v, String(i), ctx)); + if (value && typeof value.toJSON === "function") { + if (!ctx || !identity.hasAnchor(value)) + return value.toJSON(arg, ctx); + const data = { aliasCount: 0, count: 1, res: void 0 }; + ctx.anchors.set(value, data); + ctx.onCreate = (res2) => { + data.res = res2; + delete ctx.onCreate; + }; + const res = value.toJSON(arg, ctx); + if (ctx.onCreate) + ctx.onCreate(res); + return res; + } + if (typeof value === "bigint" && !ctx?.keep) + return Number(value); + return value; + } + exports.toJS = toJS; + } +}); + +// node_modules/yaml/dist/nodes/Node.js +var require_Node = __commonJS({ + "node_modules/yaml/dist/nodes/Node.js"(exports) { + "use strict"; + var applyReviver = require_applyReviver(); + var identity = require_identity(); + var toJS = require_toJS(); + var NodeBase = class { + constructor(type) { + Object.defineProperty(this, identity.NODE_TYPE, { value: type }); + } + /** Create a copy of this node. */ + clone() { + const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this)); + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** A plain JavaScript representation of this node. */ + toJS(doc, { mapAsMap, maxAliasCount, onAnchor, reviver } = {}) { + if (!identity.isDocument(doc)) + throw new TypeError("A document argument is required"); + const ctx = { + anchors: /* @__PURE__ */ new Map(), + doc, + keep: true, + mapAsMap: mapAsMap === true, + mapKeyWarned: false, + maxAliasCount: typeof maxAliasCount === "number" ? maxAliasCount : 100 + }; + const res = toJS.toJS(this, "", ctx); + if (typeof onAnchor === "function") + for (const { count, res: res2 } of ctx.anchors.values()) + onAnchor(res2, count); + return typeof reviver === "function" ? applyReviver.applyReviver(reviver, { "": res }, "", res) : res; + } + }; + exports.NodeBase = NodeBase; + } +}); + +// node_modules/yaml/dist/nodes/Alias.js +var require_Alias = __commonJS({ + "node_modules/yaml/dist/nodes/Alias.js"(exports) { + "use strict"; + var anchors = require_anchors(); + var visit = require_visit(); + var identity = require_identity(); + var Node = require_Node(); + var toJS = require_toJS(); + var Alias = class extends Node.NodeBase { + constructor(source) { + super(identity.ALIAS); + this.source = source; + Object.defineProperty(this, "tag", { + set() { + throw new Error("Alias nodes cannot have tags"); + } + }); + } + /** + * Resolve the value of this alias within `doc`, finding the last + * instance of the `source` anchor before this node. + */ + resolve(doc, ctx) { + if (ctx?.maxAliasCount === 0) + throw new ReferenceError("Alias resolution is disabled"); + let nodes; + if (ctx?.aliasResolveCache) { + nodes = ctx.aliasResolveCache; + } else { + nodes = []; + visit.visit(doc, { + Node: (_key, node) => { + if (identity.isAlias(node) || identity.hasAnchor(node)) + nodes.push(node); + } + }); + if (ctx) + ctx.aliasResolveCache = nodes; + } + let found = void 0; + for (const node of nodes) { + if (node === this) + break; + if (node.anchor === this.source) + found = node; + } + return found; + } + toJSON(_arg, ctx) { + if (!ctx) + return { source: this.source }; + const { anchors: anchors2, doc, maxAliasCount } = ctx; + const source = this.resolve(doc, ctx); + if (!source) { + const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`; + throw new ReferenceError(msg); + } + let data = anchors2.get(source); + if (!data) { + toJS.toJS(source, null, ctx); + data = anchors2.get(source); + } + if (data?.res === void 0) { + const msg = "This should not happen: Alias anchor was not resolved?"; + throw new ReferenceError(msg); + } + if (maxAliasCount >= 0) { + data.count += 1; + if (data.aliasCount === 0) + data.aliasCount = getAliasCount(doc, source, anchors2); + if (data.count * data.aliasCount > maxAliasCount) { + const msg = "Excessive alias count indicates a resource exhaustion attack"; + throw new ReferenceError(msg); + } + } + return data.res; + } + toString(ctx, _onComment, _onChompKeep) { + const src = `*${this.source}`; + if (ctx) { + anchors.anchorIsValid(this.source); + if (ctx.options.verifyAliasOrder && !ctx.anchors.has(this.source)) { + const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`; + throw new Error(msg); + } + if (ctx.implicitKey) + return `${src} `; + } + return src; + } + }; + function getAliasCount(doc, node, anchors2) { + if (identity.isAlias(node)) { + const source = node.resolve(doc); + const anchor = anchors2 && source && anchors2.get(source); + return anchor ? anchor.count * anchor.aliasCount : 0; + } else if (identity.isCollection(node)) { + let count = 0; + for (const item of node.items) { + const c = getAliasCount(doc, item, anchors2); + if (c > count) + count = c; + } + return count; + } else if (identity.isPair(node)) { + const kc = getAliasCount(doc, node.key, anchors2); + const vc = getAliasCount(doc, node.value, anchors2); + return Math.max(kc, vc); + } + return 1; + } + exports.Alias = Alias; + } +}); + +// node_modules/yaml/dist/nodes/Scalar.js +var require_Scalar = __commonJS({ + "node_modules/yaml/dist/nodes/Scalar.js"(exports) { + "use strict"; + var identity = require_identity(); + var Node = require_Node(); + var toJS = require_toJS(); + var isScalarValue = (value) => !value || typeof value !== "function" && typeof value !== "object"; + var Scalar = class extends Node.NodeBase { + constructor(value) { + super(identity.SCALAR); + this.value = value; + } + toJSON(arg, ctx) { + return ctx?.keep ? this.value : toJS.toJS(this.value, arg, ctx); + } + toString() { + return String(this.value); + } + }; + Scalar.BLOCK_FOLDED = "BLOCK_FOLDED"; + Scalar.BLOCK_LITERAL = "BLOCK_LITERAL"; + Scalar.PLAIN = "PLAIN"; + Scalar.QUOTE_DOUBLE = "QUOTE_DOUBLE"; + Scalar.QUOTE_SINGLE = "QUOTE_SINGLE"; + exports.Scalar = Scalar; + exports.isScalarValue = isScalarValue; + } +}); + +// node_modules/yaml/dist/doc/createNode.js +var require_createNode = __commonJS({ + "node_modules/yaml/dist/doc/createNode.js"(exports) { + "use strict"; + var Alias = require_Alias(); + var identity = require_identity(); + var Scalar = require_Scalar(); + var defaultTagPrefix = "tag:yaml.org,2002:"; + function findTagObject(value, tagName, tags) { + if (tagName) { + const match = tags.filter((t) => t.tag === tagName); + const tagObj = match.find((t) => !t.format) ?? match[0]; + if (!tagObj) + throw new Error(`Tag ${tagName} not found`); + return tagObj; + } + return tags.find((t) => t.identify?.(value) && !t.format); + } + function createNode(value, tagName, ctx) { + if (identity.isDocument(value)) + value = value.contents; + if (identity.isNode(value)) + return value; + if (identity.isPair(value)) { + const map = ctx.schema[identity.MAP].createNode?.(ctx.schema, null, ctx); + map.items.push(value); + return map; + } + if (value instanceof String || value instanceof Number || value instanceof Boolean || typeof BigInt !== "undefined" && value instanceof BigInt) { + value = value.valueOf(); + } + const { aliasDuplicateObjects, onAnchor, onTagObj, schema, sourceObjects } = ctx; + let ref = void 0; + if (aliasDuplicateObjects && value && typeof value === "object") { + ref = sourceObjects.get(value); + if (ref) { + ref.anchor ?? (ref.anchor = onAnchor(value)); + return new Alias.Alias(ref.anchor); + } else { + ref = { anchor: null, node: null }; + sourceObjects.set(value, ref); + } + } + if (tagName?.startsWith("!!")) + tagName = defaultTagPrefix + tagName.slice(2); + let tagObj = findTagObject(value, tagName, schema.tags); + if (!tagObj) { + if (value && typeof value.toJSON === "function") { + value = value.toJSON(); + } + if (!value || typeof value !== "object") { + const node2 = new Scalar.Scalar(value); + if (ref) + ref.node = node2; + return node2; + } + tagObj = value instanceof Map ? schema[identity.MAP] : Symbol.iterator in Object(value) ? schema[identity.SEQ] : schema[identity.MAP]; + } + if (onTagObj) { + onTagObj(tagObj); + delete ctx.onTagObj; + } + const node = tagObj?.createNode ? tagObj.createNode(ctx.schema, value, ctx) : typeof tagObj?.nodeClass?.from === "function" ? tagObj.nodeClass.from(ctx.schema, value, ctx) : new Scalar.Scalar(value); + if (tagName) + node.tag = tagName; + else if (!tagObj.default) + node.tag = tagObj.tag; + if (ref) + ref.node = node; + return node; + } + exports.createNode = createNode; + } +}); + +// node_modules/yaml/dist/nodes/Collection.js +var require_Collection = __commonJS({ + "node_modules/yaml/dist/nodes/Collection.js"(exports) { + "use strict"; + var createNode = require_createNode(); + var identity = require_identity(); + var Node = require_Node(); + function collectionFromPath(schema, path, value) { + let v = value; + for (let i = path.length - 1; i >= 0; --i) { + const k = path[i]; + if (typeof k === "number" && Number.isInteger(k) && k >= 0) { + const a = []; + a[k] = v; + v = a; + } else { + v = /* @__PURE__ */ new Map([[k, v]]); + } + } + return createNode.createNode(v, void 0, { + aliasDuplicateObjects: false, + keepUndefined: false, + onAnchor: () => { + throw new Error("This should not happen, please report a bug."); + }, + schema, + sourceObjects: /* @__PURE__ */ new Map() + }); + } + var isEmptyPath = (path) => path == null || typeof path === "object" && !!path[Symbol.iterator]().next().done; + var Collection = class extends Node.NodeBase { + constructor(type, schema) { + super(type); + Object.defineProperty(this, "schema", { + value: schema, + configurable: true, + enumerable: false, + writable: true + }); + } + /** + * Create a copy of this collection. + * + * @param schema - If defined, overwrites the original's schema + */ + clone(schema) { + const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this)); + if (schema) + copy.schema = schema; + copy.items = copy.items.map((it) => identity.isNode(it) || identity.isPair(it) ? it.clone(schema) : it); + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** + * Adds a value to the collection. For `!!map` and `!!omap` the value must + * be a Pair instance or a `{ key, value }` object, which may not have a key + * that already exists in the map. + */ + addIn(path, value) { + if (isEmptyPath(path)) + this.add(value); + else { + const [key, ...rest] = path; + const node = this.get(key, true); + if (identity.isCollection(node)) + node.addIn(rest, value); + else if (node === void 0 && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + } + /** + * Removes a value from the collection. + * @returns `true` if the item was found and removed. + */ + deleteIn(path) { + const [key, ...rest] = path; + if (rest.length === 0) + return this.delete(key); + const node = this.get(key, true); + if (identity.isCollection(node)) + return node.deleteIn(rest); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn(path, keepScalar) { + const [key, ...rest] = path; + const node = this.get(key, true); + if (rest.length === 0) + return !keepScalar && identity.isScalar(node) ? node.value : node; + else + return identity.isCollection(node) ? node.getIn(rest, keepScalar) : void 0; + } + hasAllNullValues(allowScalar) { + return this.items.every((node) => { + if (!identity.isPair(node)) + return false; + const n = node.value; + return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag; + }); + } + /** + * Checks if the collection includes a value with the key `key`. + */ + hasIn(path) { + const [key, ...rest] = path; + if (rest.length === 0) + return this.has(key); + const node = this.get(key, true); + return identity.isCollection(node) ? node.hasIn(rest) : false; + } + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn(path, value) { + const [key, ...rest] = path; + if (rest.length === 0) { + this.set(key, value); + } else { + const node = this.get(key, true); + if (identity.isCollection(node)) + node.setIn(rest, value); + else if (node === void 0 && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + } + }; + exports.Collection = Collection; + exports.collectionFromPath = collectionFromPath; + exports.isEmptyPath = isEmptyPath; + } +}); + +// node_modules/yaml/dist/stringify/stringifyComment.js +var require_stringifyComment = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyComment.js"(exports) { + "use strict"; + var stringifyComment = (str) => str.replace(/^(?!$)(?: $)?/gm, "#"); + function indentComment(comment, indent) { + if (/^\n+$/.test(comment)) + return comment.substring(1); + return indent ? comment.replace(/^(?! *$)/gm, indent) : comment; + } + var lineComment = (str, indent, comment) => str.endsWith("\n") ? indentComment(comment, indent) : comment.includes("\n") ? "\n" + indentComment(comment, indent) : (str.endsWith(" ") ? "" : " ") + comment; + exports.indentComment = indentComment; + exports.lineComment = lineComment; + exports.stringifyComment = stringifyComment; + } +}); + +// node_modules/yaml/dist/stringify/foldFlowLines.js +var require_foldFlowLines = __commonJS({ + "node_modules/yaml/dist/stringify/foldFlowLines.js"(exports) { + "use strict"; + var FOLD_FLOW = "flow"; + var FOLD_BLOCK = "block"; + var FOLD_QUOTED = "quoted"; + function foldFlowLines(text, indent, mode = "flow", { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) { + if (!lineWidth || lineWidth < 0) + return text; + if (lineWidth < minContentWidth) + minContentWidth = 0; + const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length); + if (text.length <= endStep) + return text; + const folds = []; + const escapedFolds = {}; + let end = lineWidth - indent.length; + if (typeof indentAtStart === "number") { + if (indentAtStart > lineWidth - Math.max(2, minContentWidth)) + folds.push(0); + else + end = lineWidth - indentAtStart; + } + let split = void 0; + let prev = void 0; + let overflow = false; + let i = -1; + let escStart = -1; + let escEnd = -1; + if (mode === FOLD_BLOCK) { + i = consumeMoreIndentedLines(text, i, indent.length); + if (i !== -1) + end = i + endStep; + } + for (let ch; ch = text[i += 1]; ) { + if (mode === FOLD_QUOTED && ch === "\\") { + escStart = i; + switch (text[i + 1]) { + case "x": + i += 3; + break; + case "u": + i += 5; + break; + case "U": + i += 9; + break; + default: + i += 1; + } + escEnd = i; + } + if (ch === "\n") { + if (mode === FOLD_BLOCK) + i = consumeMoreIndentedLines(text, i, indent.length); + end = i + indent.length + endStep; + split = void 0; + } else { + if (ch === " " && prev && prev !== " " && prev !== "\n" && prev !== " ") { + const next = text[i + 1]; + if (next && next !== " " && next !== "\n" && next !== " ") + split = i; + } + if (i >= end) { + if (split) { + folds.push(split); + end = split + endStep; + split = void 0; + } else if (mode === FOLD_QUOTED) { + while (prev === " " || prev === " ") { + prev = ch; + ch = text[i += 1]; + overflow = true; + } + const j = i > escEnd + 1 ? i - 2 : escStart - 1; + if (escapedFolds[j]) + return text; + folds.push(j); + escapedFolds[j] = true; + end = j + endStep; + split = void 0; + } else { + overflow = true; + } + } + } + prev = ch; + } + if (overflow && onOverflow) + onOverflow(); + if (folds.length === 0) + return text; + if (onFold) + onFold(); + let res = text.slice(0, folds[0]); + for (let i2 = 0; i2 < folds.length; ++i2) { + const fold = folds[i2]; + const end2 = folds[i2 + 1] || text.length; + if (fold === 0) + res = ` +${indent}${text.slice(0, end2)}`; + else { + if (mode === FOLD_QUOTED && escapedFolds[fold]) + res += `${text[fold]}\\`; + res += ` +${indent}${text.slice(fold + 1, end2)}`; + } + } + return res; + } + function consumeMoreIndentedLines(text, i, indent) { + let end = i; + let start = i + 1; + let ch = text[start]; + while (ch === " " || ch === " ") { + if (i < start + indent) { + ch = text[++i]; + } else { + do { + ch = text[++i]; + } while (ch && ch !== "\n"); + end = i; + start = i + 1; + ch = text[start]; + } + } + return end; + } + exports.FOLD_BLOCK = FOLD_BLOCK; + exports.FOLD_FLOW = FOLD_FLOW; + exports.FOLD_QUOTED = FOLD_QUOTED; + exports.foldFlowLines = foldFlowLines; + } +}); + +// node_modules/yaml/dist/stringify/stringifyString.js +var require_stringifyString = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyString.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var foldFlowLines = require_foldFlowLines(); + var getFoldOptions = (ctx, isBlock) => ({ + indentAtStart: isBlock ? ctx.indent.length : ctx.indentAtStart, + lineWidth: ctx.options.lineWidth, + minContentWidth: ctx.options.minContentWidth + }); + var containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str); + function lineLengthOverLimit(str, lineWidth, indentLength) { + if (!lineWidth || lineWidth < 0) + return false; + const limit = lineWidth - indentLength; + const strLen = str.length; + if (strLen <= limit) + return false; + for (let i = 0, start = 0; i < strLen; ++i) { + if (str[i] === "\n") { + if (i - start > limit) + return true; + start = i + 1; + if (strLen - start <= limit) + return false; + } + } + return true; + } + function doubleQuotedString(value, ctx) { + const json = JSON.stringify(value); + if (ctx.options.doubleQuotedAsJSON) + return json; + const { implicitKey } = ctx; + const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength; + const indent = ctx.indent || (containsDocumentMarker(value) ? " " : ""); + let str = ""; + let start = 0; + for (let i = 0, ch = json[i]; ch; ch = json[++i]) { + if (ch === " " && json[i + 1] === "\\" && json[i + 2] === "n") { + str += json.slice(start, i) + "\\ "; + i += 1; + start = i; + ch = "\\"; + } + if (ch === "\\") + switch (json[i + 1]) { + case "u": + { + str += json.slice(start, i); + const code = json.substr(i + 2, 4); + switch (code) { + case "0000": + str += "\\0"; + break; + case "0007": + str += "\\a"; + break; + case "000b": + str += "\\v"; + break; + case "001b": + str += "\\e"; + break; + case "0085": + str += "\\N"; + break; + case "00a0": + str += "\\_"; + break; + case "2028": + str += "\\L"; + break; + case "2029": + str += "\\P"; + break; + default: + if (code.substr(0, 2) === "00") + str += "\\x" + code.substr(2); + else + str += json.substr(i, 6); + } + i += 5; + start = i + 1; + } + break; + case "n": + if (implicitKey || json[i + 2] === '"' || json.length < minMultiLineLength) { + i += 1; + } else { + str += json.slice(start, i) + "\n\n"; + while (json[i + 2] === "\\" && json[i + 3] === "n" && json[i + 4] !== '"') { + str += "\n"; + i += 2; + } + str += indent; + if (json[i + 2] === " ") + str += "\\"; + i += 1; + start = i + 1; + } + break; + default: + i += 1; + } + } + str = start ? str + json.slice(start) : json; + return implicitKey ? str : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false)); + } + function singleQuotedString(value, ctx) { + if (ctx.options.singleQuote === false || ctx.implicitKey && value.includes("\n") || /[ \t]\n|\n[ \t]/.test(value)) + return doubleQuotedString(value, ctx); + const indent = ctx.indent || (containsDocumentMarker(value) ? " " : ""); + const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$& +${indent}`) + "'"; + return ctx.implicitKey ? res : foldFlowLines.foldFlowLines(res, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false)); + } + function quotedString(value, ctx) { + const { singleQuote } = ctx.options; + let qs; + if (singleQuote === false) + qs = doubleQuotedString; + else { + const hasDouble = value.includes('"'); + const hasSingle = value.includes("'"); + if (hasDouble && !hasSingle) + qs = singleQuotedString; + else if (hasSingle && !hasDouble) + qs = doubleQuotedString; + else + qs = singleQuote ? singleQuotedString : doubleQuotedString; + } + return qs(value, ctx); + } + var blockEndNewlines; + try { + blockEndNewlines = new RegExp("(^|(?\n"; + let chomp; + let endStart; + for (endStart = value.length; endStart > 0; --endStart) { + const ch = value[endStart - 1]; + if (ch !== "\n" && ch !== " " && ch !== " ") + break; + } + let end = value.substring(endStart); + const endNlPos = end.indexOf("\n"); + if (endNlPos === -1) { + chomp = "-"; + } else if (value === end || endNlPos !== end.length - 1) { + chomp = "+"; + if (onChompKeep) + onChompKeep(); + } else { + chomp = ""; + } + if (end) { + value = value.slice(0, -end.length); + if (end[end.length - 1] === "\n") + end = end.slice(0, -1); + end = end.replace(blockEndNewlines, `$&${indent}`); + } + let startWithSpace = false; + let startEnd; + let startNlPos = -1; + for (startEnd = 0; startEnd < value.length; ++startEnd) { + const ch = value[startEnd]; + if (ch === " ") + startWithSpace = true; + else if (ch === "\n") + startNlPos = startEnd; + else + break; + } + let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd); + if (start) { + value = value.substring(start.length); + start = start.replace(/\n+/g, `$&${indent}`); + } + const indentSize = indent ? "2" : "1"; + let header = (startWithSpace ? indentSize : "") + chomp; + if (comment) { + header += " " + commentString(comment.replace(/ ?[\r\n]+/g, " ")); + if (onComment) + onComment(); + } + if (!literal) { + const foldedValue = value.replace(/\n+/g, "\n$&").replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/\n+/g, `$&${indent}`); + let literalFallback = false; + const foldOptions = getFoldOptions(ctx, true); + if (blockQuote !== "folded" && type !== Scalar.Scalar.BLOCK_FOLDED) { + foldOptions.onOverflow = () => { + literalFallback = true; + }; + } + const body = foldFlowLines.foldFlowLines(`${start}${foldedValue}${end}`, indent, foldFlowLines.FOLD_BLOCK, foldOptions); + if (!literalFallback) + return `>${header} +${indent}${body}`; + } + value = value.replace(/\n+/g, `$&${indent}`); + return `|${header} +${indent}${start}${value}${end}`; + } + function plainString(item, ctx, onComment, onChompKeep) { + const { type, value } = item; + const { actualString, implicitKey, indent, indentStep, inFlow } = ctx; + if (implicitKey && value.includes("\n") || inFlow && /[[\]{},]/.test(value)) { + return quotedString(value, ctx); + } + if (/^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) { + return implicitKey || inFlow || !value.includes("\n") ? quotedString(value, ctx) : blockString(item, ctx, onComment, onChompKeep); + } + if (!implicitKey && !inFlow && type !== Scalar.Scalar.PLAIN && value.includes("\n")) { + return blockString(item, ctx, onComment, onChompKeep); + } + if (containsDocumentMarker(value)) { + if (indent === "") { + ctx.forceBlockIndent = true; + return blockString(item, ctx, onComment, onChompKeep); + } else if (implicitKey && indent === indentStep) { + return quotedString(value, ctx); + } + } + const str = value.replace(/\n+/g, `$& +${indent}`); + if (actualString) { + const test = (tag) => tag.default && tag.tag !== "tag:yaml.org,2002:str" && tag.test?.test(str); + const { compat, tags } = ctx.doc.schema; + if (tags.some(test) || compat?.some(test)) + return quotedString(value, ctx); + } + return implicitKey ? str : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false)); + } + function stringifyString(item, ctx, onComment, onChompKeep) { + const { implicitKey, inFlow } = ctx; + const ss = typeof item.value === "string" ? item : Object.assign({}, item, { value: String(item.value) }); + let { type } = item; + if (type !== Scalar.Scalar.QUOTE_DOUBLE) { + if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value)) + type = Scalar.Scalar.QUOTE_DOUBLE; + } + const _stringify = (_type) => { + switch (_type) { + case Scalar.Scalar.BLOCK_FOLDED: + case Scalar.Scalar.BLOCK_LITERAL: + return implicitKey || inFlow ? quotedString(ss.value, ctx) : blockString(ss, ctx, onComment, onChompKeep); + case Scalar.Scalar.QUOTE_DOUBLE: + return doubleQuotedString(ss.value, ctx); + case Scalar.Scalar.QUOTE_SINGLE: + return singleQuotedString(ss.value, ctx); + case Scalar.Scalar.PLAIN: + return plainString(ss, ctx, onComment, onChompKeep); + default: + return null; + } + }; + let res = _stringify(type); + if (res === null) { + const { defaultKeyType, defaultStringType } = ctx.options; + const t = implicitKey && defaultKeyType || defaultStringType; + res = _stringify(t); + if (res === null) + throw new Error(`Unsupported default string type ${t}`); + } + return res; + } + exports.stringifyString = stringifyString; + } +}); + +// node_modules/yaml/dist/stringify/stringify.js +var require_stringify = __commonJS({ + "node_modules/yaml/dist/stringify/stringify.js"(exports) { + "use strict"; + var anchors = require_anchors(); + var identity = require_identity(); + var stringifyComment = require_stringifyComment(); + var stringifyString = require_stringifyString(); + function createStringifyContext(doc, options) { + const opt = Object.assign({ + blockQuote: true, + commentString: stringifyComment.stringifyComment, + defaultKeyType: null, + defaultStringType: "PLAIN", + directives: null, + doubleQuotedAsJSON: false, + doubleQuotedMinMultiLineLength: 40, + falseStr: "false", + flowCollectionPadding: true, + indentSeq: true, + lineWidth: 80, + minContentWidth: 20, + nullStr: "null", + simpleKeys: false, + singleQuote: null, + trailingComma: false, + trueStr: "true", + verifyAliasOrder: true + }, doc.schema.toStringOptions, options); + let inFlow; + switch (opt.collectionStyle) { + case "block": + inFlow = false; + break; + case "flow": + inFlow = true; + break; + default: + inFlow = null; + } + return { + anchors: /* @__PURE__ */ new Set(), + doc, + flowCollectionPadding: opt.flowCollectionPadding ? " " : "", + indent: "", + indentStep: typeof opt.indent === "number" ? " ".repeat(opt.indent) : " ", + inFlow, + options: opt + }; + } + function getTagObject(tags, item) { + if (item.tag) { + const match = tags.filter((t) => t.tag === item.tag); + if (match.length > 0) + return match.find((t) => t.format === item.format) ?? match[0]; + } + let tagObj = void 0; + let obj; + if (identity.isScalar(item)) { + obj = item.value; + let match = tags.filter((t) => t.identify?.(obj)); + if (match.length > 1) { + const testMatch = match.filter((t) => t.test); + if (testMatch.length > 0) + match = testMatch; + } + tagObj = match.find((t) => t.format === item.format) ?? match.find((t) => !t.format); + } else { + obj = item; + tagObj = tags.find((t) => t.nodeClass && obj instanceof t.nodeClass); + } + if (!tagObj) { + const name = obj?.constructor?.name ?? (obj === null ? "null" : typeof obj); + throw new Error(`Tag not resolved for ${name} value`); + } + return tagObj; + } + function stringifyProps(node, tagObj, { anchors: anchors$1, doc }) { + if (!doc.directives) + return ""; + const props = []; + const anchor = (identity.isScalar(node) || identity.isCollection(node)) && node.anchor; + if (anchor && anchors.anchorIsValid(anchor)) { + anchors$1.add(anchor); + props.push(`&${anchor}`); + } + const tag = node.tag ?? (tagObj.default ? null : tagObj.tag); + if (tag) + props.push(doc.directives.tagString(tag)); + return props.join(" "); + } + function stringify(item, ctx, onComment, onChompKeep) { + if (identity.isPair(item)) + return item.toString(ctx, onComment, onChompKeep); + if (identity.isAlias(item)) { + if (ctx.doc.directives) + return item.toString(ctx); + if (ctx.resolvedAliases?.has(item)) { + throw new TypeError(`Cannot stringify circular structure without alias nodes`); + } else { + if (ctx.resolvedAliases) + ctx.resolvedAliases.add(item); + else + ctx.resolvedAliases = /* @__PURE__ */ new Set([item]); + item = item.resolve(ctx.doc); + } + } + let tagObj = void 0; + const node = identity.isNode(item) ? item : ctx.doc.createNode(item, { onTagObj: (o) => tagObj = o }); + tagObj ?? (tagObj = getTagObject(ctx.doc.schema.tags, node)); + const props = stringifyProps(node, tagObj, ctx); + if (props.length > 0) + ctx.indentAtStart = (ctx.indentAtStart ?? 0) + props.length + 1; + const str = typeof tagObj.stringify === "function" ? tagObj.stringify(node, ctx, onComment, onChompKeep) : identity.isScalar(node) ? stringifyString.stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep); + if (!props) + return str; + return identity.isScalar(node) || str[0] === "{" || str[0] === "[" ? `${props} ${str}` : `${props} +${ctx.indent}${str}`; + } + exports.createStringifyContext = createStringifyContext; + exports.stringify = stringify; + } +}); + +// node_modules/yaml/dist/stringify/stringifyPair.js +var require_stringifyPair = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyPair.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var stringify = require_stringify(); + var stringifyComment = require_stringifyComment(); + function stringifyPair({ key, value }, ctx, onComment, onChompKeep) { + const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx; + let keyComment = identity.isNode(key) && key.comment || null; + if (simpleKeys) { + if (keyComment) { + throw new Error("With simple keys, key nodes cannot have comments"); + } + if (identity.isCollection(key) || !identity.isNode(key) && typeof key === "object") { + const msg = "With simple keys, collection cannot be used as a key value"; + throw new Error(msg); + } + } + let explicitKey = !simpleKeys && (!key || keyComment && value == null && !ctx.inFlow || identity.isCollection(key) || (identity.isScalar(key) ? key.type === Scalar.Scalar.BLOCK_FOLDED || key.type === Scalar.Scalar.BLOCK_LITERAL : typeof key === "object")); + ctx = Object.assign({}, ctx, { + allNullValues: false, + implicitKey: !explicitKey && (simpleKeys || !allNullValues), + indent: indent + indentStep + }); + let keyCommentDone = false; + let chompKeep = false; + let str = stringify.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true); + if (!explicitKey && !ctx.inFlow && str.length > 1024) { + if (simpleKeys) + throw new Error("With simple keys, single line scalar must not span more than 1024 characters"); + explicitKey = true; + } + if (ctx.inFlow) { + if (allNullValues || value == null) { + if (keyCommentDone && onComment) + onComment(); + return str === "" ? "?" : explicitKey ? `? ${str}` : str; + } + } else if (allNullValues && !simpleKeys || value == null && explicitKey) { + str = `? ${str}`; + if (keyComment && !keyCommentDone) { + str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment)); + } else if (chompKeep && onChompKeep) + onChompKeep(); + return str; + } + if (keyCommentDone) + keyComment = null; + if (explicitKey) { + if (keyComment) + str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment)); + str = `? ${str} +${indent}:`; + } else { + str = `${str}:`; + if (keyComment) + str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment)); + } + let vsb, vcb, valueComment; + if (identity.isNode(value)) { + vsb = !!value.spaceBefore; + vcb = value.commentBefore; + valueComment = value.comment; + } else { + vsb = false; + vcb = null; + valueComment = null; + if (value && typeof value === "object") + value = doc.createNode(value); + } + ctx.implicitKey = false; + if (!explicitKey && !keyComment && identity.isScalar(value)) + ctx.indentAtStart = str.length + 1; + chompKeep = false; + if (!indentSeq && indentStep.length >= 2 && !ctx.inFlow && !explicitKey && identity.isSeq(value) && !value.flow && !value.tag && !value.anchor) { + ctx.indent = ctx.indent.substring(2); + } + let valueCommentDone = false; + const valueStr = stringify.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true); + let ws = " "; + if (keyComment || vsb || vcb) { + ws = vsb ? "\n" : ""; + if (vcb) { + const cs = commentString(vcb); + ws += ` +${stringifyComment.indentComment(cs, ctx.indent)}`; + } + if (valueStr === "" && !ctx.inFlow) { + if (ws === "\n" && valueComment) + ws = "\n\n"; + } else { + ws += ` +${ctx.indent}`; + } + } else if (!explicitKey && identity.isCollection(value)) { + const vs0 = valueStr[0]; + const nl0 = valueStr.indexOf("\n"); + const hasNewline = nl0 !== -1; + const flow = ctx.inFlow ?? value.flow ?? value.items.length === 0; + if (hasNewline || !flow) { + let hasPropsLine = false; + if (hasNewline && (vs0 === "&" || vs0 === "!")) { + let sp0 = valueStr.indexOf(" "); + if (vs0 === "&" && sp0 !== -1 && sp0 < nl0 && valueStr[sp0 + 1] === "!") { + sp0 = valueStr.indexOf(" ", sp0 + 1); + } + if (sp0 === -1 || nl0 < sp0) + hasPropsLine = true; + } + if (!hasPropsLine) + ws = ` +${ctx.indent}`; + } + } else if (valueStr === "" || valueStr[0] === "\n") { + ws = ""; + } + str += ws + valueStr; + if (ctx.inFlow) { + if (valueCommentDone && onComment) + onComment(); + } else if (valueComment && !valueCommentDone) { + str += stringifyComment.lineComment(str, ctx.indent, commentString(valueComment)); + } else if (chompKeep && onChompKeep) { + onChompKeep(); + } + return str; + } + exports.stringifyPair = stringifyPair; + } +}); + +// node_modules/yaml/dist/log.js +var require_log = __commonJS({ + "node_modules/yaml/dist/log.js"(exports) { + "use strict"; + var node_process = __require("process"); + function debug(logLevel, ...messages) { + if (logLevel === "debug") + console.log(...messages); + } + function warn(logLevel, warning) { + if (logLevel === "debug" || logLevel === "warn") { + if (typeof node_process.emitWarning === "function") + node_process.emitWarning(warning); + else + console.warn(warning); + } + } + exports.debug = debug; + exports.warn = warn; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/merge.js +var require_merge = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/merge.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var MERGE_KEY = "<<"; + var merge = { + identify: (value) => value === MERGE_KEY || typeof value === "symbol" && value.description === MERGE_KEY, + default: "key", + tag: "tag:yaml.org,2002:merge", + test: /^<<$/, + resolve: () => Object.assign(new Scalar.Scalar(Symbol(MERGE_KEY)), { + addToJSMap: addMergeToJSMap + }), + stringify: () => MERGE_KEY + }; + var isMergeKey = (ctx, key) => (merge.identify(key) || identity.isScalar(key) && (!key.type || key.type === Scalar.Scalar.PLAIN) && merge.identify(key.value)) && ctx?.doc.schema.tags.some((tag) => tag.tag === merge.tag && tag.default); + function addMergeToJSMap(ctx, map, value) { + const source = resolveAliasValue(ctx, value); + if (identity.isSeq(source)) + for (const it of source.items) + mergeValue(ctx, map, it); + else if (Array.isArray(source)) + for (const it of source) + mergeValue(ctx, map, it); + else + mergeValue(ctx, map, source); + } + function mergeValue(ctx, map, value) { + const source = resolveAliasValue(ctx, value); + if (!identity.isMap(source)) + throw new Error("Merge sources must be maps or map aliases"); + const srcMap = source.toJSON(null, ctx, Map); + for (const [key, value2] of srcMap) { + if (map instanceof Map) { + if (!map.has(key)) + map.set(key, value2); + } else if (map instanceof Set) { + map.add(key); + } else if (!Object.prototype.hasOwnProperty.call(map, key)) { + Object.defineProperty(map, key, { + value: value2, + writable: true, + enumerable: true, + configurable: true + }); + } + } + return map; + } + function resolveAliasValue(ctx, value) { + return ctx && identity.isAlias(value) ? value.resolve(ctx.doc, ctx) : value; + } + exports.addMergeToJSMap = addMergeToJSMap; + exports.isMergeKey = isMergeKey; + exports.merge = merge; + } +}); + +// node_modules/yaml/dist/nodes/addPairToJSMap.js +var require_addPairToJSMap = __commonJS({ + "node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports) { + "use strict"; + var log = require_log(); + var merge = require_merge(); + var stringify = require_stringify(); + var identity = require_identity(); + var toJS = require_toJS(); + function addPairToJSMap(ctx, map, { key, value }) { + if (identity.isNode(key) && key.addToJSMap) + key.addToJSMap(ctx, map, value); + else if (merge.isMergeKey(ctx, key)) + merge.addMergeToJSMap(ctx, map, value); + else { + const jsKey = toJS.toJS(key, "", ctx); + if (map instanceof Map) { + map.set(jsKey, toJS.toJS(value, jsKey, ctx)); + } else if (map instanceof Set) { + map.add(jsKey); + } else { + const stringKey = stringifyKey(key, jsKey, ctx); + const jsValue = toJS.toJS(value, stringKey, ctx); + if (stringKey in map) + Object.defineProperty(map, stringKey, { + value: jsValue, + writable: true, + enumerable: true, + configurable: true + }); + else + map[stringKey] = jsValue; + } + } + return map; + } + function stringifyKey(key, jsKey, ctx) { + if (jsKey === null) + return ""; + if (typeof jsKey !== "object") + return String(jsKey); + if (identity.isNode(key) && ctx?.doc) { + const strCtx = stringify.createStringifyContext(ctx.doc, {}); + strCtx.anchors = /* @__PURE__ */ new Set(); + for (const node of ctx.anchors.keys()) + strCtx.anchors.add(node.anchor); + strCtx.inFlow = true; + strCtx.inStringifyKey = true; + const strKey = key.toString(strCtx); + if (!ctx.mapKeyWarned) { + let jsonStr = JSON.stringify(strKey); + if (jsonStr.length > 40) + jsonStr = jsonStr.substring(0, 36) + '..."'; + log.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`); + ctx.mapKeyWarned = true; + } + return strKey; + } + return JSON.stringify(jsKey); + } + exports.addPairToJSMap = addPairToJSMap; + } +}); + +// node_modules/yaml/dist/nodes/Pair.js +var require_Pair = __commonJS({ + "node_modules/yaml/dist/nodes/Pair.js"(exports) { + "use strict"; + var createNode = require_createNode(); + var stringifyPair = require_stringifyPair(); + var addPairToJSMap = require_addPairToJSMap(); + var identity = require_identity(); + function createPair(key, value, ctx) { + const k = createNode.createNode(key, void 0, ctx); + const v = createNode.createNode(value, void 0, ctx); + return new Pair(k, v); + } + var Pair = class _Pair { + constructor(key, value = null) { + Object.defineProperty(this, identity.NODE_TYPE, { value: identity.PAIR }); + this.key = key; + this.value = value; + } + clone(schema) { + let { key, value } = this; + if (identity.isNode(key)) + key = key.clone(schema); + if (identity.isNode(value)) + value = value.clone(schema); + return new _Pair(key, value); + } + toJSON(_, ctx) { + const pair = ctx?.mapAsMap ? /* @__PURE__ */ new Map() : {}; + return addPairToJSMap.addPairToJSMap(ctx, pair, this); + } + toString(ctx, onComment, onChompKeep) { + return ctx?.doc ? stringifyPair.stringifyPair(this, ctx, onComment, onChompKeep) : JSON.stringify(this); + } + }; + exports.Pair = Pair; + exports.createPair = createPair; + } +}); + +// node_modules/yaml/dist/stringify/stringifyCollection.js +var require_stringifyCollection = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyCollection.js"(exports) { + "use strict"; + var identity = require_identity(); + var stringify = require_stringify(); + var stringifyComment = require_stringifyComment(); + function stringifyCollection(collection, ctx, options) { + const flow = ctx.inFlow ?? collection.flow; + const stringify2 = flow ? stringifyFlowCollection : stringifyBlockCollection; + return stringify2(collection, ctx, options); + } + function stringifyBlockCollection({ comment, items }, ctx, { blockItemPrefix, flowChars, itemIndent, onChompKeep, onComment }) { + const { indent, options: { commentString } } = ctx; + const itemCtx = Object.assign({}, ctx, { indent: itemIndent, type: null }); + let chompKeep = false; + const lines = []; + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + let comment2 = null; + if (identity.isNode(item)) { + if (!chompKeep && item.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, item.commentBefore, chompKeep); + if (item.comment) + comment2 = item.comment; + } else if (identity.isPair(item)) { + const ik = identity.isNode(item.key) ? item.key : null; + if (ik) { + if (!chompKeep && ik.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, ik.commentBefore, chompKeep); + } + } + chompKeep = false; + let str2 = stringify.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true); + if (comment2) + str2 += stringifyComment.lineComment(str2, itemIndent, commentString(comment2)); + if (chompKeep && comment2) + chompKeep = false; + lines.push(blockItemPrefix + str2); + } + let str; + if (lines.length === 0) { + str = flowChars.start + flowChars.end; + } else { + str = lines[0]; + for (let i = 1; i < lines.length; ++i) { + const line = lines[i]; + str += line ? ` +${indent}${line}` : "\n"; + } + } + if (comment) { + str += "\n" + stringifyComment.indentComment(commentString(comment), indent); + if (onComment) + onComment(); + } else if (chompKeep && onChompKeep) + onChompKeep(); + return str; + } + function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) { + const { indent, indentStep, flowCollectionPadding: fcPadding, options: { commentString } } = ctx; + itemIndent += indentStep; + const itemCtx = Object.assign({}, ctx, { + indent: itemIndent, + inFlow: true, + type: null + }); + let reqNewline = false; + let linesAtValue = 0; + const lines = []; + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + let comment = null; + if (identity.isNode(item)) { + if (item.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, item.commentBefore, false); + if (item.comment) + comment = item.comment; + } else if (identity.isPair(item)) { + const ik = identity.isNode(item.key) ? item.key : null; + if (ik) { + if (ik.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, ik.commentBefore, false); + if (ik.comment) + reqNewline = true; + } + const iv = identity.isNode(item.value) ? item.value : null; + if (iv) { + if (iv.comment) + comment = iv.comment; + if (iv.commentBefore) + reqNewline = true; + } else if (item.value == null && ik?.comment) { + comment = ik.comment; + } + } + if (comment) + reqNewline = true; + let str = stringify.stringify(item, itemCtx, () => comment = null); + reqNewline || (reqNewline = lines.length > linesAtValue || str.includes("\n")); + if (i < items.length - 1) { + str += ","; + } else if (ctx.options.trailingComma) { + if (ctx.options.lineWidth > 0) { + reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth); + } + if (reqNewline) { + str += ","; + } + } + if (comment) + str += stringifyComment.lineComment(str, itemIndent, commentString(comment)); + lines.push(str); + linesAtValue = lines.length; + } + const { start, end } = flowChars; + if (lines.length === 0) { + return start + end; + } else { + if (!reqNewline) { + const len = lines.reduce((sum, line) => sum + line.length + 2, 2); + reqNewline = ctx.options.lineWidth > 0 && len > ctx.options.lineWidth; + } + if (reqNewline) { + let str = start; + for (const line of lines) + str += line ? ` +${indentStep}${indent}${line}` : "\n"; + return `${str} +${indent}${end}`; + } else { + return `${start}${fcPadding}${lines.join(" ")}${fcPadding}${end}`; + } + } + } + function addCommentBefore({ indent, options: { commentString } }, lines, comment, chompKeep) { + if (comment && chompKeep) + comment = comment.replace(/^\n+/, ""); + if (comment) { + const ic = stringifyComment.indentComment(commentString(comment), indent); + lines.push(ic.trimStart()); + } + } + exports.stringifyCollection = stringifyCollection; + } +}); + +// node_modules/yaml/dist/nodes/YAMLMap.js +var require_YAMLMap = __commonJS({ + "node_modules/yaml/dist/nodes/YAMLMap.js"(exports) { + "use strict"; + var stringifyCollection = require_stringifyCollection(); + var addPairToJSMap = require_addPairToJSMap(); + var Collection = require_Collection(); + var identity = require_identity(); + var Pair = require_Pair(); + var Scalar = require_Scalar(); + function findPair(items, key) { + const k = identity.isScalar(key) ? key.value : key; + for (const it of items) { + if (identity.isPair(it)) { + if (it.key === key || it.key === k) + return it; + if (identity.isScalar(it.key) && it.key.value === k) + return it; + } + } + return void 0; + } + var YAMLMap = class extends Collection.Collection { + static get tagName() { + return "tag:yaml.org,2002:map"; + } + constructor(schema) { + super(identity.MAP, schema); + this.items = []; + } + /** + * A generic collection parsing method that can be extended + * to other node classes that inherit from YAMLMap + */ + static from(schema, obj, ctx) { + const { keepUndefined, replacer } = ctx; + const map = new this(schema); + const add = (key, value) => { + if (typeof replacer === "function") + value = replacer.call(obj, key, value); + else if (Array.isArray(replacer) && !replacer.includes(key)) + return; + if (value !== void 0 || keepUndefined) + map.items.push(Pair.createPair(key, value, ctx)); + }; + if (obj instanceof Map) { + for (const [key, value] of obj) + add(key, value); + } else if (obj && typeof obj === "object") { + for (const key of Object.keys(obj)) + add(key, obj[key]); + } + if (typeof schema.sortMapEntries === "function") { + map.items.sort(schema.sortMapEntries); + } + return map; + } + /** + * Adds a value to the collection. + * + * @param overwrite - If not set `true`, using a key that is already in the + * collection will throw. Otherwise, overwrites the previous value. + */ + add(pair, overwrite) { + let _pair; + if (identity.isPair(pair)) + _pair = pair; + else if (!pair || typeof pair !== "object" || !("key" in pair)) { + _pair = new Pair.Pair(pair, pair?.value); + } else + _pair = new Pair.Pair(pair.key, pair.value); + const prev = findPair(this.items, _pair.key); + const sortEntries = this.schema?.sortMapEntries; + if (prev) { + if (!overwrite) + throw new Error(`Key ${_pair.key} already set`); + if (identity.isScalar(prev.value) && Scalar.isScalarValue(_pair.value)) + prev.value.value = _pair.value; + else + prev.value = _pair.value; + } else if (sortEntries) { + const i = this.items.findIndex((item) => sortEntries(_pair, item) < 0); + if (i === -1) + this.items.push(_pair); + else + this.items.splice(i, 0, _pair); + } else { + this.items.push(_pair); + } + } + delete(key) { + const it = findPair(this.items, key); + if (!it) + return false; + const del = this.items.splice(this.items.indexOf(it), 1); + return del.length > 0; + } + get(key, keepScalar) { + const it = findPair(this.items, key); + const node = it?.value; + return (!keepScalar && identity.isScalar(node) ? node.value : node) ?? void 0; + } + has(key) { + return !!findPair(this.items, key); + } + set(key, value) { + this.add(new Pair.Pair(key, value), true); + } + /** + * @param ctx - Conversion context, originally set in Document#toJS() + * @param {Class} Type - If set, forces the returned collection type + * @returns Instance of Type, Map, or Object + */ + toJSON(_, ctx, Type) { + const map = Type ? new Type() : ctx?.mapAsMap ? /* @__PURE__ */ new Map() : {}; + if (ctx?.onCreate) + ctx.onCreate(map); + for (const item of this.items) + addPairToJSMap.addPairToJSMap(ctx, map, item); + return map; + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + for (const item of this.items) { + if (!identity.isPair(item)) + throw new Error(`Map items must all be pairs; found ${JSON.stringify(item)} instead`); + } + if (!ctx.allNullValues && this.hasAllNullValues(false)) + ctx = Object.assign({}, ctx, { allNullValues: true }); + return stringifyCollection.stringifyCollection(this, ctx, { + blockItemPrefix: "", + flowChars: { start: "{", end: "}" }, + itemIndent: ctx.indent || "", + onChompKeep, + onComment + }); + } + }; + exports.YAMLMap = YAMLMap; + exports.findPair = findPair; + } +}); + +// node_modules/yaml/dist/schema/common/map.js +var require_map = __commonJS({ + "node_modules/yaml/dist/schema/common/map.js"(exports) { + "use strict"; + var identity = require_identity(); + var YAMLMap = require_YAMLMap(); + var map = { + collection: "map", + default: true, + nodeClass: YAMLMap.YAMLMap, + tag: "tag:yaml.org,2002:map", + resolve(map2, onError) { + if (!identity.isMap(map2)) + onError("Expected a mapping for this tag"); + return map2; + }, + createNode: (schema, obj, ctx) => YAMLMap.YAMLMap.from(schema, obj, ctx) + }; + exports.map = map; + } +}); + +// node_modules/yaml/dist/nodes/YAMLSeq.js +var require_YAMLSeq = __commonJS({ + "node_modules/yaml/dist/nodes/YAMLSeq.js"(exports) { + "use strict"; + var createNode = require_createNode(); + var stringifyCollection = require_stringifyCollection(); + var Collection = require_Collection(); + var identity = require_identity(); + var Scalar = require_Scalar(); + var toJS = require_toJS(); + var YAMLSeq = class extends Collection.Collection { + static get tagName() { + return "tag:yaml.org,2002:seq"; + } + constructor(schema) { + super(identity.SEQ, schema); + this.items = []; + } + add(value) { + this.items.push(value); + } + /** + * Removes a value from the collection. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + * + * @returns `true` if the item was found and removed. + */ + delete(key) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + return false; + const del = this.items.splice(idx, 1); + return del.length > 0; + } + get(key, keepScalar) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + return void 0; + const it = this.items[idx]; + return !keepScalar && identity.isScalar(it) ? it.value : it; + } + /** + * Checks if the collection includes a value with the key `key`. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + */ + has(key) { + const idx = asItemIndex(key); + return typeof idx === "number" && idx < this.items.length; + } + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + * + * If `key` does not contain a representation of an integer, this will throw. + * It may be wrapped in a `Scalar`. + */ + set(key, value) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + throw new Error(`Expected a valid index, not ${key}.`); + const prev = this.items[idx]; + if (identity.isScalar(prev) && Scalar.isScalarValue(value)) + prev.value = value; + else + this.items[idx] = value; + } + toJSON(_, ctx) { + const seq = []; + if (ctx?.onCreate) + ctx.onCreate(seq); + let i = 0; + for (const item of this.items) + seq.push(toJS.toJS(item, String(i++), ctx)); + return seq; + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + return stringifyCollection.stringifyCollection(this, ctx, { + blockItemPrefix: "- ", + flowChars: { start: "[", end: "]" }, + itemIndent: (ctx.indent || "") + " ", + onChompKeep, + onComment + }); + } + static from(schema, obj, ctx) { + const { replacer } = ctx; + const seq = new this(schema); + if (obj && Symbol.iterator in Object(obj)) { + let i = 0; + for (let it of obj) { + if (typeof replacer === "function") { + const key = obj instanceof Set ? it : String(i++); + it = replacer.call(obj, key, it); + } + seq.items.push(createNode.createNode(it, void 0, ctx)); + } + } + return seq; + } + }; + function asItemIndex(key) { + let idx = identity.isScalar(key) ? key.value : key; + if (idx && typeof idx === "string") + idx = Number(idx); + return typeof idx === "number" && Number.isInteger(idx) && idx >= 0 ? idx : null; + } + exports.YAMLSeq = YAMLSeq; + } +}); + +// node_modules/yaml/dist/schema/common/seq.js +var require_seq = __commonJS({ + "node_modules/yaml/dist/schema/common/seq.js"(exports) { + "use strict"; + var identity = require_identity(); + var YAMLSeq = require_YAMLSeq(); + var seq = { + collection: "seq", + default: true, + nodeClass: YAMLSeq.YAMLSeq, + tag: "tag:yaml.org,2002:seq", + resolve(seq2, onError) { + if (!identity.isSeq(seq2)) + onError("Expected a sequence for this tag"); + return seq2; + }, + createNode: (schema, obj, ctx) => YAMLSeq.YAMLSeq.from(schema, obj, ctx) + }; + exports.seq = seq; + } +}); + +// node_modules/yaml/dist/schema/common/string.js +var require_string = __commonJS({ + "node_modules/yaml/dist/schema/common/string.js"(exports) { + "use strict"; + var stringifyString = require_stringifyString(); + var string = { + identify: (value) => typeof value === "string", + default: true, + tag: "tag:yaml.org,2002:str", + resolve: (str) => str, + stringify(item, ctx, onComment, onChompKeep) { + ctx = Object.assign({ actualString: true }, ctx); + return stringifyString.stringifyString(item, ctx, onComment, onChompKeep); + } + }; + exports.string = string; + } +}); + +// node_modules/yaml/dist/schema/common/null.js +var require_null = __commonJS({ + "node_modules/yaml/dist/schema/common/null.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var nullTag = { + identify: (value) => value == null, + createNode: () => new Scalar.Scalar(null), + default: true, + tag: "tag:yaml.org,2002:null", + test: /^(?:~|[Nn]ull|NULL)?$/, + resolve: () => new Scalar.Scalar(null), + stringify: ({ source }, ctx) => typeof source === "string" && nullTag.test.test(source) ? source : ctx.options.nullStr + }; + exports.nullTag = nullTag; + } +}); + +// node_modules/yaml/dist/schema/core/bool.js +var require_bool = __commonJS({ + "node_modules/yaml/dist/schema/core/bool.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var boolTag = { + identify: (value) => typeof value === "boolean", + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/, + resolve: (str) => new Scalar.Scalar(str[0] === "t" || str[0] === "T"), + stringify({ source, value }, ctx) { + if (source && boolTag.test.test(source)) { + const sv = source[0] === "t" || source[0] === "T"; + if (value === sv) + return source; + } + return value ? ctx.options.trueStr : ctx.options.falseStr; + } + }; + exports.boolTag = boolTag; + } +}); + +// node_modules/yaml/dist/stringify/stringifyNumber.js +var require_stringifyNumber = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyNumber.js"(exports) { + "use strict"; + function stringifyNumber({ format, minFractionDigits, tag, value }) { + if (typeof value === "bigint") + return String(value); + const num = typeof value === "number" ? value : Number(value); + if (!isFinite(num)) + return isNaN(num) ? ".nan" : num < 0 ? "-.inf" : ".inf"; + let n = Object.is(value, -0) ? "-0" : JSON.stringify(value); + if (!format && minFractionDigits && (!tag || tag === "tag:yaml.org,2002:float") && /^-?\d/.test(n) && !n.includes("e")) { + let i = n.indexOf("."); + if (i < 0) { + i = n.length; + n += "."; + } + let d = minFractionDigits - (n.length - i - 1); + while (d-- > 0) + n += "0"; + } + return n; + } + exports.stringifyNumber = stringifyNumber; + } +}); + +// node_modules/yaml/dist/schema/core/float.js +var require_float = __commonJS({ + "node_modules/yaml/dist/schema/core/float.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var stringifyNumber = require_stringifyNumber(); + var floatNaN = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/, + resolve: (str) => str.slice(-3).toLowerCase() === "nan" ? NaN : str[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, + stringify: stringifyNumber.stringifyNumber + }; + var floatExp = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "EXP", + test: /^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)[eE][-+]?[0-9]+$/, + resolve: (str) => parseFloat(str), + stringify(node) { + const num = Number(node.value); + return isFinite(num) ? num.toExponential() : stringifyNumber.stringifyNumber(node); + } + }; + var float = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^[-+]?(?:\.[0-9]+|[0-9]+\.[0-9]*)$/, + resolve(str) { + const node = new Scalar.Scalar(parseFloat(str)); + const dot = str.indexOf("."); + if (dot !== -1 && str[str.length - 1] === "0") + node.minFractionDigits = str.length - dot - 1; + return node; + }, + stringify: stringifyNumber.stringifyNumber + }; + exports.float = float; + exports.floatExp = floatExp; + exports.floatNaN = floatNaN; + } +}); + +// node_modules/yaml/dist/schema/core/int.js +var require_int = __commonJS({ + "node_modules/yaml/dist/schema/core/int.js"(exports) { + "use strict"; + var stringifyNumber = require_stringifyNumber(); + var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value); + var intResolve = (str, offset, radix, { intAsBigInt }) => intAsBigInt ? BigInt(str) : parseInt(str.substring(offset), radix); + function intStringify(node, radix, prefix) { + const { value } = node; + if (intIdentify(value) && value >= 0) + return prefix + value.toString(radix); + return stringifyNumber.stringifyNumber(node); + } + var intOct = { + identify: (value) => intIdentify(value) && value >= 0, + default: true, + tag: "tag:yaml.org,2002:int", + format: "OCT", + test: /^0o[0-7]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 8, opt), + stringify: (node) => intStringify(node, 8, "0o") + }; + var int = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^[-+]?[0-9]+$/, + resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt), + stringify: stringifyNumber.stringifyNumber + }; + var intHex = { + identify: (value) => intIdentify(value) && value >= 0, + default: true, + tag: "tag:yaml.org,2002:int", + format: "HEX", + test: /^0x[0-9a-fA-F]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt), + stringify: (node) => intStringify(node, 16, "0x") + }; + exports.int = int; + exports.intHex = intHex; + exports.intOct = intOct; + } +}); + +// node_modules/yaml/dist/schema/core/schema.js +var require_schema = __commonJS({ + "node_modules/yaml/dist/schema/core/schema.js"(exports) { + "use strict"; + var map = require_map(); + var _null = require_null(); + var seq = require_seq(); + var string = require_string(); + var bool = require_bool(); + var float = require_float(); + var int = require_int(); + var schema = [ + map.map, + seq.seq, + string.string, + _null.nullTag, + bool.boolTag, + int.intOct, + int.int, + int.intHex, + float.floatNaN, + float.floatExp, + float.float + ]; + exports.schema = schema; + } +}); + +// node_modules/yaml/dist/schema/json/schema.js +var require_schema2 = __commonJS({ + "node_modules/yaml/dist/schema/json/schema.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var map = require_map(); + var seq = require_seq(); + function intIdentify(value) { + return typeof value === "bigint" || Number.isInteger(value); + } + var stringifyJSON = ({ value }) => JSON.stringify(value); + var jsonScalars = [ + { + identify: (value) => typeof value === "string", + default: true, + tag: "tag:yaml.org,2002:str", + resolve: (str) => str, + stringify: stringifyJSON + }, + { + identify: (value) => value == null, + createNode: () => new Scalar.Scalar(null), + default: true, + tag: "tag:yaml.org,2002:null", + test: /^null$/, + resolve: () => null, + stringify: stringifyJSON + }, + { + identify: (value) => typeof value === "boolean", + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^true$|^false$/, + resolve: (str) => str === "true", + stringify: stringifyJSON + }, + { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^-?(?:0|[1-9][0-9]*)$/, + resolve: (str, _onError, { intAsBigInt }) => intAsBigInt ? BigInt(str) : parseInt(str, 10), + stringify: ({ value }) => intIdentify(value) ? value.toString() : JSON.stringify(value) + }, + { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$/, + resolve: (str) => parseFloat(str), + stringify: stringifyJSON + } + ]; + var jsonError = { + default: true, + tag: "", + test: /^/, + resolve(str, onError) { + onError(`Unresolved plain scalar ${JSON.stringify(str)}`); + return str; + } + }; + var schema = [map.map, seq.seq].concat(jsonScalars, jsonError); + exports.schema = schema; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/binary.js +var require_binary = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/binary.js"(exports) { + "use strict"; + var node_buffer = __require("buffer"); + var Scalar = require_Scalar(); + var stringifyString = require_stringifyString(); + var binary = { + identify: (value) => value instanceof Uint8Array, + // Buffer inherits from Uint8Array + default: false, + tag: "tag:yaml.org,2002:binary", + /** + * Returns a Buffer in node and an Uint8Array in browsers + * + * To use the resulting buffer as an image, you'll want to do something like: + * + * const blob = new Blob([buffer], { type: 'image/jpeg' }) + * document.querySelector('#photo').src = URL.createObjectURL(blob) + */ + resolve(src, onError) { + if (typeof node_buffer.Buffer === "function") { + return node_buffer.Buffer.from(src, "base64"); + } else if (typeof atob === "function") { + const str = atob(src.replace(/[\n\r]/g, "")); + const buffer = new Uint8Array(str.length); + for (let i = 0; i < str.length; ++i) + buffer[i] = str.charCodeAt(i); + return buffer; + } else { + onError("This environment does not support reading binary tags; either Buffer or atob is required"); + return src; + } + }, + stringify({ comment, type, value }, ctx, onComment, onChompKeep) { + if (!value) + return ""; + const buf = value; + let str; + if (typeof node_buffer.Buffer === "function") { + str = buf instanceof node_buffer.Buffer ? buf.toString("base64") : node_buffer.Buffer.from(buf.buffer).toString("base64"); + } else if (typeof btoa === "function") { + let s = ""; + for (let i = 0; i < buf.length; ++i) + s += String.fromCharCode(buf[i]); + str = btoa(s); + } else { + throw new Error("This environment does not support writing binary tags; either Buffer or btoa is required"); + } + type ?? (type = Scalar.Scalar.BLOCK_LITERAL); + if (type !== Scalar.Scalar.QUOTE_DOUBLE) { + const lineWidth = Math.max(ctx.options.lineWidth - ctx.indent.length, ctx.options.minContentWidth); + const n = Math.ceil(str.length / lineWidth); + const lines = new Array(n); + for (let i = 0, o = 0; i < n; ++i, o += lineWidth) { + lines[i] = str.substr(o, lineWidth); + } + str = lines.join(type === Scalar.Scalar.BLOCK_LITERAL ? "\n" : " "); + } + return stringifyString.stringifyString({ comment, type, value: str }, ctx, onComment, onChompKeep); + } + }; + exports.binary = binary; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/pairs.js +var require_pairs = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/pairs.js"(exports) { + "use strict"; + var identity = require_identity(); + var Pair = require_Pair(); + var Scalar = require_Scalar(); + var YAMLSeq = require_YAMLSeq(); + function resolvePairs(seq, onError) { + if (identity.isSeq(seq)) { + for (let i = 0; i < seq.items.length; ++i) { + let item = seq.items[i]; + if (identity.isPair(item)) + continue; + else if (identity.isMap(item)) { + if (item.items.length > 1) + onError("Each pair must have its own sequence indicator"); + const pair = item.items[0] || new Pair.Pair(new Scalar.Scalar(null)); + if (item.commentBefore) + pair.key.commentBefore = pair.key.commentBefore ? `${item.commentBefore} +${pair.key.commentBefore}` : item.commentBefore; + if (item.comment) { + const cn = pair.value ?? pair.key; + cn.comment = cn.comment ? `${item.comment} +${cn.comment}` : item.comment; + } + item = pair; + } + seq.items[i] = identity.isPair(item) ? item : new Pair.Pair(item); + } + } else + onError("Expected a sequence for this tag"); + return seq; + } + function createPairs(schema, iterable, ctx) { + const { replacer } = ctx; + const pairs2 = new YAMLSeq.YAMLSeq(schema); + pairs2.tag = "tag:yaml.org,2002:pairs"; + let i = 0; + if (iterable && Symbol.iterator in Object(iterable)) + for (let it of iterable) { + if (typeof replacer === "function") + it = replacer.call(iterable, String(i++), it); + let key, value; + if (Array.isArray(it)) { + if (it.length === 2) { + key = it[0]; + value = it[1]; + } else + throw new TypeError(`Expected [key, value] tuple: ${it}`); + } else if (it && it instanceof Object) { + const keys = Object.keys(it); + if (keys.length === 1) { + key = keys[0]; + value = it[key]; + } else { + throw new TypeError(`Expected tuple with one key, not ${keys.length} keys`); + } + } else { + key = it; + } + pairs2.items.push(Pair.createPair(key, value, ctx)); + } + return pairs2; + } + var pairs = { + collection: "seq", + default: false, + tag: "tag:yaml.org,2002:pairs", + resolve: resolvePairs, + createNode: createPairs + }; + exports.createPairs = createPairs; + exports.pairs = pairs; + exports.resolvePairs = resolvePairs; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/omap.js +var require_omap = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/omap.js"(exports) { + "use strict"; + var identity = require_identity(); + var toJS = require_toJS(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var pairs = require_pairs(); + var YAMLOMap = class _YAMLOMap extends YAMLSeq.YAMLSeq { + constructor() { + super(); + this.add = YAMLMap.YAMLMap.prototype.add.bind(this); + this.delete = YAMLMap.YAMLMap.prototype.delete.bind(this); + this.get = YAMLMap.YAMLMap.prototype.get.bind(this); + this.has = YAMLMap.YAMLMap.prototype.has.bind(this); + this.set = YAMLMap.YAMLMap.prototype.set.bind(this); + this.tag = _YAMLOMap.tag; + } + /** + * If `ctx` is given, the return type is actually `Map`, + * but TypeScript won't allow widening the signature of a child method. + */ + toJSON(_, ctx) { + if (!ctx) + return super.toJSON(_); + const map = /* @__PURE__ */ new Map(); + if (ctx?.onCreate) + ctx.onCreate(map); + for (const pair of this.items) { + let key, value; + if (identity.isPair(pair)) { + key = toJS.toJS(pair.key, "", ctx); + value = toJS.toJS(pair.value, key, ctx); + } else { + key = toJS.toJS(pair, "", ctx); + } + if (map.has(key)) + throw new Error("Ordered maps must not include duplicate keys"); + map.set(key, value); + } + return map; + } + static from(schema, iterable, ctx) { + const pairs$1 = pairs.createPairs(schema, iterable, ctx); + const omap2 = new this(); + omap2.items = pairs$1.items; + return omap2; + } + }; + YAMLOMap.tag = "tag:yaml.org,2002:omap"; + var omap = { + collection: "seq", + identify: (value) => value instanceof Map, + nodeClass: YAMLOMap, + default: false, + tag: "tag:yaml.org,2002:omap", + resolve(seq, onError) { + const pairs$1 = pairs.resolvePairs(seq, onError); + const seenKeys = []; + for (const { key } of pairs$1.items) { + if (identity.isScalar(key)) { + if (seenKeys.includes(key.value)) { + onError(`Ordered maps must not include duplicate keys: ${key.value}`); + } else { + seenKeys.push(key.value); + } + } + } + return Object.assign(new YAMLOMap(), pairs$1); + }, + createNode: (schema, iterable, ctx) => YAMLOMap.from(schema, iterable, ctx) + }; + exports.YAMLOMap = YAMLOMap; + exports.omap = omap; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/bool.js +var require_bool2 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/bool.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + function boolStringify({ value, source }, ctx) { + const boolObj = value ? trueTag : falseTag; + if (source && boolObj.test.test(source)) + return source; + return value ? ctx.options.trueStr : ctx.options.falseStr; + } + var trueTag = { + identify: (value) => value === true, + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:Y|y|[Yy]es|YES|[Tt]rue|TRUE|[Oo]n|ON)$/, + resolve: () => new Scalar.Scalar(true), + stringify: boolStringify + }; + var falseTag = { + identify: (value) => value === false, + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:N|n|[Nn]o|NO|[Ff]alse|FALSE|[Oo]ff|OFF)$/, + resolve: () => new Scalar.Scalar(false), + stringify: boolStringify + }; + exports.falseTag = falseTag; + exports.trueTag = trueTag; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/float.js +var require_float2 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/float.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var stringifyNumber = require_stringifyNumber(); + var floatNaN = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/, + resolve: (str) => str.slice(-3).toLowerCase() === "nan" ? NaN : str[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, + stringify: stringifyNumber.stringifyNumber + }; + var floatExp = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "EXP", + test: /^[-+]?(?:[0-9][0-9_]*)?(?:\.[0-9_]*)?[eE][-+]?[0-9]+$/, + resolve: (str) => parseFloat(str.replace(/_/g, "")), + stringify(node) { + const num = Number(node.value); + return isFinite(num) ? num.toExponential() : stringifyNumber.stringifyNumber(node); + } + }; + var float = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*$/, + resolve(str) { + const node = new Scalar.Scalar(parseFloat(str.replace(/_/g, ""))); + const dot = str.indexOf("."); + if (dot !== -1) { + const f = str.substring(dot + 1).replace(/_/g, ""); + if (f[f.length - 1] === "0") + node.minFractionDigits = f.length; + } + return node; + }, + stringify: stringifyNumber.stringifyNumber + }; + exports.float = float; + exports.floatExp = floatExp; + exports.floatNaN = floatNaN; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/int.js +var require_int2 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/int.js"(exports) { + "use strict"; + var stringifyNumber = require_stringifyNumber(); + var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value); + function intResolve(str, offset, radix, { intAsBigInt }) { + const sign = str[0]; + if (sign === "-" || sign === "+") + offset += 1; + str = str.substring(offset).replace(/_/g, ""); + if (intAsBigInt) { + switch (radix) { + case 2: + str = `0b${str}`; + break; + case 8: + str = `0o${str}`; + break; + case 16: + str = `0x${str}`; + break; + } + const n2 = BigInt(str); + return sign === "-" ? BigInt(-1) * n2 : n2; + } + const n = parseInt(str, radix); + return sign === "-" ? -1 * n : n; + } + function intStringify(node, radix, prefix) { + const { value } = node; + if (intIdentify(value)) { + const str = value.toString(radix); + return value < 0 ? "-" + prefix + str.substr(1) : prefix + str; + } + return stringifyNumber.stringifyNumber(node); + } + var intBin = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + format: "BIN", + test: /^[-+]?0b[0-1_]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 2, opt), + stringify: (node) => intStringify(node, 2, "0b") + }; + var intOct = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + format: "OCT", + test: /^[-+]?0[0-7_]+$/, + resolve: (str, _onError, opt) => intResolve(str, 1, 8, opt), + stringify: (node) => intStringify(node, 8, "0") + }; + var int = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^[-+]?[0-9][0-9_]*$/, + resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt), + stringify: stringifyNumber.stringifyNumber + }; + var intHex = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + format: "HEX", + test: /^[-+]?0x[0-9a-fA-F_]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt), + stringify: (node) => intStringify(node, 16, "0x") + }; + exports.int = int; + exports.intBin = intBin; + exports.intHex = intHex; + exports.intOct = intOct; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/set.js +var require_set = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/set.js"(exports) { + "use strict"; + var identity = require_identity(); + var Pair = require_Pair(); + var YAMLMap = require_YAMLMap(); + var YAMLSet = class _YAMLSet extends YAMLMap.YAMLMap { + constructor(schema) { + super(schema); + this.tag = _YAMLSet.tag; + } + add(key) { + let pair; + if (identity.isPair(key)) + pair = key; + else if (key && typeof key === "object" && "key" in key && "value" in key && key.value === null) + pair = new Pair.Pair(key.key, null); + else + pair = new Pair.Pair(key, null); + const prev = YAMLMap.findPair(this.items, pair.key); + if (!prev) + this.items.push(pair); + } + /** + * If `keepPair` is `true`, returns the Pair matching `key`. + * Otherwise, returns the value of that Pair's key. + */ + get(key, keepPair) { + const pair = YAMLMap.findPair(this.items, key); + return !keepPair && identity.isPair(pair) ? identity.isScalar(pair.key) ? pair.key.value : pair.key : pair; + } + set(key, value) { + if (typeof value !== "boolean") + throw new Error(`Expected boolean value for set(key, value) in a YAML set, not ${typeof value}`); + const prev = YAMLMap.findPair(this.items, key); + if (prev && !value) { + this.items.splice(this.items.indexOf(prev), 1); + } else if (!prev && value) { + this.items.push(new Pair.Pair(key)); + } + } + toJSON(_, ctx) { + return super.toJSON(_, ctx, Set); + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + if (this.hasAllNullValues(true)) + return super.toString(Object.assign({}, ctx, { allNullValues: true }), onComment, onChompKeep); + else + throw new Error("Set items must all have null values"); + } + static from(schema, iterable, ctx) { + const { replacer } = ctx; + const set2 = new this(schema); + if (iterable && Symbol.iterator in Object(iterable)) + for (let value of iterable) { + if (typeof replacer === "function") + value = replacer.call(iterable, value, value); + set2.items.push(Pair.createPair(value, null, ctx)); + } + return set2; + } + }; + YAMLSet.tag = "tag:yaml.org,2002:set"; + var set = { + collection: "map", + identify: (value) => value instanceof Set, + nodeClass: YAMLSet, + default: false, + tag: "tag:yaml.org,2002:set", + createNode: (schema, iterable, ctx) => YAMLSet.from(schema, iterable, ctx), + resolve(map, onError) { + if (identity.isMap(map)) { + if (map.hasAllNullValues(true)) + return Object.assign(new YAMLSet(), map); + else + onError("Set items must all have null values"); + } else + onError("Expected a mapping for this tag"); + return map; + } + }; + exports.YAMLSet = YAMLSet; + exports.set = set; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +var require_timestamp = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/timestamp.js"(exports) { + "use strict"; + var stringifyNumber = require_stringifyNumber(); + function parseSexagesimal(str, asBigInt) { + const sign = str[0]; + const parts = sign === "-" || sign === "+" ? str.substring(1) : str; + const num = (n) => asBigInt ? BigInt(n) : Number(n); + const res = parts.replace(/_/g, "").split(":").reduce((res2, p) => res2 * num(60) + num(p), num(0)); + return sign === "-" ? num(-1) * res : res; + } + function stringifySexagesimal(node) { + let { value } = node; + let num = (n) => n; + if (typeof value === "bigint") + num = (n) => BigInt(n); + else if (isNaN(value) || !isFinite(value)) + return stringifyNumber.stringifyNumber(node); + let sign = ""; + if (value < 0) { + sign = "-"; + value *= num(-1); + } + const _60 = num(60); + const parts = [value % _60]; + if (value < 60) { + parts.unshift(0); + } else { + value = (value - parts[0]) / _60; + parts.unshift(value % _60); + if (value >= 60) { + value = (value - parts[0]) / _60; + parts.unshift(value); + } + } + return sign + parts.map((n) => String(n).padStart(2, "0")).join(":").replace(/000000\d*$/, ""); + } + var intTime = { + identify: (value) => typeof value === "bigint" || Number.isInteger(value), + default: true, + tag: "tag:yaml.org,2002:int", + format: "TIME", + test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/, + resolve: (str, _onError, { intAsBigInt }) => parseSexagesimal(str, intAsBigInt), + stringify: stringifySexagesimal + }; + var floatTime = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "TIME", + test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*$/, + resolve: (str) => parseSexagesimal(str, false), + stringify: stringifySexagesimal + }; + var timestamp = { + identify: (value) => value instanceof Date, + default: true, + tag: "tag:yaml.org,2002:timestamp", + // If the time zone is omitted, the timestamp is assumed to be specified in UTC. The time part + // may be omitted altogether, resulting in a date format. In such a case, the time part is + // assumed to be 00:00:00Z (start of day, UTC). + test: RegExp("^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})(?:(?:t|T|[ \\t]+)([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?)?$"), + resolve(str) { + const match = str.match(timestamp.test); + if (!match) + throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd"); + const [, year, month, day, hour, minute, second] = match.map(Number); + const millisec = match[7] ? Number((match[7] + "00").substr(1, 3)) : 0; + let date = Date.UTC(year, month - 1, day, hour || 0, minute || 0, second || 0, millisec); + const tz = match[8]; + if (tz && tz !== "Z") { + let d = parseSexagesimal(tz, false); + if (Math.abs(d) < 30) + d *= 60; + date -= 6e4 * d; + } + return new Date(date); + }, + stringify: ({ value }) => value?.toISOString().replace(/(T00:00:00)?\.000Z$/, "") ?? "" + }; + exports.floatTime = floatTime; + exports.intTime = intTime; + exports.timestamp = timestamp; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/schema.js +var require_schema3 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/schema.js"(exports) { + "use strict"; + var map = require_map(); + var _null = require_null(); + var seq = require_seq(); + var string = require_string(); + var binary = require_binary(); + var bool = require_bool2(); + var float = require_float2(); + var int = require_int2(); + var merge = require_merge(); + var omap = require_omap(); + var pairs = require_pairs(); + var set = require_set(); + var timestamp = require_timestamp(); + var schema = [ + map.map, + seq.seq, + string.string, + _null.nullTag, + bool.trueTag, + bool.falseTag, + int.intBin, + int.intOct, + int.int, + int.intHex, + float.floatNaN, + float.floatExp, + float.float, + binary.binary, + merge.merge, + omap.omap, + pairs.pairs, + set.set, + timestamp.intTime, + timestamp.floatTime, + timestamp.timestamp + ]; + exports.schema = schema; + } +}); + +// node_modules/yaml/dist/schema/tags.js +var require_tags = __commonJS({ + "node_modules/yaml/dist/schema/tags.js"(exports) { + "use strict"; + var map = require_map(); + var _null = require_null(); + var seq = require_seq(); + var string = require_string(); + var bool = require_bool(); + var float = require_float(); + var int = require_int(); + var schema = require_schema(); + var schema$1 = require_schema2(); + var binary = require_binary(); + var merge = require_merge(); + var omap = require_omap(); + var pairs = require_pairs(); + var schema$2 = require_schema3(); + var set = require_set(); + var timestamp = require_timestamp(); + var schemas = /* @__PURE__ */ new Map([ + ["core", schema.schema], + ["failsafe", [map.map, seq.seq, string.string]], + ["json", schema$1.schema], + ["yaml11", schema$2.schema], + ["yaml-1.1", schema$2.schema] + ]); + var tagsByName = { + binary: binary.binary, + bool: bool.boolTag, + float: float.float, + floatExp: float.floatExp, + floatNaN: float.floatNaN, + floatTime: timestamp.floatTime, + int: int.int, + intHex: int.intHex, + intOct: int.intOct, + intTime: timestamp.intTime, + map: map.map, + merge: merge.merge, + null: _null.nullTag, + omap: omap.omap, + pairs: pairs.pairs, + seq: seq.seq, + set: set.set, + timestamp: timestamp.timestamp + }; + var coreKnownTags = { + "tag:yaml.org,2002:binary": binary.binary, + "tag:yaml.org,2002:merge": merge.merge, + "tag:yaml.org,2002:omap": omap.omap, + "tag:yaml.org,2002:pairs": pairs.pairs, + "tag:yaml.org,2002:set": set.set, + "tag:yaml.org,2002:timestamp": timestamp.timestamp + }; + function getTags(customTags, schemaName, addMergeTag) { + const schemaTags = schemas.get(schemaName); + if (schemaTags && !customTags) { + return addMergeTag && !schemaTags.includes(merge.merge) ? schemaTags.concat(merge.merge) : schemaTags.slice(); + } + let tags = schemaTags; + if (!tags) { + if (Array.isArray(customTags)) + tags = []; + else { + const keys = Array.from(schemas.keys()).filter((key) => key !== "yaml11").map((key) => JSON.stringify(key)).join(", "); + throw new Error(`Unknown schema "${schemaName}"; use one of ${keys} or define customTags array`); + } + } + if (Array.isArray(customTags)) { + for (const tag of customTags) + tags = tags.concat(tag); + } else if (typeof customTags === "function") { + tags = customTags(tags.slice()); + } + if (addMergeTag) + tags = tags.concat(merge.merge); + return tags.reduce((tags2, tag) => { + const tagObj = typeof tag === "string" ? tagsByName[tag] : tag; + if (!tagObj) { + const tagName = JSON.stringify(tag); + const keys = Object.keys(tagsByName).map((key) => JSON.stringify(key)).join(", "); + throw new Error(`Unknown custom tag ${tagName}; use one of ${keys}`); + } + if (!tags2.includes(tagObj)) + tags2.push(tagObj); + return tags2; + }, []); + } + exports.coreKnownTags = coreKnownTags; + exports.getTags = getTags; + } +}); + +// node_modules/yaml/dist/schema/Schema.js +var require_Schema = __commonJS({ + "node_modules/yaml/dist/schema/Schema.js"(exports) { + "use strict"; + var identity = require_identity(); + var map = require_map(); + var seq = require_seq(); + var string = require_string(); + var tags = require_tags(); + var sortMapEntriesByKey = (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0; + var Schema = class _Schema { + constructor({ compat, customTags, merge, resolveKnownTags, schema, sortMapEntries, toStringDefaults }) { + this.compat = Array.isArray(compat) ? tags.getTags(compat, "compat") : compat ? tags.getTags(null, compat) : null; + this.name = typeof schema === "string" && schema || "core"; + this.knownTags = resolveKnownTags ? tags.coreKnownTags : {}; + this.tags = tags.getTags(customTags, this.name, merge); + this.toStringOptions = toStringDefaults ?? null; + Object.defineProperty(this, identity.MAP, { value: map.map }); + Object.defineProperty(this, identity.SCALAR, { value: string.string }); + Object.defineProperty(this, identity.SEQ, { value: seq.seq }); + this.sortMapEntries = typeof sortMapEntries === "function" ? sortMapEntries : sortMapEntries === true ? sortMapEntriesByKey : null; + } + clone() { + const copy = Object.create(_Schema.prototype, Object.getOwnPropertyDescriptors(this)); + copy.tags = this.tags.slice(); + return copy; + } + }; + exports.Schema = Schema; + } +}); + +// node_modules/yaml/dist/stringify/stringifyDocument.js +var require_stringifyDocument = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyDocument.js"(exports) { + "use strict"; + var identity = require_identity(); + var stringify = require_stringify(); + var stringifyComment = require_stringifyComment(); + function stringifyDocument(doc, options) { + const lines = []; + let hasDirectives = options.directives === true; + if (options.directives !== false && doc.directives) { + const dir = doc.directives.toString(doc); + if (dir) { + lines.push(dir); + hasDirectives = true; + } else if (doc.directives.docStart) + hasDirectives = true; + } + if (hasDirectives) + lines.push("---"); + const ctx = stringify.createStringifyContext(doc, options); + const { commentString } = ctx.options; + if (doc.commentBefore) { + if (lines.length !== 1) + lines.unshift(""); + const cs = commentString(doc.commentBefore); + lines.unshift(stringifyComment.indentComment(cs, "")); + } + let chompKeep = false; + let contentComment = null; + if (doc.contents) { + if (identity.isNode(doc.contents)) { + if (doc.contents.spaceBefore && hasDirectives) + lines.push(""); + if (doc.contents.commentBefore) { + const cs = commentString(doc.contents.commentBefore); + lines.push(stringifyComment.indentComment(cs, "")); + } + ctx.forceBlockIndent = !!doc.comment; + contentComment = doc.contents.comment; + } + const onChompKeep = contentComment ? void 0 : () => chompKeep = true; + let body = stringify.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep); + if (contentComment) + body += stringifyComment.lineComment(body, "", commentString(contentComment)); + if ((body[0] === "|" || body[0] === ">") && lines[lines.length - 1] === "---") { + lines[lines.length - 1] = `--- ${body}`; + } else + lines.push(body); + } else { + lines.push(stringify.stringify(doc.contents, ctx)); + } + if (doc.directives?.docEnd) { + if (doc.comment) { + const cs = commentString(doc.comment); + if (cs.includes("\n")) { + lines.push("..."); + lines.push(stringifyComment.indentComment(cs, "")); + } else { + lines.push(`... ${cs}`); + } + } else { + lines.push("..."); + } + } else { + let dc = doc.comment; + if (dc && chompKeep) + dc = dc.replace(/^\n+/, ""); + if (dc) { + if ((!chompKeep || contentComment) && lines[lines.length - 1] !== "") + lines.push(""); + lines.push(stringifyComment.indentComment(commentString(dc), "")); + } + } + return lines.join("\n") + "\n"; + } + exports.stringifyDocument = stringifyDocument; + } +}); + +// node_modules/yaml/dist/doc/Document.js +var require_Document = __commonJS({ + "node_modules/yaml/dist/doc/Document.js"(exports) { + "use strict"; + var Alias = require_Alias(); + var Collection = require_Collection(); + var identity = require_identity(); + var Pair = require_Pair(); + var toJS = require_toJS(); + var Schema = require_Schema(); + var stringifyDocument = require_stringifyDocument(); + var anchors = require_anchors(); + var applyReviver = require_applyReviver(); + var createNode = require_createNode(); + var directives = require_directives(); + var Document = class _Document { + constructor(value, replacer, options) { + this.commentBefore = null; + this.comment = null; + this.errors = []; + this.warnings = []; + Object.defineProperty(this, identity.NODE_TYPE, { value: identity.DOC }); + let _replacer = null; + if (typeof replacer === "function" || Array.isArray(replacer)) { + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + replacer = void 0; + } + const opt = Object.assign({ + intAsBigInt: false, + keepSourceTokens: false, + logLevel: "warn", + prettyErrors: true, + strict: true, + stringKeys: false, + uniqueKeys: true, + version: "1.2" + }, options); + this.options = opt; + let { version } = opt; + if (options?._directives) { + this.directives = options._directives.atDocument(); + if (this.directives.yaml.explicit) + version = this.directives.yaml.version; + } else + this.directives = new directives.Directives({ version }); + this.setSchema(version, options); + this.contents = value === void 0 ? null : this.createNode(value, _replacer, options); + } + /** + * Create a deep copy of this Document and its contents. + * + * Custom Node values that inherit from `Object` still refer to their original instances. + */ + clone() { + const copy = Object.create(_Document.prototype, { + [identity.NODE_TYPE]: { value: identity.DOC } + }); + copy.commentBefore = this.commentBefore; + copy.comment = this.comment; + copy.errors = this.errors.slice(); + copy.warnings = this.warnings.slice(); + copy.options = Object.assign({}, this.options); + if (this.directives) + copy.directives = this.directives.clone(); + copy.schema = this.schema.clone(); + copy.contents = identity.isNode(this.contents) ? this.contents.clone(copy.schema) : this.contents; + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** Adds a value to the document. */ + add(value) { + if (assertCollection(this.contents)) + this.contents.add(value); + } + /** Adds a value to the document. */ + addIn(path, value) { + if (assertCollection(this.contents)) + this.contents.addIn(path, value); + } + /** + * Create a new `Alias` node, ensuring that the target `node` has the required anchor. + * + * If `node` already has an anchor, `name` is ignored. + * Otherwise, the `node.anchor` value will be set to `name`, + * or if an anchor with that name is already present in the document, + * `name` will be used as a prefix for a new unique anchor. + * If `name` is undefined, the generated anchor will use 'a' as a prefix. + */ + createAlias(node, name) { + if (!node.anchor) { + const prev = anchors.anchorNames(this); + node.anchor = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + !name || prev.has(name) ? anchors.findNewAnchor(name || "a", prev) : name; + } + return new Alias.Alias(node.anchor); + } + createNode(value, replacer, options) { + let _replacer = void 0; + if (typeof replacer === "function") { + value = replacer.call({ "": value }, "", value); + _replacer = replacer; + } else if (Array.isArray(replacer)) { + const keyToStr = (v) => typeof v === "number" || v instanceof String || v instanceof Number; + const asStr = replacer.filter(keyToStr).map(String); + if (asStr.length > 0) + replacer = replacer.concat(asStr); + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + replacer = void 0; + } + const { aliasDuplicateObjects, anchorPrefix, flow, keepUndefined, onTagObj, tag } = options ?? {}; + const { onAnchor, setAnchors, sourceObjects } = anchors.createNodeAnchors( + this, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + anchorPrefix || "a" + ); + const ctx = { + aliasDuplicateObjects: aliasDuplicateObjects ?? true, + keepUndefined: keepUndefined ?? false, + onAnchor, + onTagObj, + replacer: _replacer, + schema: this.schema, + sourceObjects + }; + const node = createNode.createNode(value, tag, ctx); + if (flow && identity.isCollection(node)) + node.flow = true; + setAnchors(); + return node; + } + /** + * Convert a key and a value into a `Pair` using the current schema, + * recursively wrapping all values as `Scalar` or `Collection` nodes. + */ + createPair(key, value, options = {}) { + const k = this.createNode(key, null, options); + const v = this.createNode(value, null, options); + return new Pair.Pair(k, v); + } + /** + * Removes a value from the document. + * @returns `true` if the item was found and removed. + */ + delete(key) { + return assertCollection(this.contents) ? this.contents.delete(key) : false; + } + /** + * Removes a value from the document. + * @returns `true` if the item was found and removed. + */ + deleteIn(path) { + if (Collection.isEmptyPath(path)) { + if (this.contents == null) + return false; + this.contents = null; + return true; + } + return assertCollection(this.contents) ? this.contents.deleteIn(path) : false; + } + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + get(key, keepScalar) { + return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : void 0; + } + /** + * Returns item at `path`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn(path, keepScalar) { + if (Collection.isEmptyPath(path)) + return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents; + return identity.isCollection(this.contents) ? this.contents.getIn(path, keepScalar) : void 0; + } + /** + * Checks if the document includes a value with the key `key`. + */ + has(key) { + return identity.isCollection(this.contents) ? this.contents.has(key) : false; + } + /** + * Checks if the document includes a value at `path`. + */ + hasIn(path) { + if (Collection.isEmptyPath(path)) + return this.contents !== void 0; + return identity.isCollection(this.contents) ? this.contents.hasIn(path) : false; + } + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + set(key, value) { + if (this.contents == null) { + this.contents = Collection.collectionFromPath(this.schema, [key], value); + } else if (assertCollection(this.contents)) { + this.contents.set(key, value); + } + } + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn(path, value) { + if (Collection.isEmptyPath(path)) { + this.contents = value; + } else if (this.contents == null) { + this.contents = Collection.collectionFromPath(this.schema, Array.from(path), value); + } else if (assertCollection(this.contents)) { + this.contents.setIn(path, value); + } + } + /** + * Change the YAML version and schema used by the document. + * A `null` version disables support for directives, explicit tags, anchors, and aliases. + * It also requires the `schema` option to be given as a `Schema` instance value. + * + * Overrides all previously set schema options. + */ + setSchema(version, options = {}) { + if (typeof version === "number") + version = String(version); + let opt; + switch (version) { + case "1.1": + if (this.directives) + this.directives.yaml.version = "1.1"; + else + this.directives = new directives.Directives({ version: "1.1" }); + opt = { resolveKnownTags: false, schema: "yaml-1.1" }; + break; + case "1.2": + case "next": + if (this.directives) + this.directives.yaml.version = version; + else + this.directives = new directives.Directives({ version }); + opt = { resolveKnownTags: true, schema: "core" }; + break; + case null: + if (this.directives) + delete this.directives; + opt = null; + break; + default: { + const sv = JSON.stringify(version); + throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`); + } + } + if (options.schema instanceof Object) + this.schema = options.schema; + else if (opt) + this.schema = new Schema.Schema(Object.assign(opt, options)); + else + throw new Error(`With a null YAML version, the { schema: Schema } option is required`); + } + // json & jsonArg are only used from toJSON() + toJS({ json, jsonArg, mapAsMap, maxAliasCount, onAnchor, reviver } = {}) { + const ctx = { + anchors: /* @__PURE__ */ new Map(), + doc: this, + keep: !json, + mapAsMap: mapAsMap === true, + mapKeyWarned: false, + maxAliasCount: typeof maxAliasCount === "number" ? maxAliasCount : 100 + }; + const res = toJS.toJS(this.contents, jsonArg ?? "", ctx); + if (typeof onAnchor === "function") + for (const { count, res: res2 } of ctx.anchors.values()) + onAnchor(res2, count); + return typeof reviver === "function" ? applyReviver.applyReviver(reviver, { "": res }, "", res) : res; + } + /** + * A JSON representation of the document `contents`. + * + * @param jsonArg Used by `JSON.stringify` to indicate the array index or + * property name. + */ + toJSON(jsonArg, onAnchor) { + return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor }); + } + /** A YAML representation of the document. */ + toString(options = {}) { + if (this.errors.length > 0) + throw new Error("Document with errors cannot be stringified"); + if ("indent" in options && (!Number.isInteger(options.indent) || Number(options.indent) <= 0)) { + const s = JSON.stringify(options.indent); + throw new Error(`"indent" option must be a positive integer, not ${s}`); + } + return stringifyDocument.stringifyDocument(this, options); + } + }; + function assertCollection(contents) { + if (identity.isCollection(contents)) + return true; + throw new Error("Expected a YAML collection as document contents"); + } + exports.Document = Document; + } +}); + +// node_modules/yaml/dist/errors.js +var require_errors = __commonJS({ + "node_modules/yaml/dist/errors.js"(exports) { + "use strict"; + var YAMLError = class extends Error { + constructor(name, pos, code, message) { + super(); + this.name = name; + this.code = code; + this.message = message; + this.pos = pos; + } + }; + var YAMLParseError = class extends YAMLError { + constructor(pos, code, message) { + super("YAMLParseError", pos, code, message); + } + }; + var YAMLWarning = class extends YAMLError { + constructor(pos, code, message) { + super("YAMLWarning", pos, code, message); + } + }; + var prettifyError = (src, lc) => (error) => { + if (error.pos[0] === -1) + return; + error.linePos = error.pos.map((pos) => lc.linePos(pos)); + const { line, col } = error.linePos[0]; + error.message += ` at line ${line}, column ${col}`; + let ci = col - 1; + let lineStr = src.substring(lc.lineStarts[line - 1], lc.lineStarts[line]).replace(/[\n\r]+$/, ""); + if (ci >= 60 && lineStr.length > 80) { + const trimStart = Math.min(ci - 39, lineStr.length - 79); + lineStr = "…" + lineStr.substring(trimStart); + ci -= trimStart - 1; + } + if (lineStr.length > 80) + lineStr = lineStr.substring(0, 79) + "…"; + if (line > 1 && /^ *$/.test(lineStr.substring(0, ci))) { + let prev = src.substring(lc.lineStarts[line - 2], lc.lineStarts[line - 1]); + if (prev.length > 80) + prev = prev.substring(0, 79) + "…\n"; + lineStr = prev + lineStr; + } + if (/[^ ]/.test(lineStr)) { + let count = 1; + const end = error.linePos[1]; + if (end?.line === line && end.col > col) { + count = Math.max(1, Math.min(end.col - col, 80 - ci)); + } + const pointer = " ".repeat(ci) + "^".repeat(count); + error.message += `: + +${lineStr} +${pointer} +`; + } + }; + exports.YAMLError = YAMLError; + exports.YAMLParseError = YAMLParseError; + exports.YAMLWarning = YAMLWarning; + exports.prettifyError = prettifyError; + } +}); + +// node_modules/yaml/dist/compose/resolve-props.js +var require_resolve_props = __commonJS({ + "node_modules/yaml/dist/compose/resolve-props.js"(exports) { + "use strict"; + function resolveProps(tokens, { flow, indicator, next, offset, onError, parentIndent, startOnNewline }) { + let spaceBefore = false; + let atNewline = startOnNewline; + let hasSpace = startOnNewline; + let comment = ""; + let commentSep = ""; + let hasNewline = false; + let reqSpace = false; + let tab = null; + let anchor = null; + let tag = null; + let newlineAfterProp = null; + let comma = null; + let found = null; + let start = null; + for (const token of tokens) { + if (reqSpace) { + if (token.type !== "space" && token.type !== "newline" && token.type !== "comma") + onError(token.offset, "MISSING_CHAR", "Tags and anchors must be separated from the next token by white space"); + reqSpace = false; + } + if (tab) { + if (atNewline && token.type !== "comment" && token.type !== "newline") { + onError(tab, "TAB_AS_INDENT", "Tabs are not allowed as indentation"); + } + tab = null; + } + switch (token.type) { + case "space": + if (!flow && (indicator !== "doc-start" || next?.type !== "flow-collection") && token.source.includes(" ")) { + tab = token; + } + hasSpace = true; + break; + case "comment": { + if (!hasSpace) + onError(token, "MISSING_CHAR", "Comments must be separated from other tokens by white space characters"); + const cb = token.source.substring(1) || " "; + if (!comment) + comment = cb; + else + comment += commentSep + cb; + commentSep = ""; + atNewline = false; + break; + } + case "newline": + if (atNewline) { + if (comment) + comment += token.source; + else if (!found || indicator !== "seq-item-ind") + spaceBefore = true; + } else + commentSep += token.source; + atNewline = true; + hasNewline = true; + if (anchor || tag) + newlineAfterProp = token; + hasSpace = true; + break; + case "anchor": + if (anchor) + onError(token, "MULTIPLE_ANCHORS", "A node can have at most one anchor"); + if (token.source.endsWith(":")) + onError(token.offset + token.source.length - 1, "BAD_ALIAS", "Anchor ending in : is ambiguous", true); + anchor = token; + start ?? (start = token.offset); + atNewline = false; + hasSpace = false; + reqSpace = true; + break; + case "tag": { + if (tag) + onError(token, "MULTIPLE_TAGS", "A node can have at most one tag"); + tag = token; + start ?? (start = token.offset); + atNewline = false; + hasSpace = false; + reqSpace = true; + break; + } + case indicator: + if (anchor || tag) + onError(token, "BAD_PROP_ORDER", `Anchors and tags must be after the ${token.source} indicator`); + if (found) + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${token.source} in ${flow ?? "collection"}`); + found = token; + atNewline = indicator === "seq-item-ind" || indicator === "explicit-key-ind"; + hasSpace = false; + break; + case "comma": + if (flow) { + if (comma) + onError(token, "UNEXPECTED_TOKEN", `Unexpected , in ${flow}`); + comma = token; + atNewline = false; + hasSpace = false; + break; + } + // else fallthrough + default: + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${token.type} token`); + atNewline = false; + hasSpace = false; + } + } + const last = tokens[tokens.length - 1]; + const end = last ? last.offset + last.source.length : offset; + if (reqSpace && next && next.type !== "space" && next.type !== "newline" && next.type !== "comma" && (next.type !== "scalar" || next.source !== "")) { + onError(next.offset, "MISSING_CHAR", "Tags and anchors must be separated from the next token by white space"); + } + if (tab && (atNewline && tab.indent <= parentIndent || next?.type === "block-map" || next?.type === "block-seq")) + onError(tab, "TAB_AS_INDENT", "Tabs are not allowed as indentation"); + return { + comma, + found, + spaceBefore, + comment, + hasNewline, + anchor, + tag, + newlineAfterProp, + end, + start: start ?? end + }; + } + exports.resolveProps = resolveProps; + } +}); + +// node_modules/yaml/dist/compose/util-contains-newline.js +var require_util_contains_newline = __commonJS({ + "node_modules/yaml/dist/compose/util-contains-newline.js"(exports) { + "use strict"; + function containsNewline(key) { + if (!key) + return null; + switch (key.type) { + case "alias": + case "scalar": + case "double-quoted-scalar": + case "single-quoted-scalar": + if (key.source.includes("\n")) + return true; + if (key.end) { + for (const st of key.end) + if (st.type === "newline") + return true; + } + return false; + case "flow-collection": + for (const it of key.items) { + for (const st of it.start) + if (st.type === "newline") + return true; + if (it.sep) { + for (const st of it.sep) + if (st.type === "newline") + return true; + } + if (containsNewline(it.key) || containsNewline(it.value)) + return true; + } + return false; + default: + return true; + } + } + exports.containsNewline = containsNewline; + } +}); + +// node_modules/yaml/dist/compose/util-flow-indent-check.js +var require_util_flow_indent_check = __commonJS({ + "node_modules/yaml/dist/compose/util-flow-indent-check.js"(exports) { + "use strict"; + var utilContainsNewline = require_util_contains_newline(); + function flowIndentCheck(indent, fc, onError) { + if (fc?.type === "flow-collection") { + const end = fc.end[0]; + if (end.indent === indent && (end.source === "]" || end.source === "}") && utilContainsNewline.containsNewline(fc)) { + const msg = "Flow end indicator should be more indented than parent"; + onError(end, "BAD_INDENT", msg, true); + } + } + } + exports.flowIndentCheck = flowIndentCheck; + } +}); + +// node_modules/yaml/dist/compose/util-map-includes.js +var require_util_map_includes = __commonJS({ + "node_modules/yaml/dist/compose/util-map-includes.js"(exports) { + "use strict"; + var identity = require_identity(); + function mapIncludes(ctx, items, search) { + const { uniqueKeys } = ctx.options; + if (uniqueKeys === false) + return false; + const isEqual = typeof uniqueKeys === "function" ? uniqueKeys : (a, b) => a === b || identity.isScalar(a) && identity.isScalar(b) && a.value === b.value; + return items.some((pair) => isEqual(pair.key, search)); + } + exports.mapIncludes = mapIncludes; + } +}); + +// node_modules/yaml/dist/compose/resolve-block-map.js +var require_resolve_block_map = __commonJS({ + "node_modules/yaml/dist/compose/resolve-block-map.js"(exports) { + "use strict"; + var Pair = require_Pair(); + var YAMLMap = require_YAMLMap(); + var resolveProps = require_resolve_props(); + var utilContainsNewline = require_util_contains_newline(); + var utilFlowIndentCheck = require_util_flow_indent_check(); + var utilMapIncludes = require_util_map_includes(); + var startColMsg = "All mapping items must start at the same column"; + function resolveBlockMap({ composeNode, composeEmptyNode }, ctx, bm, onError, tag) { + const NodeClass = tag?.nodeClass ?? YAMLMap.YAMLMap; + const map = new NodeClass(ctx.schema); + if (ctx.atRoot) + ctx.atRoot = false; + let offset = bm.offset; + let commentEnd = null; + for (const collItem of bm.items) { + const { start, key, sep, value } = collItem; + const keyProps = resolveProps.resolveProps(start, { + indicator: "explicit-key-ind", + next: key ?? sep?.[0], + offset, + onError, + parentIndent: bm.indent, + startOnNewline: true + }); + const implicitKey = !keyProps.found; + if (implicitKey) { + if (key) { + if (key.type === "block-seq") + onError(offset, "BLOCK_AS_IMPLICIT_KEY", "A block sequence may not be used as an implicit map key"); + else if ("indent" in key && key.indent !== bm.indent) + onError(offset, "BAD_INDENT", startColMsg); + } + if (!keyProps.anchor && !keyProps.tag && !sep) { + commentEnd = keyProps.end; + if (keyProps.comment) { + if (map.comment) + map.comment += "\n" + keyProps.comment; + else + map.comment = keyProps.comment; + } + continue; + } + if (keyProps.newlineAfterProp || utilContainsNewline.containsNewline(key)) { + onError(key ?? start[start.length - 1], "MULTILINE_IMPLICIT_KEY", "Implicit keys need to be on a single line"); + } + } else if (keyProps.found?.indent !== bm.indent) { + onError(offset, "BAD_INDENT", startColMsg); + } + ctx.atKey = true; + const keyStart = keyProps.end; + const keyNode = key ? composeNode(ctx, key, keyProps, onError) : composeEmptyNode(ctx, keyStart, start, null, keyProps, onError); + if (ctx.schema.compat) + utilFlowIndentCheck.flowIndentCheck(bm.indent, key, onError); + ctx.atKey = false; + if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode)) + onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique"); + const valueProps = resolveProps.resolveProps(sep ?? [], { + indicator: "map-value-ind", + next: value, + offset: keyNode.range[2], + onError, + parentIndent: bm.indent, + startOnNewline: !key || key.type === "block-scalar" + }); + offset = valueProps.end; + if (valueProps.found) { + if (implicitKey) { + if (value?.type === "block-map" && !valueProps.hasNewline) + onError(offset, "BLOCK_AS_IMPLICIT_KEY", "Nested mappings are not allowed in compact mappings"); + if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024) + onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key"); + } + const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep, null, valueProps, onError); + if (ctx.schema.compat) + utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError); + offset = valueNode.range[2]; + const pair = new Pair.Pair(keyNode, valueNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + map.items.push(pair); + } else { + if (implicitKey) + onError(keyNode.range, "MISSING_CHAR", "Implicit map keys need to be followed by map values"); + if (valueProps.comment) { + if (keyNode.comment) + keyNode.comment += "\n" + valueProps.comment; + else + keyNode.comment = valueProps.comment; + } + const pair = new Pair.Pair(keyNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + map.items.push(pair); + } + } + if (commentEnd && commentEnd < offset) + onError(commentEnd, "IMPOSSIBLE", "Map comment with trailing content"); + map.range = [bm.offset, offset, commentEnd ?? offset]; + return map; + } + exports.resolveBlockMap = resolveBlockMap; + } +}); + +// node_modules/yaml/dist/compose/resolve-block-seq.js +var require_resolve_block_seq = __commonJS({ + "node_modules/yaml/dist/compose/resolve-block-seq.js"(exports) { + "use strict"; + var YAMLSeq = require_YAMLSeq(); + var resolveProps = require_resolve_props(); + var utilFlowIndentCheck = require_util_flow_indent_check(); + function resolveBlockSeq({ composeNode, composeEmptyNode }, ctx, bs, onError, tag) { + const NodeClass = tag?.nodeClass ?? YAMLSeq.YAMLSeq; + const seq = new NodeClass(ctx.schema); + if (ctx.atRoot) + ctx.atRoot = false; + if (ctx.atKey) + ctx.atKey = false; + let offset = bs.offset; + let commentEnd = null; + for (const { start, value } of bs.items) { + const props = resolveProps.resolveProps(start, { + indicator: "seq-item-ind", + next: value, + offset, + onError, + parentIndent: bs.indent, + startOnNewline: true + }); + if (!props.found) { + if (props.anchor || props.tag || value) { + if (value?.type === "block-seq") + onError(props.end, "BAD_INDENT", "All sequence items must start at the same column"); + else + onError(offset, "MISSING_CHAR", "Sequence item without - indicator"); + } else { + commentEnd = props.end; + if (props.comment) + seq.comment = props.comment; + continue; + } + } + const node = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, start, null, props, onError); + if (ctx.schema.compat) + utilFlowIndentCheck.flowIndentCheck(bs.indent, value, onError); + offset = node.range[2]; + seq.items.push(node); + } + seq.range = [bs.offset, offset, commentEnd ?? offset]; + return seq; + } + exports.resolveBlockSeq = resolveBlockSeq; + } +}); + +// node_modules/yaml/dist/compose/resolve-end.js +var require_resolve_end = __commonJS({ + "node_modules/yaml/dist/compose/resolve-end.js"(exports) { + "use strict"; + function resolveEnd(end, offset, reqSpace, onError) { + let comment = ""; + if (end) { + let hasSpace = false; + let sep = ""; + for (const token of end) { + const { source, type } = token; + switch (type) { + case "space": + hasSpace = true; + break; + case "comment": { + if (reqSpace && !hasSpace) + onError(token, "MISSING_CHAR", "Comments must be separated from other tokens by white space characters"); + const cb = source.substring(1) || " "; + if (!comment) + comment = cb; + else + comment += sep + cb; + sep = ""; + break; + } + case "newline": + if (comment) + sep += source; + hasSpace = true; + break; + default: + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${type} at node end`); + } + offset += source.length; + } + } + return { comment, offset }; + } + exports.resolveEnd = resolveEnd; + } +}); + +// node_modules/yaml/dist/compose/resolve-flow-collection.js +var require_resolve_flow_collection = __commonJS({ + "node_modules/yaml/dist/compose/resolve-flow-collection.js"(exports) { + "use strict"; + var identity = require_identity(); + var Pair = require_Pair(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var resolveEnd = require_resolve_end(); + var resolveProps = require_resolve_props(); + var utilContainsNewline = require_util_contains_newline(); + var utilMapIncludes = require_util_map_includes(); + var blockMsg = "Block collections are not allowed within flow collections"; + var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq"); + function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) { + const isMap = fc.start.source === "{"; + const fcName = isMap ? "flow map" : "flow sequence"; + const NodeClass = tag?.nodeClass ?? (isMap ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq); + const coll = new NodeClass(ctx.schema); + coll.flow = true; + const atRoot = ctx.atRoot; + if (atRoot) + ctx.atRoot = false; + if (ctx.atKey) + ctx.atKey = false; + let offset = fc.offset + fc.start.source.length; + for (let i = 0; i < fc.items.length; ++i) { + const collItem = fc.items[i]; + const { start, key, sep, value } = collItem; + const props = resolveProps.resolveProps(start, { + flow: fcName, + indicator: "explicit-key-ind", + next: key ?? sep?.[0], + offset, + onError, + parentIndent: fc.indent, + startOnNewline: false + }); + if (!props.found) { + if (!props.anchor && !props.tag && !sep && !value) { + if (i === 0 && props.comma) + onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`); + else if (i < fc.items.length - 1) + onError(props.start, "UNEXPECTED_TOKEN", `Unexpected empty item in ${fcName}`); + if (props.comment) { + if (coll.comment) + coll.comment += "\n" + props.comment; + else + coll.comment = props.comment; + } + offset = props.end; + continue; + } + if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key)) + onError( + key, + // checked by containsNewline() + "MULTILINE_IMPLICIT_KEY", + "Implicit keys of flow sequence pairs need to be on a single line" + ); + } + if (i === 0) { + if (props.comma) + onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`); + } else { + if (!props.comma) + onError(props.start, "MISSING_CHAR", `Missing , between ${fcName} items`); + if (props.comment) { + let prevItemComment = ""; + loop: for (const st of start) { + switch (st.type) { + case "comma": + case "space": + break; + case "comment": + prevItemComment = st.source.substring(1); + break loop; + default: + break loop; + } + } + if (prevItemComment) { + let prev = coll.items[coll.items.length - 1]; + if (identity.isPair(prev)) + prev = prev.value ?? prev.key; + if (prev.comment) + prev.comment += "\n" + prevItemComment; + else + prev.comment = prevItemComment; + props.comment = props.comment.substring(prevItemComment.length + 1); + } + } + } + if (!isMap && !sep && !props.found) { + const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError); + coll.items.push(valueNode); + offset = valueNode.range[2]; + if (isBlock(value)) + onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg); + } else { + ctx.atKey = true; + const keyStart = props.end; + const keyNode = key ? composeNode(ctx, key, props, onError) : composeEmptyNode(ctx, keyStart, start, null, props, onError); + if (isBlock(key)) + onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg); + ctx.atKey = false; + const valueProps = resolveProps.resolveProps(sep ?? [], { + flow: fcName, + indicator: "map-value-ind", + next: value, + offset: keyNode.range[2], + onError, + parentIndent: fc.indent, + startOnNewline: false + }); + if (valueProps.found) { + if (!isMap && !props.found && ctx.options.strict) { + if (sep) + for (const st of sep) { + if (st === valueProps.found) + break; + if (st.type === "newline") { + onError(st, "MULTILINE_IMPLICIT_KEY", "Implicit keys of flow sequence pairs need to be on a single line"); + break; + } + } + if (props.start < valueProps.found.offset - 1024) + onError(valueProps.found, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit flow sequence key"); + } + } else if (value) { + if ("source" in value && value.source?.[0] === ":") + onError(value, "MISSING_CHAR", `Missing space after : in ${fcName}`); + else + onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`); + } + const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep, null, valueProps, onError) : null; + if (valueNode) { + if (isBlock(value)) + onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg); + } else if (valueProps.comment) { + if (keyNode.comment) + keyNode.comment += "\n" + valueProps.comment; + else + keyNode.comment = valueProps.comment; + } + const pair = new Pair.Pair(keyNode, valueNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + if (isMap) { + const map = coll; + if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode)) + onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique"); + map.items.push(pair); + } else { + const map = new YAMLMap.YAMLMap(ctx.schema); + map.flow = true; + map.items.push(pair); + const endRange = (valueNode ?? keyNode).range; + map.range = [keyNode.range[0], endRange[1], endRange[2]]; + coll.items.push(map); + } + offset = valueNode ? valueNode.range[2] : valueProps.end; + } + } + const expectedEnd = isMap ? "}" : "]"; + const [ce, ...ee] = fc.end; + let cePos = offset; + if (ce?.source === expectedEnd) + cePos = ce.offset + ce.source.length; + else { + const name = fcName[0].toUpperCase() + fcName.substring(1); + const msg = atRoot ? `${name} must end with a ${expectedEnd}` : `${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`; + onError(offset, atRoot ? "MISSING_CHAR" : "BAD_INDENT", msg); + if (ce && ce.source.length !== 1) + ee.unshift(ce); + } + if (ee.length > 0) { + const end = resolveEnd.resolveEnd(ee, cePos, ctx.options.strict, onError); + if (end.comment) { + if (coll.comment) + coll.comment += "\n" + end.comment; + else + coll.comment = end.comment; + } + coll.range = [fc.offset, cePos, end.offset]; + } else { + coll.range = [fc.offset, cePos, cePos]; + } + return coll; + } + exports.resolveFlowCollection = resolveFlowCollection; + } +}); + +// node_modules/yaml/dist/compose/compose-collection.js +var require_compose_collection = __commonJS({ + "node_modules/yaml/dist/compose/compose-collection.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var resolveBlockMap = require_resolve_block_map(); + var resolveBlockSeq = require_resolve_block_seq(); + var resolveFlowCollection = require_resolve_flow_collection(); + function resolveCollection(CN, ctx, token, onError, tagName, tag) { + const coll = token.type === "block-map" ? resolveBlockMap.resolveBlockMap(CN, ctx, token, onError, tag) : token.type === "block-seq" ? resolveBlockSeq.resolveBlockSeq(CN, ctx, token, onError, tag) : resolveFlowCollection.resolveFlowCollection(CN, ctx, token, onError, tag); + const Coll = coll.constructor; + if (tagName === "!" || tagName === Coll.tagName) { + coll.tag = Coll.tagName; + return coll; + } + if (tagName) + coll.tag = tagName; + return coll; + } + function composeCollection(CN, ctx, token, props, onError) { + const tagToken = props.tag; + const tagName = !tagToken ? null : ctx.directives.tagName(tagToken.source, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg)); + if (token.type === "block-seq") { + const { anchor, newlineAfterProp: nl } = props; + const lastProp = anchor && tagToken ? anchor.offset > tagToken.offset ? anchor : tagToken : anchor ?? tagToken; + if (lastProp && (!nl || nl.offset < lastProp.offset)) { + const message = "Missing newline after block sequence props"; + onError(lastProp, "MISSING_CHAR", message); + } + } + const expType = token.type === "block-map" ? "map" : token.type === "block-seq" ? "seq" : token.start.source === "{" ? "map" : "seq"; + if (!tagToken || !tagName || tagName === "!" || tagName === YAMLMap.YAMLMap.tagName && expType === "map" || tagName === YAMLSeq.YAMLSeq.tagName && expType === "seq") { + return resolveCollection(CN, ctx, token, onError, tagName); + } + let tag = ctx.schema.tags.find((t) => t.tag === tagName && t.collection === expType); + if (!tag) { + const kt = ctx.schema.knownTags[tagName]; + if (kt?.collection === expType) { + ctx.schema.tags.push(Object.assign({}, kt, { default: false })); + tag = kt; + } else { + if (kt) { + onError(tagToken, "BAD_COLLECTION_TYPE", `${kt.tag} used for ${expType} collection, but expects ${kt.collection ?? "scalar"}`, true); + } else { + onError(tagToken, "TAG_RESOLVE_FAILED", `Unresolved tag: ${tagName}`, true); + } + return resolveCollection(CN, ctx, token, onError, tagName); + } + } + const coll = resolveCollection(CN, ctx, token, onError, tagName, tag); + const res = tag.resolve?.(coll, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg), ctx.options) ?? coll; + const node = identity.isNode(res) ? res : new Scalar.Scalar(res); + node.range = coll.range; + node.tag = tagName; + if (tag?.format) + node.format = tag.format; + return node; + } + exports.composeCollection = composeCollection; + } +}); + +// node_modules/yaml/dist/compose/resolve-block-scalar.js +var require_resolve_block_scalar = __commonJS({ + "node_modules/yaml/dist/compose/resolve-block-scalar.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + function resolveBlockScalar(ctx, scalar, onError) { + const start = scalar.offset; + const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError); + if (!header) + return { value: "", type: null, comment: "", range: [start, start, start] }; + const type = header.mode === ">" ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL; + const lines = scalar.source ? splitLines(scalar.source) : []; + let chompStart = lines.length; + for (let i = lines.length - 1; i >= 0; --i) { + const content = lines[i][1]; + if (content === "" || content === "\r") + chompStart = i; + else + break; + } + if (chompStart === 0) { + const value2 = header.chomp === "+" && lines.length > 0 ? "\n".repeat(Math.max(1, lines.length - 1)) : ""; + let end2 = start + header.length; + if (scalar.source) + end2 += scalar.source.length; + return { value: value2, type, comment: header.comment, range: [start, end2, end2] }; + } + let trimIndent = scalar.indent + header.indent; + let offset = scalar.offset + header.length; + let contentStart = 0; + for (let i = 0; i < chompStart; ++i) { + const [indent, content] = lines[i]; + if (content === "" || content === "\r") { + if (header.indent === 0 && indent.length > trimIndent) + trimIndent = indent.length; + } else { + if (indent.length < trimIndent) { + const message = "Block scalars with more-indented leading empty lines must use an explicit indentation indicator"; + onError(offset + indent.length, "MISSING_CHAR", message); + } + if (header.indent === 0) + trimIndent = indent.length; + contentStart = i; + if (trimIndent === 0 && !ctx.atRoot) { + const message = "Block scalar values in collections must be indented"; + onError(offset, "BAD_INDENT", message); + } + break; + } + offset += indent.length + content.length + 1; + } + for (let i = lines.length - 1; i >= chompStart; --i) { + if (lines[i][0].length > trimIndent) + chompStart = i + 1; + } + let value = ""; + let sep = ""; + let prevMoreIndented = false; + for (let i = 0; i < contentStart; ++i) + value += lines[i][0].slice(trimIndent) + "\n"; + for (let i = contentStart; i < chompStart; ++i) { + let [indent, content] = lines[i]; + offset += indent.length + content.length + 1; + const crlf = content[content.length - 1] === "\r"; + if (crlf) + content = content.slice(0, -1); + if (content && indent.length < trimIndent) { + const src = header.indent ? "explicit indentation indicator" : "first line"; + const message = `Block scalar lines must not be less indented than their ${src}`; + onError(offset - content.length - (crlf ? 2 : 1), "BAD_INDENT", message); + indent = ""; + } + if (type === Scalar.Scalar.BLOCK_LITERAL) { + value += sep + indent.slice(trimIndent) + content; + sep = "\n"; + } else if (indent.length > trimIndent || content[0] === " ") { + if (sep === " ") + sep = "\n"; + else if (!prevMoreIndented && sep === "\n") + sep = "\n\n"; + value += sep + indent.slice(trimIndent) + content; + sep = "\n"; + prevMoreIndented = true; + } else if (content === "") { + if (sep === "\n") + value += "\n"; + else + sep = "\n"; + } else { + value += sep + content; + sep = " "; + prevMoreIndented = false; + } + } + switch (header.chomp) { + case "-": + break; + case "+": + for (let i = chompStart; i < lines.length; ++i) + value += "\n" + lines[i][0].slice(trimIndent); + if (value[value.length - 1] !== "\n") + value += "\n"; + break; + default: + value += "\n"; + } + const end = start + header.length + scalar.source.length; + return { value, type, comment: header.comment, range: [start, end, end] }; + } + function parseBlockScalarHeader({ offset, props }, strict, onError) { + if (props[0].type !== "block-scalar-header") { + onError(props[0], "IMPOSSIBLE", "Block scalar header not found"); + return null; + } + const { source } = props[0]; + const mode = source[0]; + let indent = 0; + let chomp = ""; + let error = -1; + for (let i = 1; i < source.length; ++i) { + const ch = source[i]; + if (!chomp && (ch === "-" || ch === "+")) + chomp = ch; + else { + const n = Number(ch); + if (!indent && n) + indent = n; + else if (error === -1) + error = offset + i; + } + } + if (error !== -1) + onError(error, "UNEXPECTED_TOKEN", `Block scalar header includes extra characters: ${source}`); + let hasSpace = false; + let comment = ""; + let length = source.length; + for (let i = 1; i < props.length; ++i) { + const token = props[i]; + switch (token.type) { + case "space": + hasSpace = true; + // fallthrough + case "newline": + length += token.source.length; + break; + case "comment": + if (strict && !hasSpace) { + const message = "Comments must be separated from other tokens by white space characters"; + onError(token, "MISSING_CHAR", message); + } + length += token.source.length; + comment = token.source.substring(1); + break; + case "error": + onError(token, "UNEXPECTED_TOKEN", token.message); + length += token.source.length; + break; + /* istanbul ignore next should not happen */ + default: { + const message = `Unexpected token in block scalar header: ${token.type}`; + onError(token, "UNEXPECTED_TOKEN", message); + const ts = token.source; + if (ts && typeof ts === "string") + length += ts.length; + } + } + } + return { mode, indent, chomp, comment, length }; + } + function splitLines(source) { + const split = source.split(/\n( *)/); + const first = split[0]; + const m = first.match(/^( *)/); + const line0 = m?.[1] ? [m[1], first.slice(m[1].length)] : ["", first]; + const lines = [line0]; + for (let i = 1; i < split.length; i += 2) + lines.push([split[i], split[i + 1]]); + return lines; + } + exports.resolveBlockScalar = resolveBlockScalar; + } +}); + +// node_modules/yaml/dist/compose/resolve-flow-scalar.js +var require_resolve_flow_scalar = __commonJS({ + "node_modules/yaml/dist/compose/resolve-flow-scalar.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var resolveEnd = require_resolve_end(); + function resolveFlowScalar(scalar, strict, onError) { + const { offset, type, source, end } = scalar; + let _type; + let value; + const _onError = (rel, code, msg) => onError(offset + rel, code, msg); + switch (type) { + case "scalar": + _type = Scalar.Scalar.PLAIN; + value = plainValue(source, _onError); + break; + case "single-quoted-scalar": + _type = Scalar.Scalar.QUOTE_SINGLE; + value = singleQuotedValue(source, _onError); + break; + case "double-quoted-scalar": + _type = Scalar.Scalar.QUOTE_DOUBLE; + value = doubleQuotedValue(source, _onError); + break; + /* istanbul ignore next should not happen */ + default: + onError(scalar, "UNEXPECTED_TOKEN", `Expected a flow scalar value, but found: ${type}`); + return { + value: "", + type: null, + comment: "", + range: [offset, offset + source.length, offset + source.length] + }; + } + const valueEnd = offset + source.length; + const re = resolveEnd.resolveEnd(end, valueEnd, strict, onError); + return { + value, + type: _type, + comment: re.comment, + range: [offset, valueEnd, re.offset] + }; + } + function plainValue(source, onError) { + let badChar = ""; + switch (source[0]) { + /* istanbul ignore next should not happen */ + case " ": + badChar = "a tab character"; + break; + case ",": + badChar = "flow indicator character ,"; + break; + case "%": + badChar = "directive indicator character %"; + break; + case "|": + case ">": { + badChar = `block scalar indicator ${source[0]}`; + break; + } + case "@": + case "`": { + badChar = `reserved character ${source[0]}`; + break; + } + } + if (badChar) + onError(0, "BAD_SCALAR_START", `Plain value cannot start with ${badChar}`); + return foldLines(source); + } + function singleQuotedValue(source, onError) { + if (source[source.length - 1] !== "'" || source.length === 1) + onError(source.length, "MISSING_CHAR", "Missing closing 'quote"); + return foldLines(source.slice(1, -1)).replace(/''/g, "'"); + } + function foldLines(source) { + let first, line; + try { + first = new RegExp("(.*?)(? wsStart ? source.slice(wsStart, i + 1) : ch; + } else { + res += ch; + } + } + if (source[source.length - 1] !== '"' || source.length === 1) + onError(source.length, "MISSING_CHAR", 'Missing closing "quote'); + return res; + } + function foldNewline(source, offset) { + let fold = ""; + let ch = source[offset + 1]; + while (ch === " " || ch === " " || ch === "\n" || ch === "\r") { + if (ch === "\r" && source[offset + 2] !== "\n") + break; + if (ch === "\n") + fold += "\n"; + offset += 1; + ch = source[offset + 1]; + } + if (!fold) + fold = " "; + return { fold, offset }; + } + var escapeCodes = { + "0": "\0", + // null character + a: "\x07", + // bell character + b: "\b", + // backspace + e: "\x1B", + // escape character + f: "\f", + // form feed + n: "\n", + // line feed + r: "\r", + // carriage return + t: " ", + // horizontal tab + v: "\v", + // vertical tab + N: "…", + // Unicode next line + _: " ", + // Unicode non-breaking space + L: "\u2028", + // Unicode line separator + P: "\u2029", + // Unicode paragraph separator + " ": " ", + '"': '"', + "/": "/", + "\\": "\\", + " ": " " + }; + function parseCharCode(source, offset, length, onError) { + const cc = source.substr(offset, length); + const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc); + const code = ok ? parseInt(cc, 16) : NaN; + try { + return String.fromCodePoint(code); + } catch { + const raw = source.substr(offset - 2, length + 2); + onError(offset - 2, "BAD_DQ_ESCAPE", `Invalid escape sequence ${raw}`); + return raw; + } + } + exports.resolveFlowScalar = resolveFlowScalar; + } +}); + +// node_modules/yaml/dist/compose/compose-scalar.js +var require_compose_scalar = __commonJS({ + "node_modules/yaml/dist/compose/compose-scalar.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var resolveBlockScalar = require_resolve_block_scalar(); + var resolveFlowScalar = require_resolve_flow_scalar(); + function composeScalar(ctx, token, tagToken, onError) { + const { value, type, comment, range } = token.type === "block-scalar" ? resolveBlockScalar.resolveBlockScalar(ctx, token, onError) : resolveFlowScalar.resolveFlowScalar(token, ctx.options.strict, onError); + const tagName = tagToken ? ctx.directives.tagName(tagToken.source, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg)) : null; + let tag; + if (ctx.options.stringKeys && ctx.atKey) { + tag = ctx.schema[identity.SCALAR]; + } else if (tagName) + tag = findScalarTagByName(ctx.schema, value, tagName, tagToken, onError); + else if (token.type === "scalar") + tag = findScalarTagByTest(ctx, value, token, onError); + else + tag = ctx.schema[identity.SCALAR]; + let scalar; + try { + const res = tag.resolve(value, (msg) => onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg), ctx.options); + scalar = identity.isScalar(res) ? res : new Scalar.Scalar(res); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg); + scalar = new Scalar.Scalar(value); + } + scalar.range = range; + scalar.source = value; + if (type) + scalar.type = type; + if (tagName) + scalar.tag = tagName; + if (tag.format) + scalar.format = tag.format; + if (comment) + scalar.comment = comment; + return scalar; + } + function findScalarTagByName(schema, value, tagName, tagToken, onError) { + if (tagName === "!") + return schema[identity.SCALAR]; + const matchWithTest = []; + for (const tag of schema.tags) { + if (!tag.collection && tag.tag === tagName) { + if (tag.default && tag.test) + matchWithTest.push(tag); + else + return tag; + } + } + for (const tag of matchWithTest) + if (tag.test?.test(value)) + return tag; + const kt = schema.knownTags[tagName]; + if (kt && !kt.collection) { + schema.tags.push(Object.assign({}, kt, { default: false, test: void 0 })); + return kt; + } + onError(tagToken, "TAG_RESOLVE_FAILED", `Unresolved tag: ${tagName}`, tagName !== "tag:yaml.org,2002:str"); + return schema[identity.SCALAR]; + } + function findScalarTagByTest({ atKey, directives, schema }, value, token, onError) { + const tag = schema.tags.find((tag2) => (tag2.default === true || atKey && tag2.default === "key") && tag2.test?.test(value)) || schema[identity.SCALAR]; + if (schema.compat) { + const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR]; + if (tag.tag !== compat.tag) { + const ts = directives.tagString(tag.tag); + const cs = directives.tagString(compat.tag); + const msg = `Value may be parsed as either ${ts} or ${cs}`; + onError(token, "TAG_RESOLVE_FAILED", msg, true); + } + } + return tag; + } + exports.composeScalar = composeScalar; + } +}); + +// node_modules/yaml/dist/compose/util-empty-scalar-position.js +var require_util_empty_scalar_position = __commonJS({ + "node_modules/yaml/dist/compose/util-empty-scalar-position.js"(exports) { + "use strict"; + function emptyScalarPosition(offset, before, pos) { + if (before) { + pos ?? (pos = before.length); + for (let i = pos - 1; i >= 0; --i) { + let st = before[i]; + switch (st.type) { + case "space": + case "comment": + case "newline": + offset -= st.source.length; + continue; + } + st = before[++i]; + while (st?.type === "space") { + offset += st.source.length; + st = before[++i]; + } + break; + } + } + return offset; + } + exports.emptyScalarPosition = emptyScalarPosition; + } +}); + +// node_modules/yaml/dist/compose/compose-node.js +var require_compose_node = __commonJS({ + "node_modules/yaml/dist/compose/compose-node.js"(exports) { + "use strict"; + var Alias = require_Alias(); + var identity = require_identity(); + var composeCollection = require_compose_collection(); + var composeScalar = require_compose_scalar(); + var resolveEnd = require_resolve_end(); + var utilEmptyScalarPosition = require_util_empty_scalar_position(); + var CN = { composeNode, composeEmptyNode }; + function composeNode(ctx, token, props, onError) { + const atKey = ctx.atKey; + const { spaceBefore, comment, anchor, tag } = props; + let node; + let isSrcToken = true; + switch (token.type) { + case "alias": + node = composeAlias(ctx, token, onError); + if (anchor || tag) + onError(token, "ALIAS_PROPS", "An alias node must not specify any properties"); + break; + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + case "block-scalar": + node = composeScalar.composeScalar(ctx, token, tag, onError); + if (anchor) + node.anchor = anchor.source.substring(1); + break; + case "block-map": + case "block-seq": + case "flow-collection": + try { + node = composeCollection.composeCollection(CN, ctx, token, props, onError); + if (anchor) + node.anchor = anchor.source.substring(1); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + onError(token, "RESOURCE_EXHAUSTION", message); + } + break; + default: { + const message = token.type === "error" ? token.message : `Unsupported token (type: ${token.type})`; + onError(token, "UNEXPECTED_TOKEN", message); + isSrcToken = false; + } + } + node ?? (node = composeEmptyNode(ctx, token.offset, void 0, null, props, onError)); + if (anchor && node.anchor === "") + onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string"); + if (atKey && ctx.options.stringKeys && (!identity.isScalar(node) || typeof node.value !== "string" || node.tag && node.tag !== "tag:yaml.org,2002:str")) { + const msg = "With stringKeys, all keys must be strings"; + onError(tag ?? token, "NON_STRING_KEY", msg); + } + if (spaceBefore) + node.spaceBefore = true; + if (comment) { + if (token.type === "scalar" && token.source === "") + node.comment = comment; + else + node.commentBefore = comment; + } + if (ctx.options.keepSourceTokens && isSrcToken) + node.srcToken = token; + return node; + } + function composeEmptyNode(ctx, offset, before, pos, { spaceBefore, comment, anchor, tag, end }, onError) { + const token = { + type: "scalar", + offset: utilEmptyScalarPosition.emptyScalarPosition(offset, before, pos), + indent: -1, + source: "" + }; + const node = composeScalar.composeScalar(ctx, token, tag, onError); + if (anchor) { + node.anchor = anchor.source.substring(1); + if (node.anchor === "") + onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string"); + } + if (spaceBefore) + node.spaceBefore = true; + if (comment) { + node.comment = comment; + node.range[2] = end; + } + return node; + } + function composeAlias({ options }, { offset, source, end }, onError) { + const alias = new Alias.Alias(source.substring(1)); + if (alias.source === "") + onError(offset, "BAD_ALIAS", "Alias cannot be an empty string"); + if (alias.source.endsWith(":")) + onError(offset + source.length - 1, "BAD_ALIAS", "Alias ending in : is ambiguous", true); + const valueEnd = offset + source.length; + const re = resolveEnd.resolveEnd(end, valueEnd, options.strict, onError); + alias.range = [offset, valueEnd, re.offset]; + if (re.comment) + alias.comment = re.comment; + return alias; + } + exports.composeEmptyNode = composeEmptyNode; + exports.composeNode = composeNode; + } +}); + +// node_modules/yaml/dist/compose/compose-doc.js +var require_compose_doc = __commonJS({ + "node_modules/yaml/dist/compose/compose-doc.js"(exports) { + "use strict"; + var Document = require_Document(); + var composeNode = require_compose_node(); + var resolveEnd = require_resolve_end(); + var resolveProps = require_resolve_props(); + function composeDoc(options, directives, { offset, start, value, end }, onError) { + const opts = Object.assign({ _directives: directives }, options); + const doc = new Document.Document(void 0, opts); + const ctx = { + atKey: false, + atRoot: true, + directives: doc.directives, + options: doc.options, + schema: doc.schema + }; + const props = resolveProps.resolveProps(start, { + indicator: "doc-start", + next: value ?? end?.[0], + offset, + onError, + parentIndent: 0, + startOnNewline: true + }); + if (props.found) { + doc.directives.docStart = true; + if (value && (value.type === "block-map" || value.type === "block-seq") && !props.hasNewline) + onError(props.end, "MISSING_CHAR", "Block collection cannot start on same line with directives-end marker"); + } + doc.contents = value ? composeNode.composeNode(ctx, value, props, onError) : composeNode.composeEmptyNode(ctx, props.end, start, null, props, onError); + const contentEnd = doc.contents.range[2]; + const re = resolveEnd.resolveEnd(end, contentEnd, false, onError); + if (re.comment) + doc.comment = re.comment; + doc.range = [offset, contentEnd, re.offset]; + return doc; + } + exports.composeDoc = composeDoc; + } +}); + +// node_modules/yaml/dist/compose/composer.js +var require_composer = __commonJS({ + "node_modules/yaml/dist/compose/composer.js"(exports) { + "use strict"; + var node_process = __require("process"); + var directives = require_directives(); + var Document = require_Document(); + var errors = require_errors(); + var identity = require_identity(); + var composeDoc = require_compose_doc(); + var resolveEnd = require_resolve_end(); + function getErrorPos(src) { + if (typeof src === "number") + return [src, src + 1]; + if (Array.isArray(src)) + return src.length === 2 ? src : [src[0], src[1]]; + const { offset, source } = src; + return [offset, offset + (typeof source === "string" ? source.length : 1)]; + } + function parsePrelude(prelude) { + let comment = ""; + let atComment = false; + let afterEmptyLine = false; + for (let i = 0; i < prelude.length; ++i) { + const source = prelude[i]; + switch (source[0]) { + case "#": + comment += (comment === "" ? "" : afterEmptyLine ? "\n\n" : "\n") + (source.substring(1) || " "); + atComment = true; + afterEmptyLine = false; + break; + case "%": + if (prelude[i + 1]?.[0] !== "#") + i += 1; + atComment = false; + break; + default: + if (!atComment) + afterEmptyLine = true; + atComment = false; + } + } + return { comment, afterEmptyLine }; + } + var Composer = class { + constructor(options = {}) { + this.doc = null; + this.atDirectives = false; + this.prelude = []; + this.errors = []; + this.warnings = []; + this.onError = (source, code, message, warning) => { + const pos = getErrorPos(source); + if (warning) + this.warnings.push(new errors.YAMLWarning(pos, code, message)); + else + this.errors.push(new errors.YAMLParseError(pos, code, message)); + }; + this.directives = new directives.Directives({ version: options.version || "1.2" }); + this.options = options; + } + decorate(doc, afterDoc) { + const { comment, afterEmptyLine } = parsePrelude(this.prelude); + if (comment) { + const dc = doc.contents; + if (afterDoc) { + doc.comment = doc.comment ? `${doc.comment} +${comment}` : comment; + } else if (afterEmptyLine || doc.directives.docStart || !dc) { + doc.commentBefore = comment; + } else if (identity.isCollection(dc) && !dc.flow && dc.items.length > 0) { + let it = dc.items[0]; + if (identity.isPair(it)) + it = it.key; + const cb = it.commentBefore; + it.commentBefore = cb ? `${comment} +${cb}` : comment; + } else { + const cb = dc.commentBefore; + dc.commentBefore = cb ? `${comment} +${cb}` : comment; + } + } + if (afterDoc) { + Array.prototype.push.apply(doc.errors, this.errors); + Array.prototype.push.apply(doc.warnings, this.warnings); + } else { + doc.errors = this.errors; + doc.warnings = this.warnings; + } + this.prelude = []; + this.errors = []; + this.warnings = []; + } + /** + * Current stream status information. + * + * Mostly useful at the end of input for an empty stream. + */ + streamInfo() { + return { + comment: parsePrelude(this.prelude).comment, + directives: this.directives, + errors: this.errors, + warnings: this.warnings + }; + } + /** + * Compose tokens into documents. + * + * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document. + * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly. + */ + *compose(tokens, forceDoc = false, endOffset = -1) { + for (const token of tokens) + yield* this.next(token); + yield* this.end(forceDoc, endOffset); + } + /** Advance the composer by one CST token. */ + *next(token) { + if (node_process.env.LOG_STREAM) + console.dir(token, { depth: null }); + switch (token.type) { + case "directive": + this.directives.add(token.source, (offset, message, warning) => { + const pos = getErrorPos(token); + pos[0] += offset; + this.onError(pos, "BAD_DIRECTIVE", message, warning); + }); + this.prelude.push(token.source); + this.atDirectives = true; + break; + case "document": { + const doc = composeDoc.composeDoc(this.options, this.directives, token, this.onError); + if (this.atDirectives && !doc.directives.docStart) + this.onError(token, "MISSING_CHAR", "Missing directives-end/doc-start indicator line"); + this.decorate(doc, false); + if (this.doc) + yield this.doc; + this.doc = doc; + this.atDirectives = false; + break; + } + case "byte-order-mark": + case "space": + break; + case "comment": + case "newline": + this.prelude.push(token.source); + break; + case "error": { + const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message; + const error = new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg); + if (this.atDirectives || !this.doc) + this.errors.push(error); + else + this.doc.errors.push(error); + break; + } + case "doc-end": { + if (!this.doc) { + const msg = "Unexpected doc-end without preceding document"; + this.errors.push(new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg)); + break; + } + this.doc.directives.docEnd = true; + const end = resolveEnd.resolveEnd(token.end, token.offset + token.source.length, this.doc.options.strict, this.onError); + this.decorate(this.doc, true); + if (end.comment) { + const dc = this.doc.comment; + this.doc.comment = dc ? `${dc} +${end.comment}` : end.comment; + } + this.doc.range[2] = end.offset; + break; + } + default: + this.errors.push(new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`)); + } + } + /** + * Call at end of input to yield any remaining document. + * + * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document. + * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly. + */ + *end(forceDoc = false, endOffset = -1) { + if (this.doc) { + this.decorate(this.doc, true); + yield this.doc; + this.doc = null; + } else if (forceDoc) { + const opts = Object.assign({ _directives: this.directives }, this.options); + const doc = new Document.Document(void 0, opts); + if (this.atDirectives) + this.onError(endOffset, "MISSING_CHAR", "Missing directives-end indicator line"); + doc.range = [0, endOffset, endOffset]; + this.decorate(doc, false); + yield doc; + } + } + }; + exports.Composer = Composer; + } +}); + +// node_modules/yaml/dist/parse/cst-scalar.js +var require_cst_scalar = __commonJS({ + "node_modules/yaml/dist/parse/cst-scalar.js"(exports) { + "use strict"; + var resolveBlockScalar = require_resolve_block_scalar(); + var resolveFlowScalar = require_resolve_flow_scalar(); + var errors = require_errors(); + var stringifyString = require_stringifyString(); + function resolveAsScalar(token, strict = true, onError) { + if (token) { + const _onError = (pos, code, message) => { + const offset = typeof pos === "number" ? pos : Array.isArray(pos) ? pos[0] : pos.offset; + if (onError) + onError(offset, code, message); + else + throw new errors.YAMLParseError([offset, offset + 1], code, message); + }; + switch (token.type) { + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return resolveFlowScalar.resolveFlowScalar(token, strict, _onError); + case "block-scalar": + return resolveBlockScalar.resolveBlockScalar({ options: { strict } }, token, _onError); + } + } + return null; + } + function createScalarToken(value, context) { + const { implicitKey = false, indent, inFlow = false, offset = -1, type = "PLAIN" } = context; + const source = stringifyString.stringifyString({ type, value }, { + implicitKey, + indent: indent > 0 ? " ".repeat(indent) : "", + inFlow, + options: { blockQuote: true, lineWidth: -1 } + }); + const end = context.end ?? [ + { type: "newline", offset: -1, indent, source: "\n" } + ]; + switch (source[0]) { + case "|": + case ">": { + const he = source.indexOf("\n"); + const head = source.substring(0, he); + const body = source.substring(he + 1) + "\n"; + const props = [ + { type: "block-scalar-header", offset, indent, source: head } + ]; + if (!addEndtoBlockProps(props, end)) + props.push({ type: "newline", offset: -1, indent, source: "\n" }); + return { type: "block-scalar", offset, indent, props, source: body }; + } + case '"': + return { type: "double-quoted-scalar", offset, indent, source, end }; + case "'": + return { type: "single-quoted-scalar", offset, indent, source, end }; + default: + return { type: "scalar", offset, indent, source, end }; + } + } + function setScalarValue(token, value, context = {}) { + let { afterKey = false, implicitKey = false, inFlow = false, type } = context; + let indent = "indent" in token ? token.indent : null; + if (afterKey && typeof indent === "number") + indent += 2; + if (!type) + switch (token.type) { + case "single-quoted-scalar": + type = "QUOTE_SINGLE"; + break; + case "double-quoted-scalar": + type = "QUOTE_DOUBLE"; + break; + case "block-scalar": { + const header = token.props[0]; + if (header.type !== "block-scalar-header") + throw new Error("Invalid block scalar header"); + type = header.source[0] === ">" ? "BLOCK_FOLDED" : "BLOCK_LITERAL"; + break; + } + default: + type = "PLAIN"; + } + const source = stringifyString.stringifyString({ type, value }, { + implicitKey: implicitKey || indent === null, + indent: indent !== null && indent > 0 ? " ".repeat(indent) : "", + inFlow, + options: { blockQuote: true, lineWidth: -1 } + }); + switch (source[0]) { + case "|": + case ">": + setBlockScalarValue(token, source); + break; + case '"': + setFlowScalarValue(token, source, "double-quoted-scalar"); + break; + case "'": + setFlowScalarValue(token, source, "single-quoted-scalar"); + break; + default: + setFlowScalarValue(token, source, "scalar"); + } + } + function setBlockScalarValue(token, source) { + const he = source.indexOf("\n"); + const head = source.substring(0, he); + const body = source.substring(he + 1) + "\n"; + if (token.type === "block-scalar") { + const header = token.props[0]; + if (header.type !== "block-scalar-header") + throw new Error("Invalid block scalar header"); + header.source = head; + token.source = body; + } else { + const { offset } = token; + const indent = "indent" in token ? token.indent : -1; + const props = [ + { type: "block-scalar-header", offset, indent, source: head } + ]; + if (!addEndtoBlockProps(props, "end" in token ? token.end : void 0)) + props.push({ type: "newline", offset: -1, indent, source: "\n" }); + for (const key of Object.keys(token)) + if (key !== "type" && key !== "offset") + delete token[key]; + Object.assign(token, { type: "block-scalar", indent, props, source: body }); + } + } + function addEndtoBlockProps(props, end) { + if (end) + for (const st of end) + switch (st.type) { + case "space": + case "comment": + props.push(st); + break; + case "newline": + props.push(st); + return true; + } + return false; + } + function setFlowScalarValue(token, source, type) { + switch (token.type) { + case "scalar": + case "double-quoted-scalar": + case "single-quoted-scalar": + token.type = type; + token.source = source; + break; + case "block-scalar": { + const end = token.props.slice(1); + let oa = source.length; + if (token.props[0].type === "block-scalar-header") + oa -= token.props[0].source.length; + for (const tok of end) + tok.offset += oa; + delete token.props; + Object.assign(token, { type, source, end }); + break; + } + case "block-map": + case "block-seq": { + const offset = token.offset + source.length; + const nl = { type: "newline", offset, indent: token.indent, source: "\n" }; + delete token.items; + Object.assign(token, { type, source, end: [nl] }); + break; + } + default: { + const indent = "indent" in token ? token.indent : -1; + const end = "end" in token && Array.isArray(token.end) ? token.end.filter((st) => st.type === "space" || st.type === "comment" || st.type === "newline") : []; + for (const key of Object.keys(token)) + if (key !== "type" && key !== "offset") + delete token[key]; + Object.assign(token, { type, indent, source, end }); + } + } + } + exports.createScalarToken = createScalarToken; + exports.resolveAsScalar = resolveAsScalar; + exports.setScalarValue = setScalarValue; + } +}); + +// node_modules/yaml/dist/parse/cst-stringify.js +var require_cst_stringify = __commonJS({ + "node_modules/yaml/dist/parse/cst-stringify.js"(exports) { + "use strict"; + var stringify = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst); + function stringifyToken(token) { + switch (token.type) { + case "block-scalar": { + let res = ""; + for (const tok of token.props) + res += stringifyToken(tok); + return res + token.source; + } + case "block-map": + case "block-seq": { + let res = ""; + for (const item of token.items) + res += stringifyItem(item); + return res; + } + case "flow-collection": { + let res = token.start.source; + for (const item of token.items) + res += stringifyItem(item); + for (const st of token.end) + res += st.source; + return res; + } + case "document": { + let res = stringifyItem(token); + if (token.end) + for (const st of token.end) + res += st.source; + return res; + } + default: { + let res = token.source; + if ("end" in token && token.end) + for (const st of token.end) + res += st.source; + return res; + } + } + } + function stringifyItem({ start, key, sep, value }) { + let res = ""; + for (const st of start) + res += st.source; + if (key) + res += stringifyToken(key); + if (sep) + for (const st of sep) + res += st.source; + if (value) + res += stringifyToken(value); + return res; + } + exports.stringify = stringify; + } +}); + +// node_modules/yaml/dist/parse/cst-visit.js +var require_cst_visit = __commonJS({ + "node_modules/yaml/dist/parse/cst-visit.js"(exports) { + "use strict"; + var BREAK = Symbol("break visit"); + var SKIP = Symbol("skip children"); + var REMOVE = Symbol("remove item"); + function visit(cst, visitor) { + if ("type" in cst && cst.type === "document") + cst = { start: cst.start, value: cst.value }; + _visit(Object.freeze([]), cst, visitor); + } + visit.BREAK = BREAK; + visit.SKIP = SKIP; + visit.REMOVE = REMOVE; + visit.itemAtPath = (cst, path) => { + let item = cst; + for (const [field, index] of path) { + const tok = item?.[field]; + if (tok && "items" in tok) { + item = tok.items[index]; + } else + return void 0; + } + return item; + }; + visit.parentCollection = (cst, path) => { + const parent = visit.itemAtPath(cst, path.slice(0, -1)); + const field = path[path.length - 1][0]; + const coll = parent?.[field]; + if (coll && "items" in coll) + return coll; + throw new Error("Parent collection not found"); + }; + function _visit(path, item, visitor) { + let ctrl = visitor(item, path); + if (typeof ctrl === "symbol") + return ctrl; + for (const field of ["key", "value"]) { + const token = item[field]; + if (token && "items" in token) { + for (let i = 0; i < token.items.length; ++i) { + const ci = _visit(Object.freeze(path.concat([[field, i]])), token.items[i], visitor); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + token.items.splice(i, 1); + i -= 1; + } + } + if (typeof ctrl === "function" && field === "key") + ctrl = ctrl(item, path); + } + } + return typeof ctrl === "function" ? ctrl(item, path) : ctrl; + } + exports.visit = visit; + } +}); + +// node_modules/yaml/dist/parse/cst.js +var require_cst = __commonJS({ + "node_modules/yaml/dist/parse/cst.js"(exports) { + "use strict"; + var cstScalar = require_cst_scalar(); + var cstStringify = require_cst_stringify(); + var cstVisit = require_cst_visit(); + var BOM = "\uFEFF"; + var DOCUMENT = ""; + var FLOW_END = ""; + var SCALAR = ""; + var isCollection = (token) => !!token && "items" in token; + var isScalar = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar"); + function prettyToken(token) { + switch (token) { + case BOM: + return ""; + case DOCUMENT: + return ""; + case FLOW_END: + return ""; + case SCALAR: + return ""; + default: + return JSON.stringify(token); + } + } + function tokenType(source) { + switch (source) { + case BOM: + return "byte-order-mark"; + case DOCUMENT: + return "doc-mode"; + case FLOW_END: + return "flow-error-end"; + case SCALAR: + return "scalar"; + case "---": + return "doc-start"; + case "...": + return "doc-end"; + case "": + case "\n": + case "\r\n": + return "newline"; + case "-": + return "seq-item-ind"; + case "?": + return "explicit-key-ind"; + case ":": + return "map-value-ind"; + case "{": + return "flow-map-start"; + case "}": + return "flow-map-end"; + case "[": + return "flow-seq-start"; + case "]": + return "flow-seq-end"; + case ",": + return "comma"; + } + switch (source[0]) { + case " ": + case " ": + return "space"; + case "#": + return "comment"; + case "%": + return "directive-line"; + case "*": + return "alias"; + case "&": + return "anchor"; + case "!": + return "tag"; + case "'": + return "single-quoted-scalar"; + case '"': + return "double-quoted-scalar"; + case "|": + case ">": + return "block-scalar-header"; + } + return null; + } + exports.createScalarToken = cstScalar.createScalarToken; + exports.resolveAsScalar = cstScalar.resolveAsScalar; + exports.setScalarValue = cstScalar.setScalarValue; + exports.stringify = cstStringify.stringify; + exports.visit = cstVisit.visit; + exports.BOM = BOM; + exports.DOCUMENT = DOCUMENT; + exports.FLOW_END = FLOW_END; + exports.SCALAR = SCALAR; + exports.isCollection = isCollection; + exports.isScalar = isScalar; + exports.prettyToken = prettyToken; + exports.tokenType = tokenType; + } +}); + +// node_modules/yaml/dist/parse/lexer.js +var require_lexer = __commonJS({ + "node_modules/yaml/dist/parse/lexer.js"(exports) { + "use strict"; + var cst = require_cst(); + function isEmpty(ch) { + switch (ch) { + case void 0: + case " ": + case "\n": + case "\r": + case " ": + return true; + default: + return false; + } + } + var hexDigits = new Set("0123456789ABCDEFabcdef"); + var tagChars = new Set("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()"); + var flowIndicatorChars = new Set(",[]{}"); + var invalidAnchorChars = new Set(" ,[]{}\n\r "); + var isNotAnchorChar = (ch) => !ch || invalidAnchorChars.has(ch); + var Lexer = class { + constructor() { + this.atEnd = false; + this.blockScalarIndent = -1; + this.blockScalarKeep = false; + this.buffer = ""; + this.flowKey = false; + this.flowLevel = 0; + this.indentNext = 0; + this.indentValue = 0; + this.lineEndPos = null; + this.next = null; + this.pos = 0; + } + /** + * Generate YAML tokens from the `source` string. If `incomplete`, + * a part of the last line may be left as a buffer for the next call. + * + * @returns A generator of lexical tokens + */ + *lex(source, incomplete = false) { + if (source) { + if (typeof source !== "string") + throw TypeError("source is not a string"); + this.buffer = this.buffer ? this.buffer + source : source; + this.lineEndPos = null; + } + this.atEnd = !incomplete; + let next = this.next ?? "stream"; + while (next && (incomplete || this.hasChars(1))) + next = yield* this.parseNext(next); + } + atLineEnd() { + let i = this.pos; + let ch = this.buffer[i]; + while (ch === " " || ch === " ") + ch = this.buffer[++i]; + if (!ch || ch === "#" || ch === "\n") + return true; + if (ch === "\r") + return this.buffer[i + 1] === "\n"; + return false; + } + charAt(n) { + return this.buffer[this.pos + n]; + } + continueScalar(offset) { + let ch = this.buffer[offset]; + if (this.indentNext > 0) { + let indent = 0; + while (ch === " ") + ch = this.buffer[++indent + offset]; + if (ch === "\r") { + const next = this.buffer[indent + offset + 1]; + if (next === "\n" || !next && !this.atEnd) + return offset + indent + 1; + } + return ch === "\n" || indent >= this.indentNext || !ch && !this.atEnd ? offset + indent : -1; + } + if (ch === "-" || ch === ".") { + const dt = this.buffer.substr(offset, 3); + if ((dt === "---" || dt === "...") && isEmpty(this.buffer[offset + 3])) + return -1; + } + return offset; + } + getLine() { + let end = this.lineEndPos; + if (typeof end !== "number" || end !== -1 && end < this.pos) { + end = this.buffer.indexOf("\n", this.pos); + this.lineEndPos = end; + } + if (end === -1) + return this.atEnd ? this.buffer.substring(this.pos) : null; + if (this.buffer[end - 1] === "\r") + end -= 1; + return this.buffer.substring(this.pos, end); + } + hasChars(n) { + return this.pos + n <= this.buffer.length; + } + setNext(state) { + this.buffer = this.buffer.substring(this.pos); + this.pos = 0; + this.lineEndPos = null; + this.next = state; + return null; + } + peek(n) { + return this.buffer.substr(this.pos, n); + } + *parseNext(next) { + switch (next) { + case "stream": + return yield* this.parseStream(); + case "line-start": + return yield* this.parseLineStart(); + case "block-start": + return yield* this.parseBlockStart(); + case "doc": + return yield* this.parseDocument(); + case "flow": + return yield* this.parseFlowCollection(); + case "quoted-scalar": + return yield* this.parseQuotedScalar(); + case "block-scalar": + return yield* this.parseBlockScalar(); + case "plain-scalar": + return yield* this.parsePlainScalar(); + } + } + *parseStream() { + let line = this.getLine(); + if (line === null) + return this.setNext("stream"); + if (line[0] === cst.BOM) { + yield* this.pushCount(1); + line = line.substring(1); + } + if (line[0] === "%") { + let dirEnd = line.length; + let cs = line.indexOf("#"); + while (cs !== -1) { + const ch = line[cs - 1]; + if (ch === " " || ch === " ") { + dirEnd = cs - 1; + break; + } else { + cs = line.indexOf("#", cs + 1); + } + } + while (true) { + const ch = line[dirEnd - 1]; + if (ch === " " || ch === " ") + dirEnd -= 1; + else + break; + } + const n = (yield* this.pushCount(dirEnd)) + (yield* this.pushSpaces(true)); + yield* this.pushCount(line.length - n); + this.pushNewline(); + return "stream"; + } + if (this.atLineEnd()) { + const sp = yield* this.pushSpaces(true); + yield* this.pushCount(line.length - sp); + yield* this.pushNewline(); + return "stream"; + } + yield cst.DOCUMENT; + return yield* this.parseLineStart(); + } + *parseLineStart() { + const ch = this.charAt(0); + if (!ch && !this.atEnd) + return this.setNext("line-start"); + if (ch === "-" || ch === ".") { + if (!this.atEnd && !this.hasChars(4)) + return this.setNext("line-start"); + const s = this.peek(3); + if ((s === "---" || s === "...") && isEmpty(this.charAt(3))) { + yield* this.pushCount(3); + this.indentValue = 0; + this.indentNext = 0; + return s === "---" ? "doc" : "stream"; + } + } + this.indentValue = yield* this.pushSpaces(false); + if (this.indentNext > this.indentValue && !isEmpty(this.charAt(1))) + this.indentNext = this.indentValue; + return yield* this.parseBlockStart(); + } + *parseBlockStart() { + const [ch0, ch1] = this.peek(2); + if (!ch1 && !this.atEnd) + return this.setNext("block-start"); + if ((ch0 === "-" || ch0 === "?" || ch0 === ":") && isEmpty(ch1)) { + const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true)); + this.indentNext = this.indentValue + 1; + this.indentValue += n; + return yield* this.parseBlockStart(); + } + return "doc"; + } + *parseDocument() { + yield* this.pushSpaces(true); + const line = this.getLine(); + if (line === null) + return this.setNext("doc"); + let n = yield* this.pushIndicators(); + switch (line[n]) { + case "#": + yield* this.pushCount(line.length - n); + // fallthrough + case void 0: + yield* this.pushNewline(); + return yield* this.parseLineStart(); + case "{": + case "[": + yield* this.pushCount(1); + this.flowKey = false; + this.flowLevel = 1; + return "flow"; + case "}": + case "]": + yield* this.pushCount(1); + return "doc"; + case "*": + yield* this.pushUntil(isNotAnchorChar); + return "doc"; + case '"': + case "'": + return yield* this.parseQuotedScalar(); + case "|": + case ">": + n += yield* this.parseBlockScalarHeader(); + n += yield* this.pushSpaces(true); + yield* this.pushCount(line.length - n); + yield* this.pushNewline(); + return yield* this.parseBlockScalar(); + default: + return yield* this.parsePlainScalar(); + } + } + *parseFlowCollection() { + let nl, sp; + let indent = -1; + do { + nl = yield* this.pushNewline(); + if (nl > 0) { + sp = yield* this.pushSpaces(false); + this.indentValue = indent = sp; + } else { + sp = 0; + } + sp += yield* this.pushSpaces(true); + } while (nl + sp > 0); + const line = this.getLine(); + if (line === null) + return this.setNext("flow"); + if (indent !== -1 && indent < this.indentNext && line[0] !== "#" || indent === 0 && (line.startsWith("---") || line.startsWith("...")) && isEmpty(line[3])) { + const atFlowEndMarker = indent === this.indentNext - 1 && this.flowLevel === 1 && (line[0] === "]" || line[0] === "}"); + if (!atFlowEndMarker) { + this.flowLevel = 0; + yield cst.FLOW_END; + return yield* this.parseLineStart(); + } + } + let n = 0; + while (line[n] === ",") { + n += yield* this.pushCount(1); + n += yield* this.pushSpaces(true); + this.flowKey = false; + } + n += yield* this.pushIndicators(); + switch (line[n]) { + case void 0: + return "flow"; + case "#": + yield* this.pushCount(line.length - n); + return "flow"; + case "{": + case "[": + yield* this.pushCount(1); + this.flowKey = false; + this.flowLevel += 1; + return "flow"; + case "}": + case "]": + yield* this.pushCount(1); + this.flowKey = true; + this.flowLevel -= 1; + return this.flowLevel ? "flow" : "doc"; + case "*": + yield* this.pushUntil(isNotAnchorChar); + return "flow"; + case '"': + case "'": + this.flowKey = true; + return yield* this.parseQuotedScalar(); + case ":": { + const next = this.charAt(1); + if (this.flowKey || isEmpty(next) || next === ",") { + this.flowKey = false; + yield* this.pushCount(1); + yield* this.pushSpaces(true); + return "flow"; + } + } + // fallthrough + default: + this.flowKey = false; + return yield* this.parsePlainScalar(); + } + } + *parseQuotedScalar() { + const quote = this.charAt(0); + let end = this.buffer.indexOf(quote, this.pos + 1); + if (quote === "'") { + while (end !== -1 && this.buffer[end + 1] === "'") + end = this.buffer.indexOf("'", end + 2); + } else { + while (end !== -1) { + let n = 0; + while (this.buffer[end - 1 - n] === "\\") + n += 1; + if (n % 2 === 0) + break; + end = this.buffer.indexOf('"', end + 1); + } + } + const qb = this.buffer.substring(0, end); + let nl = qb.indexOf("\n", this.pos); + if (nl !== -1) { + while (nl !== -1) { + const cs = this.continueScalar(nl + 1); + if (cs === -1) + break; + nl = qb.indexOf("\n", cs); + } + if (nl !== -1) { + end = nl - (qb[nl - 1] === "\r" ? 2 : 1); + } + } + if (end === -1) { + if (!this.atEnd) + return this.setNext("quoted-scalar"); + end = this.buffer.length; + } + yield* this.pushToIndex(end + 1, false); + return this.flowLevel ? "flow" : "doc"; + } + *parseBlockScalarHeader() { + this.blockScalarIndent = -1; + this.blockScalarKeep = false; + let i = this.pos; + while (true) { + const ch = this.buffer[++i]; + if (ch === "+") + this.blockScalarKeep = true; + else if (ch > "0" && ch <= "9") + this.blockScalarIndent = Number(ch) - 1; + else if (ch !== "-") + break; + } + return yield* this.pushUntil((ch) => isEmpty(ch) || ch === "#"); + } + *parseBlockScalar() { + let nl = this.pos - 1; + let indent = 0; + let ch; + loop: for (let i2 = this.pos; ch = this.buffer[i2]; ++i2) { + switch (ch) { + case " ": + indent += 1; + break; + case "\n": + nl = i2; + indent = 0; + break; + case "\r": { + const next = this.buffer[i2 + 1]; + if (!next && !this.atEnd) + return this.setNext("block-scalar"); + if (next === "\n") + break; + } + // fallthrough + default: + break loop; + } + } + if (!ch && !this.atEnd) + return this.setNext("block-scalar"); + if (indent >= this.indentNext) { + if (this.blockScalarIndent === -1) + this.indentNext = indent; + else { + this.indentNext = this.blockScalarIndent + (this.indentNext === 0 ? 1 : this.indentNext); + } + do { + const cs = this.continueScalar(nl + 1); + if (cs === -1) + break; + nl = this.buffer.indexOf("\n", cs); + } while (nl !== -1); + if (nl === -1) { + if (!this.atEnd) + return this.setNext("block-scalar"); + nl = this.buffer.length; + } + } + let i = nl + 1; + ch = this.buffer[i]; + while (ch === " ") + ch = this.buffer[++i]; + if (ch === " ") { + while (ch === " " || ch === " " || ch === "\r" || ch === "\n") + ch = this.buffer[++i]; + nl = i - 1; + } else if (!this.blockScalarKeep) { + do { + let i2 = nl - 1; + let ch2 = this.buffer[i2]; + if (ch2 === "\r") + ch2 = this.buffer[--i2]; + const lastChar = i2; + while (ch2 === " ") + ch2 = this.buffer[--i2]; + if (ch2 === "\n" && i2 >= this.pos && i2 + 1 + indent > lastChar) + nl = i2; + else + break; + } while (true); + } + yield cst.SCALAR; + yield* this.pushToIndex(nl + 1, true); + return yield* this.parseLineStart(); + } + *parsePlainScalar() { + const inFlow = this.flowLevel > 0; + let end = this.pos - 1; + let i = this.pos - 1; + let ch; + while (ch = this.buffer[++i]) { + if (ch === ":") { + const next = this.buffer[i + 1]; + if (isEmpty(next) || inFlow && flowIndicatorChars.has(next)) + break; + end = i; + } else if (isEmpty(ch)) { + let next = this.buffer[i + 1]; + if (ch === "\r") { + if (next === "\n") { + i += 1; + ch = "\n"; + next = this.buffer[i + 1]; + } else + end = i; + } + if (next === "#" || inFlow && flowIndicatorChars.has(next)) + break; + if (ch === "\n") { + const cs = this.continueScalar(i + 1); + if (cs === -1) + break; + i = Math.max(i, cs - 2); + } + } else { + if (inFlow && flowIndicatorChars.has(ch)) + break; + end = i; + } + } + if (!ch && !this.atEnd) + return this.setNext("plain-scalar"); + yield cst.SCALAR; + yield* this.pushToIndex(end + 1, true); + return inFlow ? "flow" : "doc"; + } + *pushCount(n) { + if (n > 0) { + yield this.buffer.substr(this.pos, n); + this.pos += n; + return n; + } + return 0; + } + *pushToIndex(i, allowEmpty) { + const s = this.buffer.slice(this.pos, i); + if (s) { + yield s; + this.pos += s.length; + return s.length; + } else if (allowEmpty) + yield ""; + return 0; + } + *pushIndicators() { + switch (this.charAt(0)) { + case "!": + return (yield* this.pushTag()) + (yield* this.pushSpaces(true)) + (yield* this.pushIndicators()); + case "&": + return (yield* this.pushUntil(isNotAnchorChar)) + (yield* this.pushSpaces(true)) + (yield* this.pushIndicators()); + case "-": + // this is an error + case "?": + // this is an error outside flow collections + case ":": { + const inFlow = this.flowLevel > 0; + const ch1 = this.charAt(1); + if (isEmpty(ch1) || inFlow && flowIndicatorChars.has(ch1)) { + if (!inFlow) + this.indentNext = this.indentValue + 1; + else if (this.flowKey) + this.flowKey = false; + return (yield* this.pushCount(1)) + (yield* this.pushSpaces(true)) + (yield* this.pushIndicators()); + } + } + } + return 0; + } + *pushTag() { + if (this.charAt(1) === "<") { + let i = this.pos + 2; + let ch = this.buffer[i]; + while (!isEmpty(ch) && ch !== ">") + ch = this.buffer[++i]; + return yield* this.pushToIndex(ch === ">" ? i + 1 : i, false); + } else { + let i = this.pos + 1; + let ch = this.buffer[i]; + while (ch) { + if (tagChars.has(ch)) + ch = this.buffer[++i]; + else if (ch === "%" && hexDigits.has(this.buffer[i + 1]) && hexDigits.has(this.buffer[i + 2])) { + ch = this.buffer[i += 3]; + } else + break; + } + return yield* this.pushToIndex(i, false); + } + } + *pushNewline() { + const ch = this.buffer[this.pos]; + if (ch === "\n") + return yield* this.pushCount(1); + else if (ch === "\r" && this.charAt(1) === "\n") + return yield* this.pushCount(2); + else + return 0; + } + *pushSpaces(allowTabs) { + let i = this.pos - 1; + let ch; + do { + ch = this.buffer[++i]; + } while (ch === " " || allowTabs && ch === " "); + const n = i - this.pos; + if (n > 0) { + yield this.buffer.substr(this.pos, n); + this.pos = i; + } + return n; + } + *pushUntil(test) { + let i = this.pos; + let ch = this.buffer[i]; + while (!test(ch)) + ch = this.buffer[++i]; + return yield* this.pushToIndex(i, false); + } + }; + exports.Lexer = Lexer; + } +}); + +// node_modules/yaml/dist/parse/line-counter.js +var require_line_counter = __commonJS({ + "node_modules/yaml/dist/parse/line-counter.js"(exports) { + "use strict"; + var LineCounter = class { + constructor() { + this.lineStarts = []; + this.addNewLine = (offset) => this.lineStarts.push(offset); + this.linePos = (offset) => { + let low = 0; + let high = this.lineStarts.length; + while (low < high) { + const mid = low + high >> 1; + if (this.lineStarts[mid] < offset) + low = mid + 1; + else + high = mid; + } + if (this.lineStarts[low] === offset) + return { line: low + 1, col: 1 }; + if (low === 0) + return { line: 0, col: offset }; + const start = this.lineStarts[low - 1]; + return { line: low, col: offset - start + 1 }; + }; + } + }; + exports.LineCounter = LineCounter; + } +}); + +// node_modules/yaml/dist/parse/parser.js +var require_parser = __commonJS({ + "node_modules/yaml/dist/parse/parser.js"(exports) { + "use strict"; + var node_process = __require("process"); + var cst = require_cst(); + var lexer = require_lexer(); + function includesToken(list, type) { + for (let i = 0; i < list.length; ++i) + if (list[i].type === type) + return true; + return false; + } + function findNonEmptyIndex(list) { + for (let i = 0; i < list.length; ++i) { + switch (list[i].type) { + case "space": + case "comment": + case "newline": + break; + default: + return i; + } + } + return -1; + } + function isFlowToken(token) { + switch (token?.type) { + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + case "flow-collection": + return true; + default: + return false; + } + } + function getPrevProps(parent) { + switch (parent.type) { + case "document": + return parent.start; + case "block-map": { + const it = parent.items[parent.items.length - 1]; + return it.sep ?? it.start; + } + case "block-seq": + return parent.items[parent.items.length - 1].start; + /* istanbul ignore next should not happen */ + default: + return []; + } + } + function getFirstKeyStartProps(prev) { + if (prev.length === 0) + return []; + let i = prev.length; + loop: while (--i >= 0) { + switch (prev[i].type) { + case "doc-start": + case "explicit-key-ind": + case "map-value-ind": + case "seq-item-ind": + case "newline": + break loop; + } + } + while (prev[++i]?.type === "space") { + } + return prev.splice(i, prev.length); + } + function fixFlowSeqItems(fc) { + if (fc.start.type === "flow-seq-start") { + for (const it of fc.items) { + if (it.sep && !it.value && !includesToken(it.start, "explicit-key-ind") && !includesToken(it.sep, "map-value-ind")) { + if (it.key) + it.value = it.key; + delete it.key; + if (isFlowToken(it.value)) { + if (it.value.end) + Array.prototype.push.apply(it.value.end, it.sep); + else + it.value.end = it.sep; + } else + Array.prototype.push.apply(it.start, it.sep); + delete it.sep; + } + } + } + } + var Parser = class { + /** + * @param onNewLine - If defined, called separately with the start position of + * each new line (in `parse()`, including the start of input). + */ + constructor(onNewLine) { + this.atNewLine = true; + this.atScalar = false; + this.indent = 0; + this.offset = 0; + this.onKeyLine = false; + this.stack = []; + this.source = ""; + this.type = ""; + this.lexer = new lexer.Lexer(); + this.onNewLine = onNewLine; + } + /** + * Parse `source` as a YAML stream. + * If `incomplete`, a part of the last line may be left as a buffer for the next call. + * + * Errors are not thrown, but yielded as `{ type: 'error', message }` tokens. + * + * @returns A generator of tokens representing each directive, document, and other structure. + */ + *parse(source, incomplete = false) { + if (this.onNewLine && this.offset === 0) + this.onNewLine(0); + for (const lexeme of this.lexer.lex(source, incomplete)) + yield* this.next(lexeme); + if (!incomplete) + yield* this.end(); + } + /** + * Advance the parser by the `source` of one lexical token. + */ + *next(source) { + this.source = source; + if (node_process.env.LOG_TOKENS) + console.log("|", cst.prettyToken(source)); + if (this.atScalar) { + this.atScalar = false; + yield* this.step(); + this.offset += source.length; + return; + } + const type = cst.tokenType(source); + if (!type) { + const message = `Not a YAML token: ${source}`; + yield* this.pop({ type: "error", offset: this.offset, message, source }); + this.offset += source.length; + } else if (type === "scalar") { + this.atNewLine = false; + this.atScalar = true; + this.type = "scalar"; + } else { + this.type = type; + yield* this.step(); + switch (type) { + case "newline": + this.atNewLine = true; + this.indent = 0; + if (this.onNewLine) + this.onNewLine(this.offset + source.length); + break; + case "space": + if (this.atNewLine && source[0] === " ") + this.indent += source.length; + break; + case "explicit-key-ind": + case "map-value-ind": + case "seq-item-ind": + if (this.atNewLine) + this.indent += source.length; + break; + case "doc-mode": + case "flow-error-end": + return; + default: + this.atNewLine = false; + } + this.offset += source.length; + } + } + /** Call at end of input to push out any remaining constructions */ + *end() { + while (this.stack.length > 0) + yield* this.pop(); + } + get sourceToken() { + const st = { + type: this.type, + offset: this.offset, + indent: this.indent, + source: this.source + }; + return st; + } + *step() { + const top = this.peek(1); + if (this.type === "doc-end" && top?.type !== "doc-end") { + while (this.stack.length > 0) + yield* this.pop(); + this.stack.push({ + type: "doc-end", + offset: this.offset, + source: this.source + }); + return; + } + if (!top) + return yield* this.stream(); + switch (top.type) { + case "document": + return yield* this.document(top); + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return yield* this.scalar(top); + case "block-scalar": + return yield* this.blockScalar(top); + case "block-map": + return yield* this.blockMap(top); + case "block-seq": + return yield* this.blockSequence(top); + case "flow-collection": + return yield* this.flowCollection(top); + case "doc-end": + return yield* this.documentEnd(top); + } + yield* this.pop(); + } + peek(n) { + return this.stack[this.stack.length - n]; + } + *pop(error) { + const token = error ?? this.stack.pop(); + if (!token) { + const message = "Tried to pop an empty stack"; + yield { type: "error", offset: this.offset, source: "", message }; + } else if (this.stack.length === 0) { + yield token; + } else { + const top = this.peek(1); + if (token.type === "block-scalar") { + token.indent = "indent" in top ? top.indent : 0; + } else if (token.type === "flow-collection" && top.type === "document") { + token.indent = 0; + } + if (token.type === "flow-collection") + fixFlowSeqItems(token); + switch (top.type) { + case "document": + top.value = token; + break; + case "block-scalar": + top.props.push(token); + break; + case "block-map": { + const it = top.items[top.items.length - 1]; + if (it.value) { + top.items.push({ start: [], key: token, sep: [] }); + this.onKeyLine = true; + return; + } else if (it.sep) { + it.value = token; + } else { + Object.assign(it, { key: token, sep: [] }); + this.onKeyLine = !it.explicitKey; + return; + } + break; + } + case "block-seq": { + const it = top.items[top.items.length - 1]; + if (it.value) + top.items.push({ start: [], value: token }); + else + it.value = token; + break; + } + case "flow-collection": { + const it = top.items[top.items.length - 1]; + if (!it || it.value) + top.items.push({ start: [], key: token, sep: [] }); + else if (it.sep) + it.value = token; + else + Object.assign(it, { key: token, sep: [] }); + return; + } + /* istanbul ignore next should not happen */ + default: + yield* this.pop(); + yield* this.pop(token); + } + if ((top.type === "document" || top.type === "block-map" || top.type === "block-seq") && (token.type === "block-map" || token.type === "block-seq")) { + const last = token.items[token.items.length - 1]; + if (last && !last.sep && !last.value && last.start.length > 0 && findNonEmptyIndex(last.start) === -1 && (token.indent === 0 || last.start.every((st) => st.type !== "comment" || st.indent < token.indent))) { + if (top.type === "document") + top.end = last.start; + else + top.items.push({ start: last.start }); + token.items.splice(-1, 1); + } + } + } + } + *stream() { + switch (this.type) { + case "directive-line": + yield { type: "directive", offset: this.offset, source: this.source }; + return; + case "byte-order-mark": + case "space": + case "comment": + case "newline": + yield this.sourceToken; + return; + case "doc-mode": + case "doc-start": { + const doc = { + type: "document", + offset: this.offset, + start: [] + }; + if (this.type === "doc-start") + doc.start.push(this.sourceToken); + this.stack.push(doc); + return; + } + } + yield { + type: "error", + offset: this.offset, + message: `Unexpected ${this.type} token in YAML stream`, + source: this.source + }; + } + *document(doc) { + if (doc.value) + return yield* this.lineEnd(doc); + switch (this.type) { + case "doc-start": { + if (findNonEmptyIndex(doc.start) !== -1) { + yield* this.pop(); + yield* this.step(); + } else + doc.start.push(this.sourceToken); + return; + } + case "anchor": + case "tag": + case "space": + case "comment": + case "newline": + doc.start.push(this.sourceToken); + return; + } + const bv = this.startBlockValue(doc); + if (bv) + this.stack.push(bv); + else { + yield { + type: "error", + offset: this.offset, + message: `Unexpected ${this.type} token in YAML document`, + source: this.source + }; + } + } + *scalar(scalar) { + if (this.type === "map-value-ind") { + const prev = getPrevProps(this.peek(2)); + const start = getFirstKeyStartProps(prev); + let sep; + if (scalar.end) { + sep = scalar.end; + sep.push(this.sourceToken); + delete scalar.end; + } else + sep = [this.sourceToken]; + const map = { + type: "block-map", + offset: scalar.offset, + indent: scalar.indent, + items: [{ start, key: scalar, sep }] + }; + this.onKeyLine = true; + this.stack[this.stack.length - 1] = map; + } else + yield* this.lineEnd(scalar); + } + *blockScalar(scalar) { + switch (this.type) { + case "space": + case "comment": + case "newline": + scalar.props.push(this.sourceToken); + return; + case "scalar": + scalar.source = this.source; + this.atNewLine = true; + this.indent = 0; + if (this.onNewLine) { + let nl = this.source.indexOf("\n") + 1; + while (nl !== 0) { + this.onNewLine(this.offset + nl); + nl = this.source.indexOf("\n", nl) + 1; + } + } + yield* this.pop(); + break; + /* istanbul ignore next should not happen */ + default: + yield* this.pop(); + yield* this.step(); + } + } + *blockMap(map) { + const it = map.items[map.items.length - 1]; + switch (this.type) { + case "newline": + this.onKeyLine = false; + if (it.value) { + const end = "end" in it.value ? it.value.end : void 0; + const last = Array.isArray(end) ? end[end.length - 1] : void 0; + if (last?.type === "comment") + end?.push(this.sourceToken); + else + map.items.push({ start: [this.sourceToken] }); + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + it.start.push(this.sourceToken); + } + return; + case "space": + case "comment": + if (it.value) { + map.items.push({ start: [this.sourceToken] }); + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + if (this.atIndentedComment(it.start, map.indent)) { + const prev = map.items[map.items.length - 2]; + const end = prev?.value?.end; + if (Array.isArray(end)) { + Array.prototype.push.apply(end, it.start); + end.push(this.sourceToken); + map.items.pop(); + return; + } + } + it.start.push(this.sourceToken); + } + return; + } + if (this.indent >= map.indent) { + const atMapIndent = !this.onKeyLine && this.indent === map.indent; + const atNextItem = atMapIndent && (it.sep || it.explicitKey) && this.type !== "seq-item-ind"; + let start = []; + if (atNextItem && it.sep && !it.value) { + const nl = []; + for (let i = 0; i < it.sep.length; ++i) { + const st = it.sep[i]; + switch (st.type) { + case "newline": + nl.push(i); + break; + case "space": + break; + case "comment": + if (st.indent > map.indent) + nl.length = 0; + break; + default: + nl.length = 0; + } + } + if (nl.length >= 2) + start = it.sep.splice(nl[1]); + } + switch (this.type) { + case "anchor": + case "tag": + if (atNextItem || it.value) { + start.push(this.sourceToken); + map.items.push({ start }); + this.onKeyLine = true; + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + it.start.push(this.sourceToken); + } + return; + case "explicit-key-ind": + if (!it.sep && !it.explicitKey) { + it.start.push(this.sourceToken); + it.explicitKey = true; + } else if (atNextItem || it.value) { + start.push(this.sourceToken); + map.items.push({ start, explicitKey: true }); + } else { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: [this.sourceToken], explicitKey: true }] + }); + } + this.onKeyLine = true; + return; + case "map-value-ind": + if (it.explicitKey) { + if (!it.sep) { + if (includesToken(it.start, "newline")) { + Object.assign(it, { key: null, sep: [this.sourceToken] }); + } else { + const start2 = getFirstKeyStartProps(it.start); + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: start2, key: null, sep: [this.sourceToken] }] + }); + } + } else if (it.value) { + map.items.push({ start: [], key: null, sep: [this.sourceToken] }); + } else if (includesToken(it.sep, "map-value-ind")) { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, key: null, sep: [this.sourceToken] }] + }); + } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) { + const start2 = getFirstKeyStartProps(it.start); + const key = it.key; + const sep = it.sep; + sep.push(this.sourceToken); + delete it.key; + delete it.sep; + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: start2, key, sep }] + }); + } else if (start.length > 0) { + it.sep = it.sep.concat(start, this.sourceToken); + } else { + it.sep.push(this.sourceToken); + } + } else { + if (!it.sep) { + Object.assign(it, { key: null, sep: [this.sourceToken] }); + } else if (it.value || atNextItem) { + map.items.push({ start, key: null, sep: [this.sourceToken] }); + } else if (includesToken(it.sep, "map-value-ind")) { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: [], key: null, sep: [this.sourceToken] }] + }); + } else { + it.sep.push(this.sourceToken); + } + } + this.onKeyLine = true; + return; + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": { + const fs = this.flowScalar(this.type); + if (atNextItem || it.value) { + map.items.push({ start, key: fs, sep: [] }); + this.onKeyLine = true; + } else if (it.sep) { + this.stack.push(fs); + } else { + Object.assign(it, { key: fs, sep: [] }); + this.onKeyLine = true; + } + return; + } + default: { + const bv = this.startBlockValue(map); + if (bv) { + if (bv.type === "block-seq") { + if (!it.explicitKey && it.sep && !includesToken(it.sep, "newline")) { + yield* this.pop({ + type: "error", + offset: this.offset, + message: "Unexpected block-seq-ind on same line with key", + source: this.source + }); + return; + } + } else if (atMapIndent) { + map.items.push({ start }); + } + this.stack.push(bv); + return; + } + } + } + } + yield* this.pop(); + yield* this.step(); + } + *blockSequence(seq) { + const it = seq.items[seq.items.length - 1]; + switch (this.type) { + case "newline": + if (it.value) { + const end = "end" in it.value ? it.value.end : void 0; + const last = Array.isArray(end) ? end[end.length - 1] : void 0; + if (last?.type === "comment") + end?.push(this.sourceToken); + else + seq.items.push({ start: [this.sourceToken] }); + } else + it.start.push(this.sourceToken); + return; + case "space": + case "comment": + if (it.value) + seq.items.push({ start: [this.sourceToken] }); + else { + if (this.atIndentedComment(it.start, seq.indent)) { + const prev = seq.items[seq.items.length - 2]; + const end = prev?.value?.end; + if (Array.isArray(end)) { + Array.prototype.push.apply(end, it.start); + end.push(this.sourceToken); + seq.items.pop(); + return; + } + } + it.start.push(this.sourceToken); + } + return; + case "anchor": + case "tag": + if (it.value || this.indent <= seq.indent) + break; + it.start.push(this.sourceToken); + return; + case "seq-item-ind": + if (this.indent !== seq.indent) + break; + if (it.value || includesToken(it.start, "seq-item-ind")) + seq.items.push({ start: [this.sourceToken] }); + else + it.start.push(this.sourceToken); + return; + } + if (this.indent > seq.indent) { + const bv = this.startBlockValue(seq); + if (bv) { + this.stack.push(bv); + return; + } + } + yield* this.pop(); + yield* this.step(); + } + *flowCollection(fc) { + const it = fc.items[fc.items.length - 1]; + if (this.type === "flow-error-end") { + let top; + do { + yield* this.pop(); + top = this.peek(1); + } while (top?.type === "flow-collection"); + } else if (fc.end.length === 0) { + switch (this.type) { + case "comma": + case "explicit-key-ind": + if (!it || it.sep) + fc.items.push({ start: [this.sourceToken] }); + else + it.start.push(this.sourceToken); + return; + case "map-value-ind": + if (!it || it.value) + fc.items.push({ start: [], key: null, sep: [this.sourceToken] }); + else if (it.sep) + it.sep.push(this.sourceToken); + else + Object.assign(it, { key: null, sep: [this.sourceToken] }); + return; + case "space": + case "comment": + case "newline": + case "anchor": + case "tag": + if (!it || it.value) + fc.items.push({ start: [this.sourceToken] }); + else if (it.sep) + it.sep.push(this.sourceToken); + else + it.start.push(this.sourceToken); + return; + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": { + const fs = this.flowScalar(this.type); + if (!it || it.value) + fc.items.push({ start: [], key: fs, sep: [] }); + else if (it.sep) + this.stack.push(fs); + else + Object.assign(it, { key: fs, sep: [] }); + return; + } + case "flow-map-end": + case "flow-seq-end": + fc.end.push(this.sourceToken); + return; + } + const bv = this.startBlockValue(fc); + if (bv) + this.stack.push(bv); + else { + yield* this.pop(); + yield* this.step(); + } + } else { + const parent = this.peek(2); + if (parent.type === "block-map" && (this.type === "map-value-ind" && parent.indent === fc.indent || this.type === "newline" && !parent.items[parent.items.length - 1].sep)) { + yield* this.pop(); + yield* this.step(); + } else if (this.type === "map-value-ind" && parent.type !== "flow-collection") { + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + fixFlowSeqItems(fc); + const sep = fc.end.splice(1, fc.end.length); + sep.push(this.sourceToken); + const map = { + type: "block-map", + offset: fc.offset, + indent: fc.indent, + items: [{ start, key: fc, sep }] + }; + this.onKeyLine = true; + this.stack[this.stack.length - 1] = map; + } else { + yield* this.lineEnd(fc); + } + } + } + flowScalar(type) { + if (this.onNewLine) { + let nl = this.source.indexOf("\n") + 1; + while (nl !== 0) { + this.onNewLine(this.offset + nl); + nl = this.source.indexOf("\n", nl) + 1; + } + } + return { + type, + offset: this.offset, + indent: this.indent, + source: this.source + }; + } + startBlockValue(parent) { + switch (this.type) { + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return this.flowScalar(this.type); + case "block-scalar-header": + return { + type: "block-scalar", + offset: this.offset, + indent: this.indent, + props: [this.sourceToken], + source: "" + }; + case "flow-map-start": + case "flow-seq-start": + return { + type: "flow-collection", + offset: this.offset, + indent: this.indent, + start: this.sourceToken, + items: [], + end: [] + }; + case "seq-item-ind": + return { + type: "block-seq", + offset: this.offset, + indent: this.indent, + items: [{ start: [this.sourceToken] }] + }; + case "explicit-key-ind": { + this.onKeyLine = true; + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + start.push(this.sourceToken); + return { + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, explicitKey: true }] + }; + } + case "map-value-ind": { + this.onKeyLine = true; + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + return { + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, key: null, sep: [this.sourceToken] }] + }; + } + } + return null; + } + atIndentedComment(start, indent) { + if (this.type !== "comment") + return false; + if (this.indent <= indent) + return false; + return start.every((st) => st.type === "newline" || st.type === "space"); + } + *documentEnd(docEnd) { + if (this.type !== "doc-mode") { + if (docEnd.end) + docEnd.end.push(this.sourceToken); + else + docEnd.end = [this.sourceToken]; + if (this.type === "newline") + yield* this.pop(); + } + } + *lineEnd(token) { + switch (this.type) { + case "comma": + case "doc-start": + case "doc-end": + case "flow-seq-end": + case "flow-map-end": + case "map-value-ind": + yield* this.pop(); + yield* this.step(); + break; + case "newline": + this.onKeyLine = false; + // fallthrough + case "space": + case "comment": + default: + if (token.end) + token.end.push(this.sourceToken); + else + token.end = [this.sourceToken]; + if (this.type === "newline") + yield* this.pop(); + } + } + }; + exports.Parser = Parser; + } +}); + +// node_modules/yaml/dist/public-api.js +var require_public_api = __commonJS({ + "node_modules/yaml/dist/public-api.js"(exports) { + "use strict"; + var composer = require_composer(); + var Document = require_Document(); + var errors = require_errors(); + var log = require_log(); + var identity = require_identity(); + var lineCounter = require_line_counter(); + var parser = require_parser(); + function parseOptions(options) { + const prettyErrors = options.prettyErrors !== false; + const lineCounter$1 = options.lineCounter || prettyErrors && new lineCounter.LineCounter() || null; + return { lineCounter: lineCounter$1, prettyErrors }; + } + function parseAllDocuments(source, options = {}) { + const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options); + const parser$1 = new parser.Parser(lineCounter2?.addNewLine); + const composer$1 = new composer.Composer(options); + const docs = Array.from(composer$1.compose(parser$1.parse(source))); + if (prettyErrors && lineCounter2) + for (const doc of docs) { + doc.errors.forEach(errors.prettifyError(source, lineCounter2)); + doc.warnings.forEach(errors.prettifyError(source, lineCounter2)); + } + if (docs.length > 0) + return docs; + return Object.assign([], { empty: true }, composer$1.streamInfo()); + } + function parseDocument(source, options = {}) { + const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options); + const parser$1 = new parser.Parser(lineCounter2?.addNewLine); + const composer$1 = new composer.Composer(options); + let doc = null; + for (const _doc of composer$1.compose(parser$1.parse(source), true, source.length)) { + if (!doc) + doc = _doc; + else if (doc.options.logLevel !== "silent") { + doc.errors.push(new errors.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()")); + break; + } + } + if (prettyErrors && lineCounter2) { + doc.errors.forEach(errors.prettifyError(source, lineCounter2)); + doc.warnings.forEach(errors.prettifyError(source, lineCounter2)); + } + return doc; + } + function parse(src, reviver, options) { + let _reviver = void 0; + if (typeof reviver === "function") { + _reviver = reviver; + } else if (options === void 0 && reviver && typeof reviver === "object") { + options = reviver; + } + const doc = parseDocument(src, options); + if (!doc) + return null; + doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning)); + if (doc.errors.length > 0) { + if (doc.options.logLevel !== "silent") + throw doc.errors[0]; + else + doc.errors = []; + } + return doc.toJS(Object.assign({ reviver: _reviver }, options)); + } + function stringify(value, replacer, options) { + let _replacer = null; + if (typeof replacer === "function" || Array.isArray(replacer)) { + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + } + if (typeof options === "string") + options = options.length; + if (typeof options === "number") { + const indent = Math.round(options); + options = indent < 1 ? void 0 : indent > 8 ? { indent: 8 } : { indent }; + } + if (value === void 0) { + const { keepUndefined } = options ?? replacer ?? {}; + if (!keepUndefined) + return void 0; + } + if (identity.isDocument(value) && !_replacer) + return value.toString(options); + return new Document.Document(value, _replacer, options).toString(options); + } + exports.parse = parse; + exports.parseAllDocuments = parseAllDocuments; + exports.parseDocument = parseDocument; + exports.stringify = stringify; + } +}); + +// node_modules/yaml/dist/index.js +var require_dist = __commonJS({ + "node_modules/yaml/dist/index.js"(exports) { + "use strict"; + var composer = require_composer(); + var Document = require_Document(); + var Schema = require_Schema(); + var errors = require_errors(); + var Alias = require_Alias(); + var identity = require_identity(); + var Pair = require_Pair(); + var Scalar = require_Scalar(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var cst = require_cst(); + var lexer = require_lexer(); + var lineCounter = require_line_counter(); + var parser = require_parser(); + var publicApi = require_public_api(); + var visit = require_visit(); + exports.Composer = composer.Composer; + exports.Document = Document.Document; + exports.Schema = Schema.Schema; + exports.YAMLError = errors.YAMLError; + exports.YAMLParseError = errors.YAMLParseError; + exports.YAMLWarning = errors.YAMLWarning; + exports.Alias = Alias.Alias; + exports.isAlias = identity.isAlias; + exports.isCollection = identity.isCollection; + exports.isDocument = identity.isDocument; + exports.isMap = identity.isMap; + exports.isNode = identity.isNode; + exports.isPair = identity.isPair; + exports.isScalar = identity.isScalar; + exports.isSeq = identity.isSeq; + exports.Pair = Pair.Pair; + exports.Scalar = Scalar.Scalar; + exports.YAMLMap = YAMLMap.YAMLMap; + exports.YAMLSeq = YAMLSeq.YAMLSeq; + exports.CST = cst; + exports.Lexer = lexer.Lexer; + exports.LineCounter = lineCounter.LineCounter; + exports.Parser = parser.Parser; + exports.parse = publicApi.parse; + exports.parseAllDocuments = publicApi.parseAllDocuments; + exports.parseDocument = publicApi.parseDocument; + exports.stringify = publicApi.stringify; + exports.visit = visit.visit; + exports.visitAsync = visit.visitAsync; + } +}); + +// tools/installer/ide/platform-codes.js +var require_platform_codes = __commonJS({ + "tools/installer/ide/platform-codes.js"(exports, module) { + var fs = require_fs_native(); + var path = __require("node:path"); + var yaml = require_dist(); + var _cachedPlatformCodes = null; + function resolvePlatformCodesPath() { + return process.env.BMAD_IDE_PLATFORM_CODES || path.join(__dirname, "platform-codes.yaml"); + } + async function loadPlatformCodes() { + if (_cachedPlatformCodes) { + return _cachedPlatformCodes; + } + const PLATFORM_CODES_PATH = resolvePlatformCodesPath(); + if (!await fs.pathExists(PLATFORM_CODES_PATH)) { + throw new Error(`Platform codes configuration not found at: ${PLATFORM_CODES_PATH}`); + } + const content = await fs.readFile(PLATFORM_CODES_PATH, "utf8"); + _cachedPlatformCodes = yaml.parse(content); + return _cachedPlatformCodes; + } + function clearCache() { + _cachedPlatformCodes = null; + } + async function formatPlatformList() { + const { IdeManager } = require_manager(); + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const entries = ideManager.getAvailableIdes().map((ide) => { + const handler = ideManager.handlers.get(ide.value); + return { + id: ide.value, + name: ide.name, + targetDir: handler?.installerConfig?.target_dir || "", + preferred: ide.preferred + }; + }); + const idWidth = Math.max(...entries.map((e) => e.id.length), "ID".length); + const nameWidth = Math.max(...entries.map((e) => e.name.length), "Name".length); + const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length)); + const lines = [ + `Supported tool IDs (pass via --tools [,...]):`, + "", + ` ${pad("ID", idWidth)} ${pad("Name", nameWidth)} Target dir`, + ` ${pad("-".repeat(idWidth), idWidth)} ${pad("-".repeat(nameWidth), nameWidth)} ${"-".repeat(10)}` + ]; + for (const e of entries) { + const star = e.preferred ? " *" : " "; + lines.push(`${star}${pad(e.id, idWidth)} ${pad(e.name, nameWidth)} ${e.targetDir}`); + } + lines.push("", "* = recommended / preferred", "", "Example: bmad-method install --modules bmm --tools claude-code"); + return lines.join("\n"); + } + module.exports = { + loadPlatformCodes, + clearCache, + formatPlatformList + }; + } +}); + +// node_modules/csv-parse/dist/cjs/sync.cjs +var require_sync = __commonJS({ + "node_modules/csv-parse/dist/cjs/sync.cjs"(exports) { + "use strict"; + var CsvError = class _CsvError extends Error { + constructor(code, message, options, ...contexts) { + if (Array.isArray(message)) message = message.join(" ").trim(); + super(message); + if (Error.captureStackTrace !== void 0) { + Error.captureStackTrace(this, _CsvError); + } + this.code = code; + for (const context of contexts) { + for (const key in context) { + const value = context[key]; + this[key] = Buffer.isBuffer(value) ? value.toString(options.encoding) : value == null ? value : JSON.parse(JSON.stringify(value)); + } + } + } + }; + var is_object = function(obj) { + return typeof obj === "object" && obj !== null && !Array.isArray(obj); + }; + var normalize_columns_array = function(columns) { + const normalizedColumns = []; + for (let i = 0, l = columns.length; i < l; i++) { + const column = columns[i]; + if (column === void 0 || column === null || column === false) { + normalizedColumns[i] = { disabled: true }; + } else if (typeof column === "string") { + normalizedColumns[i] = { name: column }; + } else if (is_object(column)) { + if (typeof column.name !== "string") { + throw new CsvError("CSV_OPTION_COLUMNS_MISSING_NAME", [ + "Option columns missing name:", + `property "name" is required at position ${i}`, + "when column is an object literal" + ]); + } + normalizedColumns[i] = column; + } else { + throw new CsvError("CSV_INVALID_COLUMN_DEFINITION", [ + "Invalid column definition:", + "expect a string or a literal object,", + `got ${JSON.stringify(column)} at position ${i}` + ]); + } + } + return normalizedColumns; + }; + var ResizeableBuffer = class { + constructor(size = 100) { + this.size = size; + this.length = 0; + this.buf = Buffer.allocUnsafe(size); + } + prepend(val) { + if (Buffer.isBuffer(val)) { + const length = this.length + val.length; + if (length >= this.size) { + this.resize(); + if (length >= this.size) { + throw Error("INVALID_BUFFER_STATE"); + } + } + const buf = this.buf; + this.buf = Buffer.allocUnsafe(this.size); + val.copy(this.buf, 0); + buf.copy(this.buf, val.length); + this.length += val.length; + } else { + const length = this.length++; + if (length === this.size) { + this.resize(); + } + const buf = this.clone(); + this.buf[0] = val; + buf.copy(this.buf, 1, 0, length); + } + } + append(val) { + const length = this.length++; + if (length === this.size) { + this.resize(); + } + this.buf[length] = val; + } + clone() { + return Buffer.from(this.buf.slice(0, this.length)); + } + resize() { + const length = this.length; + this.size = this.size * 2; + const buf = Buffer.allocUnsafe(this.size); + this.buf.copy(buf, 0, 0, length); + this.buf = buf; + } + toString(encoding) { + if (encoding) { + return this.buf.slice(0, this.length).toString(encoding); + } else { + return Uint8Array.prototype.slice.call(this.buf.slice(0, this.length)); + } + } + toJSON() { + return this.toString("utf8"); + } + reset() { + this.length = 0; + } + }; + var np = 12; + var cr$1 = 13; + var nl$1 = 10; + var space = 32; + var tab = 9; + var init_state = function(options) { + return { + bomSkipped: false, + bufBytesStart: 0, + castField: options.cast_function, + commenting: false, + // Current error encountered by a record + error: void 0, + enabled: options.from_line === 1, + escaping: false, + escapeIsQuote: Buffer.isBuffer(options.escape) && Buffer.isBuffer(options.quote) && Buffer.compare(options.escape, options.quote) === 0, + // columns can be `false`, `true`, `Array` + expectedRecordLength: Array.isArray(options.columns) ? options.columns.length : void 0, + field: new ResizeableBuffer(20), + firstLineToHeaders: options.cast_first_line_to_header, + needMoreDataSize: Math.max( + // Skip if the remaining buffer smaller than comment + options.comment !== null ? options.comment.length : 0, + ...options.delimiter.map((delimiter) => delimiter.length), + // Skip if the remaining buffer can be escape sequence + options.quote !== null ? options.quote.length : 0 + ), + previousBuf: void 0, + quoting: false, + stop: false, + rawBuffer: new ResizeableBuffer(100), + record: [], + recordHasError: false, + record_length: 0, + recordDelimiterMaxLength: options.record_delimiter.length === 0 ? 0 : Math.max(...options.record_delimiter.map((v) => v.length)), + trimChars: [ + Buffer.from(" ", options.encoding)[0], + Buffer.from(" ", options.encoding)[0] + ], + wasQuoting: false, + wasRowDelimiter: false, + timchars: [ + Buffer.from(Buffer.from([cr$1], "utf8").toString(), options.encoding), + Buffer.from(Buffer.from([nl$1], "utf8").toString(), options.encoding), + Buffer.from(Buffer.from([np], "utf8").toString(), options.encoding), + Buffer.from(Buffer.from([space], "utf8").toString(), options.encoding), + Buffer.from(Buffer.from([tab], "utf8").toString(), options.encoding) + ] + }; + }; + var underscore = function(str) { + return str.replace(/([A-Z])/g, function(_, match) { + return "_" + match.toLowerCase(); + }); + }; + var normalize_options = function(opts) { + const options = {}; + for (const opt in opts) { + options[underscore(opt)] = opts[opt]; + } + if (options.encoding === void 0 || options.encoding === true) { + options.encoding = "utf8"; + } else if (options.encoding === null || options.encoding === false) { + options.encoding = null; + } else if (typeof options.encoding !== "string" && options.encoding !== null) { + throw new CsvError( + "CSV_INVALID_OPTION_ENCODING", + [ + "Invalid option encoding:", + "encoding must be a string or null to return a buffer,", + `got ${JSON.stringify(options.encoding)}` + ], + options + ); + } + if (options.bom === void 0 || options.bom === null || options.bom === false) { + options.bom = false; + } else if (options.bom !== true) { + throw new CsvError( + "CSV_INVALID_OPTION_BOM", + [ + "Invalid option bom:", + "bom must be true,", + `got ${JSON.stringify(options.bom)}` + ], + options + ); + } + options.cast_function = null; + if (options.cast === void 0 || options.cast === null || options.cast === false || options.cast === "") { + options.cast = void 0; + } else if (typeof options.cast === "function") { + options.cast_function = options.cast; + options.cast = true; + } else if (options.cast !== true) { + throw new CsvError( + "CSV_INVALID_OPTION_CAST", + [ + "Invalid option cast:", + "cast must be true or a function,", + `got ${JSON.stringify(options.cast)}` + ], + options + ); + } + if (options.cast_date === void 0 || options.cast_date === null || options.cast_date === false || options.cast_date === "") { + options.cast_date = false; + } else if (options.cast_date === true) { + options.cast_date = function(value) { + const date = Date.parse(value); + return !isNaN(date) ? new Date(date) : value; + }; + } else if (typeof options.cast_date !== "function") { + throw new CsvError( + "CSV_INVALID_OPTION_CAST_DATE", + [ + "Invalid option cast_date:", + "cast_date must be true or a function,", + `got ${JSON.stringify(options.cast_date)}` + ], + options + ); + } + options.cast_first_line_to_header = void 0; + if (options.columns === true) { + options.cast_first_line_to_header = void 0; + } else if (typeof options.columns === "function") { + options.cast_first_line_to_header = options.columns; + options.columns = true; + } else if (Array.isArray(options.columns)) { + options.columns = normalize_columns_array(options.columns); + } else if (options.columns === void 0 || options.columns === null || options.columns === false) { + options.columns = false; + } else { + throw new CsvError( + "CSV_INVALID_OPTION_COLUMNS", + [ + "Invalid option columns:", + "expect an array, a function or true,", + `got ${JSON.stringify(options.columns)}` + ], + options + ); + } + if (options.group_columns_by_name === void 0 || options.group_columns_by_name === null || options.group_columns_by_name === false) { + options.group_columns_by_name = false; + } else if (options.group_columns_by_name !== true) { + throw new CsvError( + "CSV_INVALID_OPTION_GROUP_COLUMNS_BY_NAME", + [ + "Invalid option group_columns_by_name:", + "expect an boolean,", + `got ${JSON.stringify(options.group_columns_by_name)}` + ], + options + ); + } else if (options.columns === false) { + throw new CsvError( + "CSV_INVALID_OPTION_GROUP_COLUMNS_BY_NAME", + [ + "Invalid option group_columns_by_name:", + "the `columns` mode must be activated." + ], + options + ); + } + if (options.comment === void 0 || options.comment === null || options.comment === false || options.comment === "") { + options.comment = null; + } else { + if (typeof options.comment === "string") { + options.comment = Buffer.from(options.comment, options.encoding); + } + if (!Buffer.isBuffer(options.comment)) { + throw new CsvError( + "CSV_INVALID_OPTION_COMMENT", + [ + "Invalid option comment:", + "comment must be a buffer or a string,", + `got ${JSON.stringify(options.comment)}` + ], + options + ); + } + } + if (options.comment_no_infix === void 0 || options.comment_no_infix === null || options.comment_no_infix === false) { + options.comment_no_infix = false; + } else if (options.comment_no_infix !== true) { + throw new CsvError( + "CSV_INVALID_OPTION_COMMENT", + [ + "Invalid option comment_no_infix:", + "value must be a boolean,", + `got ${JSON.stringify(options.comment_no_infix)}` + ], + options + ); + } + const delimiter_json = JSON.stringify(options.delimiter); + if (!Array.isArray(options.delimiter)) + options.delimiter = [options.delimiter]; + if (options.delimiter.length === 0) { + throw new CsvError( + "CSV_INVALID_OPTION_DELIMITER", + [ + "Invalid option delimiter:", + "delimiter must be a non empty string or buffer or array of string|buffer,", + `got ${delimiter_json}` + ], + options + ); + } + options.delimiter = options.delimiter.map(function(delimiter) { + if (delimiter === void 0 || delimiter === null || delimiter === false) { + return Buffer.from(",", options.encoding); + } + if (typeof delimiter === "string") { + delimiter = Buffer.from(delimiter, options.encoding); + } + if (!Buffer.isBuffer(delimiter) || delimiter.length === 0) { + throw new CsvError( + "CSV_INVALID_OPTION_DELIMITER", + [ + "Invalid option delimiter:", + "delimiter must be a non empty string or buffer or array of string|buffer,", + `got ${delimiter_json}` + ], + options + ); + } + return delimiter; + }); + if (options.escape === void 0 || options.escape === true) { + options.escape = Buffer.from('"', options.encoding); + } else if (typeof options.escape === "string") { + options.escape = Buffer.from(options.escape, options.encoding); + } else if (options.escape === null || options.escape === false) { + options.escape = null; + } + if (options.escape !== null) { + if (!Buffer.isBuffer(options.escape)) { + throw new Error( + `Invalid Option: escape must be a buffer, a string or a boolean, got ${JSON.stringify(options.escape)}` + ); + } + } + if (options.from === void 0 || options.from === null) { + options.from = 1; + } else { + if (typeof options.from === "string" && /\d+/.test(options.from)) { + options.from = parseInt(options.from); + } + if (Number.isInteger(options.from)) { + if (options.from < 0) { + throw new Error( + `Invalid Option: from must be a positive integer, got ${JSON.stringify(opts.from)}` + ); + } + } else { + throw new Error( + `Invalid Option: from must be an integer, got ${JSON.stringify(options.from)}` + ); + } + } + if (options.from_line === void 0 || options.from_line === null) { + options.from_line = 1; + } else { + if (typeof options.from_line === "string" && /\d+/.test(options.from_line)) { + options.from_line = parseInt(options.from_line); + } + if (Number.isInteger(options.from_line)) { + if (options.from_line <= 0) { + throw new Error( + `Invalid Option: from_line must be a positive integer greater than 0, got ${JSON.stringify(opts.from_line)}` + ); + } + } else { + throw new Error( + `Invalid Option: from_line must be an integer, got ${JSON.stringify(opts.from_line)}` + ); + } + } + if (options.ignore_last_delimiters === void 0 || options.ignore_last_delimiters === null) { + options.ignore_last_delimiters = false; + } else if (typeof options.ignore_last_delimiters === "number") { + options.ignore_last_delimiters = Math.floor(options.ignore_last_delimiters); + if (options.ignore_last_delimiters === 0) { + options.ignore_last_delimiters = false; + } + } else if (typeof options.ignore_last_delimiters !== "boolean") { + throw new CsvError( + "CSV_INVALID_OPTION_IGNORE_LAST_DELIMITERS", + [ + "Invalid option `ignore_last_delimiters`:", + "the value must be a boolean value or an integer,", + `got ${JSON.stringify(options.ignore_last_delimiters)}` + ], + options + ); + } + if (options.ignore_last_delimiters === true && options.columns === false) { + throw new CsvError( + "CSV_IGNORE_LAST_DELIMITERS_REQUIRES_COLUMNS", + [ + "The option `ignore_last_delimiters`", + "requires the activation of the `columns` option" + ], + options + ); + } + if (options.info === void 0 || options.info === null || options.info === false) { + options.info = false; + } else if (options.info !== true) { + throw new Error( + `Invalid Option: info must be true, got ${JSON.stringify(options.info)}` + ); + } + if (options.max_record_size === void 0 || options.max_record_size === null || options.max_record_size === false) { + options.max_record_size = 0; + } else if (Number.isInteger(options.max_record_size) && options.max_record_size >= 0) ; + else if (typeof options.max_record_size === "string" && /\d+/.test(options.max_record_size)) { + options.max_record_size = parseInt(options.max_record_size); + } else { + throw new Error( + `Invalid Option: max_record_size must be a positive integer, got ${JSON.stringify(options.max_record_size)}` + ); + } + if (options.objname === void 0 || options.objname === null || options.objname === false) { + options.objname = void 0; + } else if (Buffer.isBuffer(options.objname)) { + if (options.objname.length === 0) { + throw new Error(`Invalid Option: objname must be a non empty buffer`); + } + if (options.encoding === null) ; + else { + options.objname = options.objname.toString(options.encoding); + } + } else if (typeof options.objname === "string") { + if (options.objname.length === 0) { + throw new Error(`Invalid Option: objname must be a non empty string`); + } + } else if (typeof options.objname === "number") ; + else { + throw new Error( + `Invalid Option: objname must be a string or a buffer, got ${options.objname}` + ); + } + if (options.objname !== void 0) { + if (typeof options.objname === "number") { + if (options.columns !== false) { + throw Error( + "Invalid Option: objname index cannot be combined with columns or be defined as a field" + ); + } + } else { + if (options.columns === false) { + throw Error( + "Invalid Option: objname field must be combined with columns or be defined as an index" + ); + } + } + } + if (options.on_record === void 0 || options.on_record === null) { + options.on_record = void 0; + } else if (typeof options.on_record !== "function") { + throw new CsvError( + "CSV_INVALID_OPTION_ON_RECORD", + [ + "Invalid option `on_record`:", + "expect a function,", + `got ${JSON.stringify(options.on_record)}` + ], + options + ); + } + if (options.on_skip !== void 0 && options.on_skip !== null && typeof options.on_skip !== "function") { + throw new Error( + `Invalid Option: on_skip must be a function, got ${JSON.stringify(options.on_skip)}` + ); + } + if (options.quote === null || options.quote === false || options.quote === "") { + options.quote = null; + } else { + if (options.quote === void 0 || options.quote === true) { + options.quote = Buffer.from('"', options.encoding); + } else if (typeof options.quote === "string") { + options.quote = Buffer.from(options.quote, options.encoding); + } + if (!Buffer.isBuffer(options.quote)) { + throw new Error( + `Invalid Option: quote must be a buffer or a string, got ${JSON.stringify(options.quote)}` + ); + } + } + if (options.raw === void 0 || options.raw === null || options.raw === false) { + options.raw = false; + } else if (options.raw !== true) { + throw new Error( + `Invalid Option: raw must be true, got ${JSON.stringify(options.raw)}` + ); + } + if (options.record_delimiter === void 0) { + options.record_delimiter = []; + } else if (typeof options.record_delimiter === "string" || Buffer.isBuffer(options.record_delimiter)) { + if (options.record_delimiter.length === 0) { + throw new CsvError( + "CSV_INVALID_OPTION_RECORD_DELIMITER", + [ + "Invalid option `record_delimiter`:", + "value must be a non empty string or buffer,", + `got ${JSON.stringify(options.record_delimiter)}` + ], + options + ); + } + options.record_delimiter = [options.record_delimiter]; + } else if (!Array.isArray(options.record_delimiter)) { + throw new CsvError( + "CSV_INVALID_OPTION_RECORD_DELIMITER", + [ + "Invalid option `record_delimiter`:", + "value must be a string, a buffer or array of string|buffer,", + `got ${JSON.stringify(options.record_delimiter)}` + ], + options + ); + } + options.record_delimiter = options.record_delimiter.map(function(rd, i) { + if (typeof rd !== "string" && !Buffer.isBuffer(rd)) { + throw new CsvError( + "CSV_INVALID_OPTION_RECORD_DELIMITER", + [ + "Invalid option `record_delimiter`:", + "value must be a string, a buffer or array of string|buffer", + `at index ${i},`, + `got ${JSON.stringify(rd)}` + ], + options + ); + } else if (rd.length === 0) { + throw new CsvError( + "CSV_INVALID_OPTION_RECORD_DELIMITER", + [ + "Invalid option `record_delimiter`:", + "value must be a non empty string or buffer", + `at index ${i},`, + `got ${JSON.stringify(rd)}` + ], + options + ); + } + if (typeof rd === "string") { + rd = Buffer.from(rd, options.encoding); + } + return rd; + }); + if (typeof options.relax_column_count === "boolean") ; + else if (options.relax_column_count === void 0 || options.relax_column_count === null) { + options.relax_column_count = false; + } else { + throw new Error( + `Invalid Option: relax_column_count must be a boolean, got ${JSON.stringify(options.relax_column_count)}` + ); + } + if (typeof options.relax_column_count_less === "boolean") ; + else if (options.relax_column_count_less === void 0 || options.relax_column_count_less === null) { + options.relax_column_count_less = false; + } else { + throw new Error( + `Invalid Option: relax_column_count_less must be a boolean, got ${JSON.stringify(options.relax_column_count_less)}` + ); + } + if (typeof options.relax_column_count_more === "boolean") ; + else if (options.relax_column_count_more === void 0 || options.relax_column_count_more === null) { + options.relax_column_count_more = false; + } else { + throw new Error( + `Invalid Option: relax_column_count_more must be a boolean, got ${JSON.stringify(options.relax_column_count_more)}` + ); + } + if (typeof options.relax_quotes === "boolean") ; + else if (options.relax_quotes === void 0 || options.relax_quotes === null) { + options.relax_quotes = false; + } else { + throw new Error( + `Invalid Option: relax_quotes must be a boolean, got ${JSON.stringify(options.relax_quotes)}` + ); + } + if (typeof options.skip_empty_lines === "boolean") ; + else if (options.skip_empty_lines === void 0 || options.skip_empty_lines === null) { + options.skip_empty_lines = false; + } else { + throw new Error( + `Invalid Option: skip_empty_lines must be a boolean, got ${JSON.stringify(options.skip_empty_lines)}` + ); + } + if (typeof options.skip_records_with_empty_values === "boolean") ; + else if (options.skip_records_with_empty_values === void 0 || options.skip_records_with_empty_values === null) { + options.skip_records_with_empty_values = false; + } else { + throw new Error( + `Invalid Option: skip_records_with_empty_values must be a boolean, got ${JSON.stringify(options.skip_records_with_empty_values)}` + ); + } + if (typeof options.skip_records_with_error === "boolean") ; + else if (options.skip_records_with_error === void 0 || options.skip_records_with_error === null) { + options.skip_records_with_error = false; + } else { + throw new Error( + `Invalid Option: skip_records_with_error must be a boolean, got ${JSON.stringify(options.skip_records_with_error)}` + ); + } + if (options.rtrim === void 0 || options.rtrim === null || options.rtrim === false) { + options.rtrim = false; + } else if (options.rtrim !== true) { + throw new Error( + `Invalid Option: rtrim must be a boolean, got ${JSON.stringify(options.rtrim)}` + ); + } + if (options.ltrim === void 0 || options.ltrim === null || options.ltrim === false) { + options.ltrim = false; + } else if (options.ltrim !== true) { + throw new Error( + `Invalid Option: ltrim must be a boolean, got ${JSON.stringify(options.ltrim)}` + ); + } + if (options.trim === void 0 || options.trim === null || options.trim === false) { + options.trim = false; + } else if (options.trim !== true) { + throw new Error( + `Invalid Option: trim must be a boolean, got ${JSON.stringify(options.trim)}` + ); + } + if (options.trim === true && opts.ltrim !== false) { + options.ltrim = true; + } else if (options.ltrim !== true) { + options.ltrim = false; + } + if (options.trim === true && opts.rtrim !== false) { + options.rtrim = true; + } else if (options.rtrim !== true) { + options.rtrim = false; + } + if (options.to === void 0 || options.to === null) { + options.to = -1; + } else if (options.to !== -1) { + if (typeof options.to === "string" && /\d+/.test(options.to)) { + options.to = parseInt(options.to); + } + if (Number.isInteger(options.to)) { + if (options.to <= 0) { + throw new Error( + `Invalid Option: to must be a positive integer greater than 0, got ${JSON.stringify(opts.to)}` + ); + } + } else { + throw new Error( + `Invalid Option: to must be an integer, got ${JSON.stringify(opts.to)}` + ); + } + } + if (options.to_line === void 0 || options.to_line === null) { + options.to_line = -1; + } else if (options.to_line !== -1) { + if (typeof options.to_line === "string" && /\d+/.test(options.to_line)) { + options.to_line = parseInt(options.to_line); + } + if (Number.isInteger(options.to_line)) { + if (options.to_line <= 0) { + throw new Error( + `Invalid Option: to_line must be a positive integer greater than 0, got ${JSON.stringify(opts.to_line)}` + ); + } + } else { + throw new Error( + `Invalid Option: to_line must be an integer, got ${JSON.stringify(opts.to_line)}` + ); + } + } + return options; + }; + var isRecordEmpty = function(record) { + return record.every( + (field) => field == null || field.toString && field.toString().trim() === "" + ); + }; + var cr = 13; + var nl = 10; + var boms = { + // Note, the following are equals: + // Buffer.from("\ufeff") + // Buffer.from([239, 187, 191]) + // Buffer.from('EFBBBF', 'hex') + utf8: Buffer.from([239, 187, 191]), + // Note, the following are equals: + // Buffer.from "\ufeff", 'utf16le + // Buffer.from([255, 254]) + utf16le: Buffer.from([255, 254]) + }; + var transform = function(original_options = {}) { + const info = { + bytes: 0, + comment_lines: 0, + empty_lines: 0, + invalid_field_length: 0, + lines: 1, + records: 0 + }; + const options = normalize_options(original_options); + return { + info, + original_options, + options, + state: init_state(options), + __needMoreData: function(i, bufLen, end) { + if (end) return false; + const { encoding, escape, quote } = this.options; + const { quoting, needMoreDataSize, recordDelimiterMaxLength } = this.state; + const numOfCharLeft = bufLen - i - 1; + const requiredLength = Math.max( + needMoreDataSize, + // Skip if the remaining buffer smaller than record delimiter + // If "record_delimiter" is yet to be discovered: + // 1. It is equals to `[]` and "recordDelimiterMaxLength" equals `0` + // 2. We set the length to windows line ending in the current encoding + // Note, that encoding is known from user or bom discovery at that point + // recordDelimiterMaxLength, + recordDelimiterMaxLength === 0 ? Buffer.from("\r\n", encoding).length : recordDelimiterMaxLength, + // Skip if remaining buffer can be an escaped quote + quoting ? (escape === null ? 0 : escape.length) + quote.length : 0, + // Skip if remaining buffer can be record delimiter following the closing quote + quoting ? quote.length + recordDelimiterMaxLength : 0 + ); + return numOfCharLeft < requiredLength; + }, + // Central parser implementation + parse: function(nextBuf, end, push, close) { + const { + bom, + comment_no_infix, + encoding, + from_line, + ltrim, + max_record_size, + raw, + relax_quotes, + rtrim, + skip_empty_lines, + to, + to_line + } = this.options; + let { comment, escape, quote, record_delimiter } = this.options; + const { bomSkipped, previousBuf, rawBuffer, escapeIsQuote } = this.state; + let buf; + if (previousBuf === void 0) { + if (nextBuf === void 0) { + close(); + return; + } else { + buf = nextBuf; + } + } else if (previousBuf !== void 0 && nextBuf === void 0) { + buf = previousBuf; + } else { + buf = Buffer.concat([previousBuf, nextBuf]); + } + if (bomSkipped === false) { + if (bom === false) { + this.state.bomSkipped = true; + } else if (buf.length < 3) { + if (end === false) { + this.state.previousBuf = buf; + return; + } + } else { + for (const encoding2 in boms) { + if (boms[encoding2].compare(buf, 0, boms[encoding2].length) === 0) { + const bomLength = boms[encoding2].length; + this.state.bufBytesStart += bomLength; + buf = buf.slice(bomLength); + const options2 = normalize_options({ + ...this.original_options, + encoding: encoding2 + }); + for (const key in options2) { + this.options[key] = options2[key]; + } + ({ comment, escape, quote } = this.options); + break; + } + } + this.state.bomSkipped = true; + } + } + const bufLen = buf.length; + let pos; + for (pos = 0; pos < bufLen; pos++) { + if (this.__needMoreData(pos, bufLen, end)) { + break; + } + if (this.state.wasRowDelimiter === true) { + this.info.lines++; + this.state.wasRowDelimiter = false; + } + if (to_line !== -1 && this.info.lines > to_line) { + this.state.stop = true; + close(); + return; + } + if (this.state.quoting === false && record_delimiter.length === 0) { + const record_delimiterCount = this.__autoDiscoverRecordDelimiter( + buf, + pos + ); + if (record_delimiterCount) { + record_delimiter = this.options.record_delimiter; + } + } + const chr = buf[pos]; + if (raw === true) { + rawBuffer.append(chr); + } + if ((chr === cr || chr === nl) && this.state.wasRowDelimiter === false) { + this.state.wasRowDelimiter = true; + } + if (this.state.escaping === true) { + this.state.escaping = false; + } else { + if (escape !== null && this.state.quoting === true && this.__isEscape(buf, pos, chr) && pos + escape.length < bufLen) { + if (escapeIsQuote) { + if (this.__isQuote(buf, pos + escape.length)) { + this.state.escaping = true; + pos += escape.length - 1; + continue; + } + } else { + this.state.escaping = true; + pos += escape.length - 1; + continue; + } + } + if (this.state.commenting === false && this.__isQuote(buf, pos)) { + if (this.state.quoting === true) { + const nextChr = buf[pos + quote.length]; + const isNextChrTrimable = rtrim && this.__isCharTrimable(buf, pos + quote.length); + const isNextChrComment = comment !== null && this.__compareBytes(comment, buf, pos + quote.length, nextChr); + const isNextChrDelimiter = this.__isDelimiter( + buf, + pos + quote.length, + nextChr + ); + const isNextChrRecordDelimiter = record_delimiter.length === 0 ? this.__autoDiscoverRecordDelimiter(buf, pos + quote.length) : this.__isRecordDelimiter(nextChr, buf, pos + quote.length); + if (escape !== null && this.__isEscape(buf, pos, chr) && this.__isQuote(buf, pos + escape.length)) { + pos += escape.length - 1; + } else if (!nextChr || isNextChrDelimiter || isNextChrRecordDelimiter || isNextChrComment || isNextChrTrimable) { + this.state.quoting = false; + this.state.wasQuoting = true; + pos += quote.length - 1; + continue; + } else if (relax_quotes === false) { + const err = this.__error( + new CsvError( + "CSV_INVALID_CLOSING_QUOTE", + [ + "Invalid Closing Quote:", + `got "${String.fromCharCode(nextChr)}"`, + `at line ${this.info.lines}`, + "instead of delimiter, record delimiter, trimable character", + "(if activated) or comment" + ], + this.options, + this.__infoField() + ) + ); + if (err !== void 0) return err; + } else { + this.state.quoting = false; + this.state.wasQuoting = true; + this.state.field.prepend(quote); + pos += quote.length - 1; + } + } else { + if (this.state.field.length !== 0) { + if (relax_quotes === false) { + const info2 = this.__infoField(); + const bom2 = Object.keys(boms).map( + (b) => boms[b].equals(this.state.field.toString()) ? b : false + ).filter(Boolean)[0]; + const err = this.__error( + new CsvError( + "INVALID_OPENING_QUOTE", + [ + "Invalid Opening Quote:", + `a quote is found on field ${JSON.stringify(info2.column)} at line ${info2.lines}, value is ${JSON.stringify(this.state.field.toString(encoding))}`, + bom2 ? `(${bom2} bom)` : void 0 + ], + this.options, + info2, + { + field: this.state.field + } + ) + ); + if (err !== void 0) return err; + } + } else { + this.state.quoting = true; + pos += quote.length - 1; + continue; + } + } + } + if (this.state.quoting === false) { + const recordDelimiterLength = this.__isRecordDelimiter( + chr, + buf, + pos + ); + if (recordDelimiterLength !== 0) { + const skipCommentLine = this.state.commenting && this.state.wasQuoting === false && this.state.record.length === 0 && this.state.field.length === 0; + if (skipCommentLine) { + this.info.comment_lines++; + } else { + if (this.state.enabled === false && this.info.lines + (this.state.wasRowDelimiter === true ? 1 : 0) >= from_line) { + this.state.enabled = true; + this.__resetField(); + this.__resetRecord(); + pos += recordDelimiterLength - 1; + continue; + } + if (skip_empty_lines === true && this.state.wasQuoting === false && this.state.record.length === 0 && this.state.field.length === 0) { + this.info.empty_lines++; + pos += recordDelimiterLength - 1; + continue; + } + this.info.bytes = this.state.bufBytesStart + pos; + const errField = this.__onField(); + if (errField !== void 0) return errField; + this.info.bytes = this.state.bufBytesStart + pos + recordDelimiterLength; + const errRecord = this.__onRecord(push); + if (errRecord !== void 0) return errRecord; + if (to !== -1 && this.info.records >= to) { + this.state.stop = true; + close(); + return; + } + } + this.state.commenting = false; + pos += recordDelimiterLength - 1; + continue; + } + if (this.state.commenting) { + continue; + } + if (comment !== null && (comment_no_infix === false || this.state.record.length === 0 && this.state.field.length === 0)) { + const commentCount = this.__compareBytes(comment, buf, pos, chr); + if (commentCount !== 0) { + this.state.commenting = true; + continue; + } + } + const delimiterLength = this.__isDelimiter(buf, pos, chr); + if (delimiterLength !== 0) { + this.info.bytes = this.state.bufBytesStart + pos; + const errField = this.__onField(); + if (errField !== void 0) return errField; + pos += delimiterLength - 1; + continue; + } + } + } + if (this.state.commenting === false) { + if (max_record_size !== 0 && this.state.record_length + this.state.field.length > max_record_size) { + return this.__error( + new CsvError( + "CSV_MAX_RECORD_SIZE", + [ + "Max Record Size:", + "record exceed the maximum number of tolerated bytes", + `of ${max_record_size}`, + `at line ${this.info.lines}` + ], + this.options, + this.__infoField() + ) + ); + } + } + const lappend = ltrim === false || this.state.quoting === true || this.state.field.length !== 0 || !this.__isCharTrimable(buf, pos); + const rappend = rtrim === false || this.state.wasQuoting === false; + if (lappend === true && rappend === true) { + this.state.field.append(chr); + } else if (rtrim === true && !this.__isCharTrimable(buf, pos)) { + return this.__error( + new CsvError( + "CSV_NON_TRIMABLE_CHAR_AFTER_CLOSING_QUOTE", + [ + "Invalid Closing Quote:", + "found non trimable byte after quote", + `at line ${this.info.lines}` + ], + this.options, + this.__infoField() + ) + ); + } else { + if (lappend === false) { + pos += this.__isCharTrimable(buf, pos) - 1; + } + continue; + } + } + if (end === true) { + if (this.state.quoting === true) { + const err = this.__error( + new CsvError( + "CSV_QUOTE_NOT_CLOSED", + [ + "Quote Not Closed:", + `the parsing is finished with an opening quote at line ${this.info.lines}` + ], + this.options, + this.__infoField() + ) + ); + if (err !== void 0) return err; + } else { + if (this.state.wasQuoting === true || this.state.record.length !== 0 || this.state.field.length !== 0) { + this.info.bytes = this.state.bufBytesStart + pos; + const errField = this.__onField(); + if (errField !== void 0) return errField; + const errRecord = this.__onRecord(push); + if (errRecord !== void 0) return errRecord; + } else if (this.state.wasRowDelimiter === true) { + this.info.empty_lines++; + } else if (this.state.commenting === true) { + this.info.comment_lines++; + } + } + } else { + this.state.bufBytesStart += pos; + this.state.previousBuf = buf.slice(pos); + } + if (this.state.wasRowDelimiter === true) { + this.info.lines++; + this.state.wasRowDelimiter = false; + } + }, + __onRecord: function(push) { + const { + columns, + group_columns_by_name, + encoding, + info: info2, + from, + relax_column_count, + relax_column_count_less, + relax_column_count_more, + raw, + skip_records_with_empty_values + } = this.options; + const { enabled, record } = this.state; + if (enabled === false) { + return this.__resetRecord(); + } + const recordLength = record.length; + if (columns === true) { + if (skip_records_with_empty_values === true && isRecordEmpty(record)) { + this.__resetRecord(); + return; + } + return this.__firstLineToColumns(record); + } + if (columns === false && this.info.records === 0) { + this.state.expectedRecordLength = recordLength; + } + if (recordLength !== this.state.expectedRecordLength) { + const err = columns === false ? new CsvError( + "CSV_RECORD_INCONSISTENT_FIELDS_LENGTH", + [ + "Invalid Record Length:", + `expect ${this.state.expectedRecordLength},`, + `got ${recordLength} on line ${this.info.lines}` + ], + this.options, + this.__infoField(), + { + record + } + ) : new CsvError( + "CSV_RECORD_INCONSISTENT_COLUMNS", + [ + "Invalid Record Length:", + `columns length is ${columns.length},`, + // rename columns + `got ${recordLength} on line ${this.info.lines}` + ], + this.options, + this.__infoField(), + { + record + } + ); + if (relax_column_count === true || relax_column_count_less === true && recordLength < this.state.expectedRecordLength || relax_column_count_more === true && recordLength > this.state.expectedRecordLength) { + this.info.invalid_field_length++; + this.state.error = err; + } else { + const finalErr = this.__error(err); + if (finalErr) return finalErr; + } + } + if (skip_records_with_empty_values === true && isRecordEmpty(record)) { + this.__resetRecord(); + return; + } + if (this.state.recordHasError === true) { + this.__resetRecord(); + this.state.recordHasError = false; + return; + } + this.info.records++; + if (from === 1 || this.info.records >= from) { + const { objname } = this.options; + if (columns !== false) { + const obj = {}; + for (let i = 0, l = record.length; i < l; i++) { + if (columns[i] === void 0 || columns[i].disabled) continue; + if (group_columns_by_name === true && obj[columns[i].name] !== void 0) { + if (Array.isArray(obj[columns[i].name])) { + obj[columns[i].name] = obj[columns[i].name].concat(record[i]); + } else { + obj[columns[i].name] = [obj[columns[i].name], record[i]]; + } + } else { + obj[columns[i].name] = record[i]; + } + } + if (raw === true || info2 === true) { + const extRecord = Object.assign( + { record: obj }, + raw === true ? { raw: this.state.rawBuffer.toString(encoding) } : {}, + info2 === true ? { info: this.__infoRecord() } : {} + ); + const err = this.__push( + objname === void 0 ? extRecord : [obj[objname], extRecord], + push + ); + if (err) { + return err; + } + } else { + const err = this.__push( + objname === void 0 ? obj : [obj[objname], obj], + push + ); + if (err) { + return err; + } + } + } else { + if (raw === true || info2 === true) { + const extRecord = Object.assign( + { record }, + raw === true ? { raw: this.state.rawBuffer.toString(encoding) } : {}, + info2 === true ? { info: this.__infoRecord() } : {} + ); + const err = this.__push( + objname === void 0 ? extRecord : [record[objname], extRecord], + push + ); + if (err) { + return err; + } + } else { + const err = this.__push( + objname === void 0 ? record : [record[objname], record], + push + ); + if (err) { + return err; + } + } + } + } + this.__resetRecord(); + }, + __firstLineToColumns: function(record) { + const { firstLineToHeaders } = this.state; + try { + const headers = firstLineToHeaders === void 0 ? record : firstLineToHeaders.call(null, record); + if (!Array.isArray(headers)) { + return this.__error( + new CsvError( + "CSV_INVALID_COLUMN_MAPPING", + [ + "Invalid Column Mapping:", + "expect an array from column function,", + `got ${JSON.stringify(headers)}` + ], + this.options, + this.__infoField(), + { + headers + } + ) + ); + } + const normalizedHeaders = normalize_columns_array(headers); + this.state.expectedRecordLength = normalizedHeaders.length; + this.options.columns = normalizedHeaders; + this.__resetRecord(); + return; + } catch (err) { + return err; + } + }, + __resetRecord: function() { + if (this.options.raw === true) { + this.state.rawBuffer.reset(); + } + this.state.error = void 0; + this.state.record = []; + this.state.record_length = 0; + }, + __onField: function() { + const { cast, encoding, rtrim, max_record_size } = this.options; + const { enabled, wasQuoting } = this.state; + if (enabled === false) { + return this.__resetField(); + } + let field = this.state.field.toString(encoding); + if (rtrim === true && wasQuoting === false) { + field = field.trimRight(); + } + if (cast === true) { + const [err, f] = this.__cast(field); + if (err !== void 0) return err; + field = f; + } + this.state.record.push(field); + if (max_record_size !== 0 && typeof field === "string") { + this.state.record_length += field.length; + } + this.__resetField(); + }, + __resetField: function() { + this.state.field.reset(); + this.state.wasQuoting = false; + }, + __push: function(record, push) { + const { on_record } = this.options; + if (on_record !== void 0) { + const info2 = this.__infoRecord(); + try { + record = on_record.call(null, record, info2); + } catch (err) { + return err; + } + if (record === void 0 || record === null) { + return; + } + } + push(record); + }, + // Return a tuple with the error and the casted value + __cast: function(field) { + const { columns, relax_column_count } = this.options; + const isColumns = Array.isArray(columns); + if (isColumns === true && relax_column_count && this.options.columns.length <= this.state.record.length) { + return [void 0, void 0]; + } + if (this.state.castField !== null) { + try { + const info2 = this.__infoField(); + return [void 0, this.state.castField.call(null, field, info2)]; + } catch (err) { + return [err]; + } + } + if (this.__isFloat(field)) { + return [void 0, parseFloat(field)]; + } else if (this.options.cast_date !== false) { + const info2 = this.__infoField(); + return [void 0, this.options.cast_date.call(null, field, info2)]; + } + return [void 0, field]; + }, + // Helper to test if a character is a space or a line delimiter + __isCharTrimable: function(buf, pos) { + const isTrim = (buf2, pos2) => { + const { timchars } = this.state; + loop1: for (let i = 0; i < timchars.length; i++) { + const timchar = timchars[i]; + for (let j = 0; j < timchar.length; j++) { + if (timchar[j] !== buf2[pos2 + j]) continue loop1; + } + return timchar.length; + } + return 0; + }; + return isTrim(buf, pos); + }, + // Keep it in case we implement the `cast_int` option + // __isInt(value){ + // // return Number.isInteger(parseInt(value)) + // // return !isNaN( parseInt( obj ) ); + // return /^(\-|\+)?[1-9][0-9]*$/.test(value) + // } + __isFloat: function(value) { + return value - parseFloat(value) + 1 >= 0; + }, + __compareBytes: function(sourceBuf, targetBuf, targetPos, firstByte) { + if (sourceBuf[0] !== firstByte) return 0; + const sourceLength = sourceBuf.length; + for (let i = 1; i < sourceLength; i++) { + if (sourceBuf[i] !== targetBuf[targetPos + i]) return 0; + } + return sourceLength; + }, + __isDelimiter: function(buf, pos, chr) { + const { delimiter, ignore_last_delimiters } = this.options; + if (ignore_last_delimiters === true && this.state.record.length === this.options.columns.length - 1) { + return 0; + } else if (ignore_last_delimiters !== false && typeof ignore_last_delimiters === "number" && this.state.record.length === ignore_last_delimiters - 1) { + return 0; + } + loop1: for (let i = 0; i < delimiter.length; i++) { + const del = delimiter[i]; + if (del[0] === chr) { + for (let j = 1; j < del.length; j++) { + if (del[j] !== buf[pos + j]) continue loop1; + } + return del.length; + } + } + return 0; + }, + __isRecordDelimiter: function(chr, buf, pos) { + const { record_delimiter } = this.options; + const recordDelimiterLength = record_delimiter.length; + loop1: for (let i = 0; i < recordDelimiterLength; i++) { + const rd = record_delimiter[i]; + const rdLength = rd.length; + if (rd[0] !== chr) { + continue; + } + for (let j = 1; j < rdLength; j++) { + if (rd[j] !== buf[pos + j]) { + continue loop1; + } + } + return rd.length; + } + return 0; + }, + __isEscape: function(buf, pos, chr) { + const { escape } = this.options; + if (escape === null) return false; + const l = escape.length; + if (escape[0] === chr) { + for (let i = 0; i < l; i++) { + if (escape[i] !== buf[pos + i]) { + return false; + } + } + return true; + } + return false; + }, + __isQuote: function(buf, pos) { + const { quote } = this.options; + if (quote === null) return false; + const l = quote.length; + for (let i = 0; i < l; i++) { + if (quote[i] !== buf[pos + i]) { + return false; + } + } + return true; + }, + __autoDiscoverRecordDelimiter: function(buf, pos) { + const { encoding } = this.options; + const rds = [ + // Important, the windows line ending must be before mac os 9 + Buffer.from("\r\n", encoding), + Buffer.from("\n", encoding), + Buffer.from("\r", encoding) + ]; + loop: for (let i = 0; i < rds.length; i++) { + const l = rds[i].length; + for (let j = 0; j < l; j++) { + if (rds[i][j] !== buf[pos + j]) { + continue loop; + } + } + this.options.record_delimiter.push(rds[i]); + this.state.recordDelimiterMaxLength = rds[i].length; + return rds[i].length; + } + return 0; + }, + __error: function(msg) { + const { encoding, raw, skip_records_with_error } = this.options; + const err = typeof msg === "string" ? new Error(msg) : msg; + if (skip_records_with_error) { + this.state.recordHasError = true; + if (this.options.on_skip !== void 0) { + try { + this.options.on_skip( + err, + raw ? this.state.rawBuffer.toString(encoding) : void 0 + ); + } catch (err2) { + return err2; + } + } + return void 0; + } else { + return err; + } + }, + __infoDataSet: function() { + return { + ...this.info, + columns: this.options.columns + }; + }, + __infoRecord: function() { + const { columns, raw, encoding } = this.options; + return { + ...this.__infoDataSet(), + error: this.state.error, + header: columns === true, + index: this.state.record.length, + raw: raw ? this.state.rawBuffer.toString(encoding) : void 0 + }; + }, + __infoField: function() { + const { columns } = this.options; + const isColumns = Array.isArray(columns); + return { + ...this.__infoRecord(), + column: isColumns === true ? columns.length > this.state.record.length ? columns[this.state.record.length].name : null : this.state.record.length, + quoting: this.state.wasQuoting + }; + } + }; + }; + var parse = function(data, opts = {}) { + if (typeof data === "string") { + data = Buffer.from(data); + } + const records = opts && opts.objname ? {} : []; + const parser = transform(opts); + const push = (record) => { + if (parser.options.objname === void 0) records.push(record); + else { + records[record[0]] = record[1]; + } + }; + const close = () => { + }; + const error = parser.parse(data, true, push, close); + if (error !== void 0) throw error; + return records; + }; + exports.CsvError = CsvError; + exports.parse = parse; + } +}); + +// tools/installer/ide/shared/installed-skills.js +var require_installed_skills = __commonJS({ + "tools/installer/ide/shared/installed-skills.js"(exports, module) { + var path = __require("node:path"); + var fs = require_fs_native(); + var csv = require_sync(); + async function getInstalledCanonicalIds(bmadDir) { + const ids = /* @__PURE__ */ new Set(); + if (!bmadDir) return ids; + const csvPath = path.join(bmadDir, "_config", "skill-manifest.csv"); + if (!await fs.pathExists(csvPath)) return ids; + try { + const content = await fs.readFile(csvPath, "utf8"); + const records = csv.parse(content, { columns: true, skip_empty_lines: true }); + for (const record of records) { + if (record.canonicalId) ids.add(record.canonicalId); + } + } catch { + } + return ids; + } + function isBmadOwnedEntry(entry, canonicalIds) { + if (!entry || typeof entry !== "string") return false; + if (entry.toLowerCase().startsWith("bmad-os-")) return false; + if (canonicalIds && canonicalIds.size > 0) return canonicalIds.has(entry); + return entry.toLowerCase().startsWith("bmad"); + } + module.exports = { getInstalledCanonicalIds, isBmadOwnedEntry }; + } +}); + +// src/core-skills/bmad-module/scripts/lib/vendor/shims/project-root.cjs +var require_project_root = __commonJS({ + "src/core-skills/bmad-module/scripts/lib/vendor/shims/project-root.cjs"(exports, module) { + "use strict"; + var path = __require("node:path"); + function getProjectRoot() { + return process.cwd(); + } + function getSourcePath(...segments) { + return path.join(process.cwd(), ...segments); + } + module.exports = { getProjectRoot, getSourcePath }; + } +}); + +// tools/installer/ide/_config-driven.js +var require_config_driven = __commonJS({ + "tools/installer/ide/_config-driven.js"(exports, module) { + var path = __require("node:path"); + var fs = require_fs_native(); + var yaml = require_dist(); + var prompts = require_prompts(); + var csv = require_sync(); + var { BMAD_FOLDER_NAME } = require_path_utils(); + var { getInstalledCanonicalIds, isBmadOwnedEntry } = require_installed_skills(); + var RESERVED_OPENCODE_COMMANDS = /* @__PURE__ */ new Set([ + "review", + "commit", + "init", + "help", + "skills", + "fast", + "compact", + "clear", + "undo", + "redo", + "edit", + "editor", + "exit", + "quit", + "theme", + "config", + "model", + "session" + ]); + function yamlSafeSingleLine(value) { + const collapsed = String(value).replaceAll(/[\r\n]+/g, " ").trim(); + const needsQuoting = /[:#'"\\]/.test(collapsed) || /^[!&*?|>%@`[{]/.test(collapsed); + if (!needsQuoting) return collapsed; + const escaped = collapsed.replaceAll("\\", "\\\\").replaceAll('"', String.raw`\"`); + return `"${escaped}"`; + } + function isSafeCanonicalId(value) { + return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value) && !value.includes(".."); + } + var DEFAULT_COMMANDS_BODY_TEMPLATE = "@skills/{canonicalId}"; + async function isAgentSkill(record, bmadDir) { + if (!record?.path || !bmadDir) return false; + const bmadFolderName = path.basename(bmadDir); + const bmadPrefix = bmadFolderName + "/"; + const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path; + const tomlPath = path.join(bmadDir, path.dirname(relativePath), "customize.toml"); + if (!await fs.pathExists(tomlPath)) return false; + try { + const content = await fs.readFile(tomlPath, "utf8"); + return /^\[agent\]/m.test(content); + } catch { + return false; + } + } + function expandBodyTemplate(template, { canonicalId, targetDir }) { + return template.replaceAll("{canonicalId}", canonicalId).replaceAll("{target_dir}", targetDir); + } + function buildCommandPointerBody(description, canonicalId, { template, targetDir }) { + const bodyText = expandBodyTemplate(template, { canonicalId, targetDir }); + return `--- +description: ${yamlSafeSingleLine(description)} +--- + +${bodyText} +`; + } + function looksLikeGeneratorOutput(content, canonicalId, { template, targetDir }) { + if (typeof content !== "string") return false; + const trimmed = content.trim(); + const expectedTail = expandBodyTemplate(template, { canonicalId, targetDir }).trim(); + if (!trimmed.endsWith(expectedTail)) return false; + const fmMatch = trimmed.match(/^---\n([\S\s]*?)\n---\n/); + if (!fmMatch) return false; + const fmLines = fmMatch[1].split("\n").filter((l) => l.length > 0); + if (fmLines.length !== 1) return false; + if (!fmLines[0].startsWith("description:")) return false; + return true; + } + var ConfigDrivenIdeSetup = class { + constructor(platformCode, platformConfig) { + this.name = platformCode; + this.displayName = platformConfig.name || platformCode; + this.preferred = platformConfig.preferred || false; + this.platformConfig = platformConfig; + this.installerConfig = platformConfig.installer || null; + this.bmadFolderName = BMAD_FOLDER_NAME; + this.configDir = this.installerConfig?.target_dir || null; + } + setBmadFolderName(bmadFolderName) { + this.bmadFolderName = bmadFolderName; + } + /** + * Detect whether this IDE already has configuration in the project. + * Checks for bmad-prefixed entries in target_dir. + * @param {string} projectDir - Project directory + * @returns {Promise} + */ + async detect(projectDir) { + if (!this.configDir) return false; + const root = projectDir || process.cwd(); + const dir = path.join(root, this.configDir); + if (!await fs.pathExists(dir)) return false; + let entries; + try { + entries = await fs.readdir(dir); + } catch { + return false; + } + const bmadDir = await this._findBmadDir(root); + const canonicalIds = await getInstalledCanonicalIds(bmadDir); + return entries.some((e) => isBmadOwnedEntry(e, canonicalIds)); + } + /** + * Main setup method - called by IdeManager + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + * @returns {Promise} Setup result + */ + async setup(projectDir, bmadDir, options = {}) { + if (this.installerConfig?.ancestor_conflict_check) { + const conflict = await this.findAncestorConflict(projectDir); + if (conflict) { + await prompts.log.error( + `Found existing BMAD skills in ancestor installation: ${conflict} + ${this.name} inherits skills from parent directories, so this would cause duplicates. + Please remove the BMAD files from that directory first: + rm -rf "${conflict}"/bmad*` + ); + return { + success: false, + reason: "ancestor-conflict", + error: `Ancestor conflict: ${conflict}`, + conflictDir: conflict + }; + } + } + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); + await this.cleanup(projectDir, options, bmadDir); + if (!this.installerConfig) { + return { success: false, reason: "no-config" }; + } + if (options.skipTarget) { + const results = { skills: 0, sharedTargetHandledByPeer: true }; + if (this.installerConfig.commands_target_dir) { + results.commands = await this.installCommandPointers(projectDir, bmadDir, this.installerConfig, options); + } + return { success: true, results }; + } + if (this.installerConfig.target_dir) { + return this.installToTarget(projectDir, bmadDir, this.installerConfig, options); + } + return { success: false, reason: "invalid-config" }; + } + /** + * Install to a single target directory + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} config - Installation configuration + * @param {Object} options - Setup options + * @returns {Promise} Installation result + */ + async installToTarget(projectDir, bmadDir, config, options) { + const { target_dir } = config; + const targetPath = path.join(projectDir, target_dir); + await fs.ensureDir(targetPath); + this.skillWriteTracker = /* @__PURE__ */ new Set(); + const results = { skills: 0 }; + results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); + results.skillDirectories = this.skillWriteTracker.size; + if (config.commands_target_dir) { + results.commands = await this.installCommandPointers(projectDir, bmadDir, config, options); + } + await this.printSummary(results, target_dir, options); + this.skillWriteTracker = null; + return { success: true, results }; + } + /** + * Generate per-skill command pointer files for IDEs that surface commands + * separately from skills (e.g. OpenCode's `.opencode/commands/.md`). + * + * Each pointer is a tiny markdown file whose body is `@skills/` + * so invoking `/` routes the user straight to the skill instead + * of forcing them through a `/skills` menu. + * + * Skips: + * - Names that collide with reserved built-in slash commands. + * - canonicalIds that aren't safe basename-only identifiers (defense + * against path traversal even though the manifest is currently trusted). + * - Existing files whose body looks user-modified (preserves hand edits); + * pointer files matching the generator pattern get overwritten so that + * description changes in skill-manifest.csv propagate on re-install. + * + * Per-file write failures are recorded and reported but do not abort the + * rest of the install — pointer files are a non-essential adjunct to the + * skill copy that already succeeded. + * + * @param {string} projectDir + * @param {string} bmadDir + * @param {Object} config - Installer config; reads commands_target_dir. + * @param {Object} options - Setup options. forceCommands overwrites existing + * files unconditionally (including hand-modified ones). + * @returns {Promise} { created, updated, skippedExisting, skippedCollision, skippedInvalidId, writeFailures, fallbackDescription } + */ + async installCommandPointers(projectDir, bmadDir, config, options = {}) { + const result = { + created: 0, + updated: 0, + skippedExisting: 0, + skippedCollision: 0, + skippedInvalidId: 0, + skippedFiltered: 0, + writeFailures: 0, + fallbackDescription: 0 + }; + const csvPath = path.join(bmadDir, "_config", "skill-manifest.csv"); + if (!await fs.pathExists(csvPath)) return result; + const commandsPath = path.join(projectDir, config.commands_target_dir); + await fs.ensureDir(commandsPath); + const extension = config.commands_extension || ".md"; + const template = config.commands_body_template || DEFAULT_COMMANDS_BODY_TEMPLATE; + const targetDir = config.target_dir; + const filter = config.commands_filter || null; + const csvContent = await fs.readFile(csvPath, "utf8"); + const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true }); + for (const record of records) { + const canonicalId = record.canonicalId; + if (!canonicalId) continue; + if (!isSafeCanonicalId(canonicalId)) { + result.skippedInvalidId++; + continue; + } + if (filter === "agents-only" && !await isAgentSkill(record, bmadDir)) { + result.skippedFiltered++; + continue; + } + if (this.name === "opencode" && RESERVED_OPENCODE_COMMANDS.has(canonicalId)) { + result.skippedCollision++; + continue; + } + let description = (record.description || "").trim(); + if (!description) { + description = `Run the ${canonicalId} skill`; + result.fallbackDescription++; + } + const body = buildCommandPointerBody(description, canonicalId, { template, targetDir }); + const commandFile = path.join(commandsPath, `${canonicalId}${extension}`); + if (!options.forceCommands && await fs.pathExists(commandFile)) { + let existing; + try { + existing = await fs.readFile(commandFile, "utf8"); + } catch { + result.skippedExisting++; + continue; + } + if (existing === body) { + result.skippedExisting++; + continue; + } + if (looksLikeGeneratorOutput(existing, canonicalId, { template, targetDir })) { + try { + await fs.writeFile(commandFile, body, "utf8"); + result.updated++; + } catch (error) { + result.writeFailures++; + if (!options.silent) { + await prompts.log.warn(`Failed to update command pointer ${canonicalId}${extension}: ${error.message}`); + } + } + continue; + } + result.skippedExisting++; + continue; + } + try { + await fs.writeFile(commandFile, body, "utf8"); + result.created++; + } catch (error) { + result.writeFailures++; + if (!options.silent) { + await prompts.log.warn(`Failed to write command pointer ${canonicalId}${extension}: ${error.message}`); + } + } + } + return result; + } + /** + * Install verbatim native SKILL.md directories from skill-manifest.csv. + * Copies the entire source directory as-is into the IDE skill directory. + * The source SKILL.md is used directly — no frontmatter transformation or file generation. + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {string} targetPath - Target skills directory + * @param {Object} config - Installation configuration + * @returns {Promise} Count of skills installed + */ + async installVerbatimSkills(projectDir, bmadDir, targetPath, config) { + const bmadFolderName = path.basename(bmadDir); + const bmadPrefix = bmadFolderName + "/"; + const csvPath = path.join(bmadDir, "_config", "skill-manifest.csv"); + if (!await fs.pathExists(csvPath)) return 0; + const csvContent = await fs.readFile(csvPath, "utf8"); + const records = csv.parse(csvContent, { + columns: true, + skip_empty_lines: true + }); + let count = 0; + for (const record of records) { + const canonicalId = record.canonicalId; + if (!canonicalId) continue; + const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path; + const sourceFile = path.join(bmadDir, relativePath); + const sourceDir = path.dirname(sourceFile); + if (!await fs.pathExists(sourceDir)) continue; + const skillDir = path.join(targetPath, canonicalId); + await fs.remove(skillDir); + await fs.ensureDir(skillDir); + this.skillWriteTracker?.add(canonicalId); + const skipPatterns = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]); + const skipSuffixes = ["~", ".swp", ".swo", ".bak"]; + const filter = (src) => { + const name = path.basename(src); + if (src === sourceDir) return true; + if (skipPatterns.has(name)) return false; + if (name.startsWith(".") && name !== ".gitkeep") return false; + if (skipSuffixes.some((s) => name.endsWith(s))) return false; + return true; + }; + await fs.copy(sourceDir, skillDir, { filter }); + count++; + } + return count; + } + /** + * Print installation summary + * @param {Object} results - Installation results + * @param {string} targetDir - Target directory (relative) + */ + async printSummary(results, targetDir, options = {}) { + if (options.silent) return; + const count = results.skillDirectories || results.skills || 0; + if (count > 0) { + await prompts.log.success(`${this.name} configured: ${count} skills → ${targetDir}`); + } + const cmd = results.commands; + if (cmd && (cmd.created > 0 || cmd.updated > 0) && this.installerConfig?.commands_target_dir) { + const total = cmd.created + cmd.updated; + const detail = cmd.updated > 0 ? `${cmd.created} new, ${cmd.updated} refreshed` : `${total}`; + await prompts.log.success(`${this.name} commands: ${detail} → ${this.installerConfig.commands_target_dir}`); + if (cmd.skippedCollision > 0) { + await prompts.log.message(` (${cmd.skippedCollision} skipped — name collides with reserved slash command)`); + } + if (cmd.writeFailures > 0) { + await prompts.log.warn(` (${cmd.writeFailures} pointer writes failed — see warnings above)`); + } + } + } + /** + * Cleanup IDE configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir, options = {}, bmadDir = null) { + const resolvedBmadDir = bmadDir || await this._findBmadDir(projectDir); + let removalSet; + if (options.previousSkillIds) { + removalSet = new Set(options.previousSkillIds); + if (resolvedBmadDir) { + const removals = await this.loadRemovalLists(resolvedBmadDir); + for (const entry of removals) removalSet.add(entry); + } + } else if (resolvedBmadDir) { + removalSet = await this._buildUninstallSet(resolvedBmadDir); + } else { + removalSet = /* @__PURE__ */ new Set(); + } + if (this.name === "github-copilot") { + await this.cleanupCopilotInstructions(projectDir, options); + } + if (this.name === "kilo") { + await this.cleanupKiloModes(projectDir, options); + } + if (this.name === "rovo-dev") { + await this.cleanupRovoDevPrompts(projectDir, options); + } + if (this.installerConfig?.commands_target_dir) { + const isInstallFlow = !!options.previousSkillIds; + const activeSkillIds = isInstallFlow ? await this._readActiveSkillIds(resolvedBmadDir) : /* @__PURE__ */ new Set(); + const extension = this.installerConfig.commands_extension || ".md"; + await this.cleanupCommandPointers( + projectDir, + this.installerConfig.commands_target_dir, + options, + removalSet, + activeSkillIds, + extension + ); + } + if (options.skipTarget) return; + if (this.installerConfig?.target_dir) { + await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet); + } + } + /** + * Find the _bmad directory in a project + * @param {string} projectDir - Project directory + * @returns {string|null} Path to bmad dir or null + */ + async _findBmadDir(projectDir) { + const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); + return await fs.pathExists(bmadDir) ? bmadDir : null; + } + /** + * Build the full set of entries to remove for uninstall. + * Reads skill-manifest.csv to know exactly what was installed, plus removal lists. + * @param {string} bmadDir - BMAD installation directory + * @returns {Set} Set of entries to remove + */ + async _buildUninstallSet(bmadDir) { + const removals = await this.loadRemovalLists(bmadDir); + const csvPath = path.join(bmadDir, "_config", "skill-manifest.csv"); + try { + if (await fs.pathExists(csvPath)) { + const content = await fs.readFile(csvPath, "utf8"); + const records = csv.parse(content, { columns: true, skip_empty_lines: true }); + for (const record of records) { + if (record.canonicalId) { + removals.add(record.canonicalId); + } + } + } + } catch { + } + return removals; + } + /** + * Load removal lists from all module sources in the bmad directory. + * Each module can have an optional removals.txt listing entries to remove. + * @param {string} bmadDir - BMAD installation directory + * @returns {Set} Set of entries to remove + */ + async loadRemovalLists(bmadDir) { + const removals = /* @__PURE__ */ new Set(); + const { getProjectRoot } = require_project_root(); + const projectRemovalsPath = path.join(getProjectRoot(), "removals.txt"); + await this._readRemovalFile(projectRemovalsPath, removals); + try { + const entries = await fs.readdir(bmadDir); + for (const entry of entries) { + if (entry.startsWith("_")) continue; + const removalPath = path.join(bmadDir, entry, "removals.txt"); + await this._readRemovalFile(removalPath, removals); + } + } catch { + } + return removals; + } + /** + * Read a removals.txt file and add entries to the set + * @param {string} filePath - Path to removals.txt + * @param {Set} removals - Set to add entries to + */ + async _readRemovalFile(filePath, removals) { + try { + if (await fs.pathExists(filePath)) { + const content = await fs.readFile(filePath, "utf8"); + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith("#")) { + removals.add(trimmed); + } + } + } + } catch { + } + } + /** + * Cleanup generated command pointer files for entries in removalSet. + * Symmetric counterpart to installCommandPointers — removes + * `` files whose canonicalId is in the set. Removes + * the commands directory entirely if it ends up empty. + * @param {string} projectDir + * @param {string} commandsTargetDir - Relative dir (e.g. .opencode/commands) + * @param {Object} options + * @param {Set} removalSet - canonicalIds whose pointer files to remove + * @param {Set} [activeSkillIds] - canonicalIds present in the + * current manifest. Pointers for IDs in this set are spared so an + * install-flow cleanup (where removalSet === previousSkillIds and the + * same skills are about to be re-installed) doesn't wipe hand-edited + * pointer files. Pass an empty set or omit to delete every match in + * removalSet (uninstall flow). + * @param {string} [extension] - Pointer file extension (default '.md'); + * matches the platform's commands_extension config value so cleanup + * correctly identifies pointer files for IDEs whose convention isn't .md + * (e.g. Copilot's `.agent.md`). + */ + async cleanupCommandPointers(projectDir, commandsTargetDir, options = {}, removalSet = /* @__PURE__ */ new Set(), activeSkillIds = /* @__PURE__ */ new Set(), extension = ".md") { + if (!removalSet || removalSet.size === 0) return; + const commandsPath = path.join(projectDir, commandsTargetDir); + if (!await fs.pathExists(commandsPath)) return; + let entries; + try { + entries = await fs.readdir(commandsPath); + } catch { + return; + } + for (const entry of entries) { + if (!entry.endsWith(extension)) continue; + const canonicalId = entry.slice(0, -extension.length); + if (!removalSet.has(canonicalId)) continue; + if (activeSkillIds.has(canonicalId)) continue; + try { + await fs.remove(path.join(commandsPath, entry)); + } catch { + } + } + try { + const remaining = await fs.readdir(commandsPath); + if (remaining.length === 0) { + await fs.remove(commandsPath); + } + } catch { + } + } + /** + * Read the canonicalIds currently present in the skill-manifest.csv. + * Used by cleanup to distinguish "re-install of an existing skill" + * (preserve pointer) from "skill truly being removed" (delete pointer). + * @param {string|null} bmadDir + * @returns {Promise>} + */ + async _readActiveSkillIds(bmadDir) { + const ids = /* @__PURE__ */ new Set(); + if (!bmadDir) return ids; + const csvPath = path.join(bmadDir, "_config", "skill-manifest.csv"); + if (!await fs.pathExists(csvPath)) return ids; + try { + const content = await fs.readFile(csvPath, "utf8"); + const records = csv.parse(content, { columns: true, skip_empty_lines: true }); + for (const record of records) { + if (record.canonicalId) ids.add(record.canonicalId); + } + } catch { + } + return ids; + } + /** + * Cleanup a specific target directory. + * When removalSet is provided, only removes entries in that set. + * When removalSet is null (legacy dirs), removes all bmad-prefixed entries. + * @param {string} projectDir - Project directory + * @param {string} targetDir - Target directory to clean + * @param {Object} options - Cleanup options + * @param {Set|null} removalSet - Entries to remove, or null for legacy prefix matching + */ + async cleanupTarget(projectDir, targetDir, options = {}, removalSet = /* @__PURE__ */ new Set()) { + const targetPath = path.join(projectDir, targetDir); + if (!await fs.pathExists(targetPath)) { + return; + } + if (removalSet && removalSet.size === 0) { + return; + } + let entries; + try { + entries = await fs.readdir(targetPath); + } catch { + return; + } + if (!entries || !Array.isArray(entries)) { + return; + } + let removedCount = 0; + for (const entry of entries) { + if (!entry || typeof entry !== "string") continue; + if (entry.startsWith("bmad-os-")) continue; + const shouldRemove = removalSet ? removalSet.has(entry) : isBmadOwnedEntry(entry, null); + if (shouldRemove) { + try { + await fs.remove(path.join(targetPath, entry)); + removedCount++; + } catch { + } + } + } + if (removedCount > 0) { + try { + const remaining = await fs.readdir(targetPath); + if (remaining.length === 0) { + await fs.remove(targetPath); + } + } catch { + } + } + } + /** + * Strip BMAD-owned content from .github/copilot-instructions.md. + * The old custom installer injected content between and markers. + * Deletes the file if nothing remains. Restores .bak backup if one exists. + */ + async cleanupCopilotInstructions(projectDir, options = {}) { + const filePath = path.join(projectDir, ".github", "copilot-instructions.md"); + if (!await fs.pathExists(filePath)) return; + try { + const content = await fs.readFile(filePath, "utf8"); + const startIdx = content.indexOf(""); + const endIdx = content.indexOf(""); + if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return; + const cleaned = content.slice(0, startIdx) + content.slice(endIdx + "".length); + if (cleaned.trim().length === 0) { + await fs.remove(filePath); + const backupPath = `${filePath}.bak`; + if (await fs.pathExists(backupPath)) { + await fs.rename(backupPath, filePath); + if (!options.silent) await prompts.log.message(" Restored copilot-instructions.md from backup"); + } + } else { + await fs.writeFile(filePath, cleaned, "utf8"); + const backupPath = `${filePath}.bak`; + if (await fs.pathExists(backupPath)) await fs.remove(backupPath); + } + if (!options.silent) await prompts.log.message(" Cleaned BMAD markers from copilot-instructions.md"); + } catch { + if (!options.silent) await prompts.log.warn(" Warning: Could not clean BMAD markers from copilot-instructions.md"); + } + } + /** + * Strip BMAD-owned modes from .kilocodemodes. + * The old custom kilo.js installer added modes with slug starting with 'bmad-'. + * Parses YAML, filters out BMAD modes, rewrites. Leaves file as-is on parse failure. + */ + async cleanupKiloModes(projectDir, options = {}) { + const kiloModesPath = path.join(projectDir, ".kilocodemodes"); + if (!await fs.pathExists(kiloModesPath)) return; + const content = await fs.readFile(kiloModesPath, "utf8"); + let config; + try { + config = yaml.parse(content) || {}; + } catch { + if (!options.silent) await prompts.log.warn(" Warning: Could not parse .kilocodemodes for cleanup"); + return; + } + if (!Array.isArray(config.customModes)) return; + const originalCount = config.customModes.length; + config.customModes = config.customModes.filter((mode) => mode && (!mode.slug || !mode.slug.startsWith("bmad-"))); + const removedCount = originalCount - config.customModes.length; + if (removedCount > 0) { + try { + await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); + if (!options.silent) await prompts.log.message(` Removed ${removedCount} BMAD modes from .kilocodemodes`); + } catch { + if (!options.silent) await prompts.log.warn(" Warning: Could not write .kilocodemodes during cleanup"); + } + } + } + /** + * Strip BMAD-owned entries from .rovodev/prompts.yml. + * The old custom rovodev.js installer registered workflows in prompts.yml. + * Parses YAML, filters out entries with name starting with 'bmad-', rewrites. + * Removes the file if no entries remain. + */ + async cleanupRovoDevPrompts(projectDir, options = {}) { + const promptsPath = path.join(projectDir, ".rovodev", "prompts.yml"); + if (!await fs.pathExists(promptsPath)) return; + const content = await fs.readFile(promptsPath, "utf8"); + let config; + try { + config = yaml.parse(content) || {}; + } catch { + if (!options.silent) await prompts.log.warn(" Warning: Could not parse prompts.yml for cleanup"); + return; + } + if (!Array.isArray(config.prompts)) return; + const originalCount = config.prompts.length; + config.prompts = config.prompts.filter((entry) => entry && (!entry.name || !entry.name.startsWith("bmad-"))); + const removedCount = originalCount - config.prompts.length; + if (removedCount > 0) { + try { + if (config.prompts.length === 0) { + await fs.remove(promptsPath); + } else { + await fs.writeFile(promptsPath, yaml.stringify(config, { lineWidth: 0 })); + } + if (!options.silent) await prompts.log.message(` Removed ${removedCount} BMAD entries from prompts.yml`); + } catch { + if (!options.silent) await prompts.log.warn(" Warning: Could not write prompts.yml during cleanup"); + } + } + } + /** + * Check ancestor directories for existing BMAD files in the same target_dir. + * IDEs like Claude Code inherit commands from parent directories, so an existing + * installation in an ancestor would cause duplicate commands. + * @param {string} projectDir - Project directory being installed to + * @returns {Promise} Path to conflicting directory, or null if clean + */ + async findAncestorConflict(projectDir) { + const targetDir = this.installerConfig?.target_dir; + if (!targetDir) return null; + const resolvedProject = await fs.realpath(path.resolve(projectDir)); + let current = path.dirname(resolvedProject); + const root = path.parse(current).root; + while (current !== root && current.length > root.length) { + const candidatePath = path.join(current, targetDir); + try { + if (await fs.pathExists(candidatePath)) { + const entries = await fs.readdir(candidatePath); + const ancestorBmadDir = await this._findBmadDir(current); + const canonicalIds = await getInstalledCanonicalIds(ancestorBmadDir); + if (entries.some((e) => isBmadOwnedEntry(e, canonicalIds))) { + return candidatePath; + } + } + } catch { + } + current = path.dirname(current); + } + return null; + } + }; + module.exports = { ConfigDrivenIdeSetup }; + } +}); + +// tools/installer/ide/manager.js +var require_manager = __commonJS({ + "tools/installer/ide/manager.js"(exports, module) { + var { BMAD_FOLDER_NAME } = require_path_utils(); + var prompts = require_prompts(); + var IdeManager = class { + constructor() { + this.handlers = /* @__PURE__ */ new Map(); + this._initialized = false; + this.bmadFolderName = BMAD_FOLDER_NAME; + } + /** + * Set the bmad folder name for all IDE handlers + * @param {string} bmadFolderName - The bmad folder name + */ + setBmadFolderName(bmadFolderName) { + this.bmadFolderName = bmadFolderName; + for (const handler of this.handlers.values()) { + if (typeof handler.setBmadFolderName === "function") { + handler.setBmadFolderName(bmadFolderName); + } + } + } + /** + * Ensure handlers are loaded (lazy loading) + */ + async ensureInitialized() { + if (!this._initialized) { + await this.loadHandlers(); + this._initialized = true; + } + } + /** + * Dynamically load all IDE handlers from platform-codes.yaml + */ + async loadHandlers() { + await this.loadConfigDrivenHandlers(); + } + /** + * Load config-driven handlers from platform-codes.yaml + * This creates ConfigDrivenIdeSetup instances for platforms with installer config + */ + async loadConfigDrivenHandlers() { + const { loadPlatformCodes } = require_platform_codes(); + const platformConfig = await loadPlatformCodes(); + const { ConfigDrivenIdeSetup } = require_config_driven(); + for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) { + if (!platformInfo.installer) continue; + const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); + if (typeof handler.setBmadFolderName === "function") { + handler.setBmadFolderName(this.bmadFolderName); + } + this.handlers.set(platformCode, handler); + } + } + /** + * Get all available IDEs with their metadata + * @returns {Array} Array of IDE information objects + */ + getAvailableIdes() { + const ides = []; + for (const [key, handler] of this.handlers) { + const name = handler.displayName || handler.name || key; + if (!key || !name || typeof key !== "string" || typeof name !== "string") { + continue; + } + if (handler.platformConfig?.suspended) { + continue; + } + ides.push({ + value: key, + name, + preferred: handler.preferred || false + }); + } + ides.sort((a, b) => { + if (a.preferred && !b.preferred) return -1; + if (!a.preferred && b.preferred) return 1; + return a.name.localeCompare(b.name); + }); + return ides; + } + /** + * Get preferred IDEs + * @returns {Array} Array of preferred IDE information + */ + getPreferredIdes() { + return this.getAvailableIdes().filter((ide) => ide.preferred); + } + /** + * Get non-preferred IDEs + * @returns {Array} Array of non-preferred IDE information + */ + getOtherIdes() { + return this.getAvailableIdes().filter((ide) => !ide.preferred); + } + /** + * Setup IDE configuration + * @param {string} ideName - Name of the IDE + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + */ + async setup(ideName, projectDir, bmadDir, options = {}) { + const handler = this.handlers.get(ideName.toLowerCase()); + if (!handler) { + await prompts.log.warn(`IDE '${ideName}' is not yet supported`); + await prompts.log.message(`Supported IDEs: ${[...this.handlers.keys()].join(", ")}`); + return { success: false, ide: ideName, error: "unsupported IDE" }; + } + if (handler.platformConfig?.suspended) { + if (!options.silent) { + await prompts.log.warn(`${handler.displayName || ideName}: ${handler.platformConfig.suspended}`); + } + if (typeof handler.cleanup === "function") { + try { + await handler.cleanup(projectDir, { silent: true }); + } catch { + } + } + return { success: false, ide: ideName, error: "suspended" }; + } + try { + const handlerResult = await handler.setup(projectDir, bmadDir, options); + let detail = ""; + if (handlerResult && handlerResult.results) { + const r = handlerResult.results; + let count = r.skillDirectories || r.skills || 0; + if (count === 0 && r.sharedTargetHandledByPeer && options.sharedSkillCount) { + count = options.sharedSkillCount; + } + const targetDir = handler.installerConfig?.target_dir || null; + if (count > 0 && targetDir) { + detail = `${count} skills → ${targetDir}`; + } else if (count > 0) { + detail = `${count} skills`; + } + } + const success = handlerResult?.success !== false; + return { success, ide: ideName, detail, error: handlerResult?.error, handlerResult }; + } catch (error) { + await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); + return { success: false, ide: ideName, error: error.message }; + } + } + /** + * Run setup for multiple IDEs as a single batch. + * Dedupes work when several selected platforms share the same target_dir: + * the first platform owns the directory write, peers skip it. + * @param {Array} ideList - IDE names to set up + * @param {string} projectDir + * @param {string} bmadDir + * @param {Object} [options] - Forwarded to each handler.setup + * @returns {Promise} Per-IDE results + */ + async setupBatch(ideList, projectDir, bmadDir, options = {}) { + await this.ensureInitialized(); + const results = []; + const claimedTargets = /* @__PURE__ */ new Map(); + for (const ideName of ideList) { + const handler = this.handlers.get(ideName.toLowerCase()); + if (!handler) { + results.push(await this.setup(ideName, projectDir, bmadDir, options)); + continue; + } + const target = handler.installerConfig?.target_dir || null; + const claim = target ? claimedTargets.get(target) : null; + const skipTarget = !!claim; + const result = await this.setup(ideName, projectDir, bmadDir, { + ...options, + skipTarget, + sharedWith: claim?.firstIde || null, + sharedTarget: target, + sharedSkillCount: claim?.skillCount || 0 + }); + if (target && !claim) { + const writtenCount = result.handlerResult?.results?.skillDirectories || result.handlerResult?.results?.skills || 0; + if (result.success && writtenCount > 0) { + claimedTargets.set(target, { firstIde: ideName, skillCount: writtenCount }); + } + } + results.push(result); + } + return results; + } + /** + * Cleanup IDE configurations + * @param {string} projectDir - Project directory + * @param {Object} [options] - Cleanup options passed through to handlers + */ + async cleanup(projectDir, options = {}) { + const results = []; + for (const [name, handler] of this.handlers) { + try { + await handler.cleanup(projectDir, options); + results.push({ ide: name, success: true }); + } catch (error) { + results.push({ ide: name, success: false, error: error.message }); + } + } + return results; + } + /** + * Cleanup only the IDEs in the provided list + * Falls back to cleanup() (all handlers) if ideList is empty or undefined + * @param {string} projectDir - Project directory + * @param {Array} ideList - List of IDE names to clean up + * @param {Object} [options] - Cleanup options passed through to handlers + * options.remainingIdes - IDE names still installed after this cleanup; used + * to skip target_dir wipe when a co-installed platform shares the dir. + * @returns {Array} Results array + */ + async cleanupByList(projectDir, ideList, options = {}) { + if (!ideList || ideList.length === 0) { + return this.cleanup(projectDir, options); + } + await this.ensureInitialized(); + const results = []; + const lowercaseHandlers = new Map([...this.handlers.entries()].map(([k, v]) => [k.toLowerCase(), v])); + const remainingTargets = /* @__PURE__ */ new Set(); + if (Array.isArray(options.remainingIdes)) { + for (const remaining of options.remainingIdes) { + const h = lowercaseHandlers.get(String(remaining).toLowerCase()); + const t = h?.installerConfig?.target_dir; + if (t) remainingTargets.add(t); + } + } + for (const ideName of ideList) { + const handler = lowercaseHandlers.get(ideName.toLowerCase()); + if (!handler) continue; + const target = handler.installerConfig?.target_dir || null; + const skipTarget = target && remainingTargets.has(target); + const cleanupOptions = skipTarget ? { ...options, skipTarget: true } : options; + try { + await handler.cleanup(projectDir, cleanupOptions); + results.push({ ide: ideName, success: true, skippedTarget: !!skipTarget }); + } catch (error) { + results.push({ ide: ideName, success: false, error: error.message }); + } + } + return results; + } + /** + * Detect installed IDEs + * @param {string} projectDir - Project directory + * @returns {Array} List of detected IDEs + */ + async detectInstalledIdes(projectDir) { + const detected = []; + for (const [name, handler] of this.handlers) { + if (typeof handler.detect === "function" && await handler.detect(projectDir)) { + detected.push(name); + } + } + return detected; + } + }; + module.exports = { IdeManager }; + } +}); + +// tools/installer/core/ide-sync.js +var require_ide_sync = __commonJS({ + "tools/installer/core/ide-sync.js"(exports, module) { + var path = __require("node:path"); + var fs = require_fs_native(); + var { IdeManager } = require_manager(); + var { BMAD_FOLDER_NAME } = require_path_utils(); + var writeOut = (m) => process.stdout.write(`${m} +`); + var writeErr = (m) => process.stderr.write(`${m} +`); + var DEFAULT_LOGGER = { info: writeOut, warn: writeErr, error: writeErr }; + async function syncIdes({ projectRoot, bmadDir, ides, previousSkillIds = [], verbose = false, cleanup = true, silent = false }) { + const validIdes = (ides || []).filter((ide) => ide && typeof ide === "string"); + if (validIdes.length === 0) return { skipped: true, results: [] }; + const ideManager = new IdeManager(); + ideManager.setBmadFolderName(path.basename(bmadDir)); + await ideManager.ensureInitialized(); + const results = await ideManager.setupBatch(validIdes, projectRoot, bmadDir, { + previousSkillIds: new Set(previousSkillIds), + verbose, + silent + }); + const allSucceeded = results.every((r) => r && r.success); + if (cleanup && allSucceeded) await cleanupBmadSkillDirs(bmadDir); + return { skipped: false, results }; + } + async function cleanupBmadSkillDirs(bmadDir) { + const csv = require_sync(); + const csvPath = path.join(bmadDir, "_config", "skill-manifest.csv"); + if (!await fs.pathExists(csvPath)) return; + const csvContent = await fs.readFile(csvPath, "utf8"); + const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true }); + const bmadFolderName = path.basename(bmadDir); + const bmadPrefix = bmadFolderName + "/"; + const bmadRoot = path.resolve(bmadDir); + for (const record of records) { + if (!record.path) continue; + const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path; + const skillFilePath = path.resolve(bmadDir, relativePath); + if (skillFilePath !== bmadRoot && !skillFilePath.startsWith(bmadRoot + path.sep)) continue; + const sourceDir = path.dirname(skillFilePath); + if (sourceDir === bmadRoot) continue; + if (await fs.pathExists(sourceDir)) { + await fs.remove(sourceDir); + await removeEmptyParents(path.dirname(sourceDir), bmadDir); + } + } + } + async function removeEmptyParents(dir, bmadDir) { + let current = dir; + while (true) { + const rel = path.relative(bmadDir, current); + if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) break; + try { + const entries = await fs.readdir(current); + if (entries.length > 0) break; + await fs.rmdir(current); + } catch { + break; + } + current = path.dirname(current); + } + } + async function readSelectedIdes(bmadDir) { + const yaml = require_dist(); + const manifestPath = path.join(bmadDir, "_config", "manifest.yaml"); + if (!await fs.pathExists(manifestPath)) return []; + try { + const parsed = yaml.parse(await fs.readFile(manifestPath, "utf8")); + return Array.isArray(parsed?.ides) ? parsed.ides.filter((i) => i && typeof i === "string") : []; + } catch { + return []; + } + } + async function runIdeSync(opts = {}) { + const logger = opts.logger || DEFAULT_LOGGER; + const projectRoot = path.resolve(opts.directory || "."); + const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME); + if (!await fs.pathExists(bmadDir)) { + logger.error(`[ide-sync] no BMAD installation (_bmad/) found in ${projectRoot}. Run \`bmad install\` first.`); + return 2; + } + const ides = await readSelectedIdes(bmadDir); + if (ides.length === 0) { + logger.info("[ide-sync] no IDEs configured in manifest.yaml — nothing to distribute."); + return 0; + } + const previousSkillIds = normalizeIdList(opts.prune); + const { results } = await syncIdes({ + projectRoot, + bmadDir, + ides, + previousSkillIds, + verbose: !!opts.verbose, + // Standalone path prints its own concise [ide-sync] lines; suppress the + // engine's interactive-style status output (errors still surface). + silent: true + }); + let failed = 0; + for (const r of results) { + if (r.success) { + logger.info(`[ide-sync] ${r.ide}: ${r.detail || "configured"}`); + } else { + failed++; + logger.error(`[ide-sync] ${r.ide}: FAILED — ${r.error || "unknown error"}`); + } + } + return failed > 0 ? 1 : 0; + } + function normalizeIdList(value) { + if (!value) return []; + const arr = Array.isArray(value) ? value : String(value).split(","); + return arr.map((s) => String(s).trim()).filter(Boolean); + } + async function runIdeSyncCli2(argv = []) { + const opts = { directory: ".", prune: "", verbose: false }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith("--directory=")) { + opts.directory = a.slice("--directory=".length); + continue; + } + if (a.startsWith("--prune=")) { + opts.prune = a.slice("--prune=".length); + continue; + } + switch (a) { + case "-d": + case "--directory": { + opts.directory = argv[++i] ?? "."; + break; + } + case "--prune": { + opts.prune = argv[++i] ?? ""; + break; + } + case "-v": + case "--verbose": { + opts.verbose = true; + break; + } + default: { + break; + } + } + } + return runIdeSync(opts); + } + module.exports = { + syncIdes, + cleanupBmadSkillDirs, + readSelectedIdes, + runIdeSync, + runIdeSyncCli: runIdeSyncCli2 + }; + } +}); + +// tools/installer/ide-sync-entry.mjs +var import_ide_sync = __toESM(require_ide_sync(), 1); +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +var __dir = dirname(fileURLToPath(import.meta.url)); +if (!process.env.BMAD_IDE_PLATFORM_CODES) { + process.env.BMAD_IDE_PLATFORM_CODES = join(__dir, "platform-codes.yaml"); +} +(0, import_ide_sync.runIdeSyncCli)(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => { + process.stderr.write("[ide-sync] " + (err && err.stack || err) + "\n"); + process.exit(1); +}); diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/platform-codes.yaml b/src/core-skills/bmad-module/scripts/lib/vendor/platform-codes.yaml new file mode 100644 index 000000000..2bde8b245 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/platform-codes.yaml @@ -0,0 +1,329 @@ +# BMAD Platform Codes Configuration +# +# Each platform entry has: +# name: Display name shown to users +# preferred: Whether shown as a recommended option on install +# suspended: (optional) Message explaining why install is blocked +# installer: +# target_dir: Directory where skill directories are installed (project/workspace) +# global_target_dir: (optional) User-home directory for global install +# ancestor_conflict_check: (optional) Refuse install when ancestor dir has BMAD files +# +# Multiple platforms may share the same target_dir or global_target_dir — many tools +# read from the shared `.agents/skills/` and `~/.agents/skills/` cross-tool standard. +# Paths verified against each tool's primary docs as of 2026-04-25. + +platforms: + adal: + name: "AdaL" + preferred: false + installer: + target_dir: .adal/skills + global_target_dir: ~/.adal/skills + + amp: + name: "Sourcegraph Amp" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.config/agents/skills + + antigravity: + name: "Google Antigravity" + preferred: false + installer: + target_dir: .agent/skills + global_target_dir: ~/.gemini/antigravity/skills + + auggie: + name: "Auggie" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + bob: + name: "IBM Bob" + preferred: false + installer: + target_dir: .bob/skills + global_target_dir: ~/.bob/skills + + claude-code: + name: "Claude Code" + preferred: true + installer: + target_dir: .claude/skills + global_target_dir: ~/.claude/skills + + cline: + name: "Cline" + preferred: false + installer: + target_dir: .cline/skills + global_target_dir: ~/.cline/skills + + codex: + name: "Codex" + preferred: true + installer: + target_dir: .agents/skills + global_target_dir: ~/.codex/skills + + codewhale: + name: "CodeWhale" + preferred: false + installer: + target_dir: .codewhale/skills + global_target_dir: ~/.codewhale/skills + + codebuddy: + name: "CodeBuddy" + preferred: false + installer: + target_dir: .codebuddy/skills + global_target_dir: ~/.codebuddy/skills + + command-code: + name: "Command Code" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + cortex: + name: "Snowflake Cortex Code" + preferred: false + installer: + target_dir: .cortex/skills + global_target_dir: ~/.snowflake/cortex/skills + + crush: + name: "Crush" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.config/agents/skills + + cursor: + name: "Cursor" + preferred: true + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + droid: + name: "Factory Droid" + preferred: false + installer: + target_dir: .factory/skills + global_target_dir: ~/.factory/skills + + firebender: + name: "Firebender" + preferred: false + installer: + target_dir: .firebender/skills + global_target_dir: ~/.agents/skills + + gemini: + name: "Gemini CLI" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + github-copilot: + name: "GitHub Copilot" + preferred: true + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + commands_target_dir: .github/agents + commands_extension: .agent.md + commands_body_template: "LOAD the FULL {project-root}/{target_dir}/{canonicalId}/SKILL.md, READ its entire contents and follow its directions exactly!" + # The Custom Agents picker should only show persona agents (not + # workflows/tools). Detected by reading each skill's source + # `customize.toml` and checking for an `[agent]` section — that's + # the actual configuration source of truth: every BMAD persona is + # configured under `[agent]`, every workflow under `[workflow]`, + # every standalone skill has no customize.toml. This signal is + # naming-independent, so personas like `bmad-tea` (which doesn't + # follow the `-agent-` convention) are still included, and + # meta-skills like `bmad-agent-builder` (which contains `-agent-` + # but is a skill-builder workflow, not a persona) are correctly + # excluded. + commands_filter: agents-only + + goose: + name: "Block Goose" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.config/agents/skills + + iflow: + name: "iFlow" + preferred: false + installer: + target_dir: .iflow/skills + global_target_dir: ~/.iflow/skills + + junie: + name: "Junie" + preferred: false + installer: + target_dir: .junie/skills + global_target_dir: ~/.junie/skills + + kilo: + name: "KiloCoder" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.kilocode/skills + + kimi-code: + name: "Kimi Code" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + kiro: + name: "Kiro" + preferred: false + installer: + target_dir: .kiro/skills + global_target_dir: ~/.kiro/skills + + kode: + name: "Kode" + preferred: false + installer: + target_dir: .kode/skills + global_target_dir: ~/.kode/skills + + mistral-vibe: + name: "Mistral Vibe" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.vibe/skills + + mux: + name: "Mux" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + neovate: + name: "Neovate" + preferred: false + installer: + target_dir: .neovate/skills + global_target_dir: ~/.neovate/skills + + ona: + name: "Ona" + preferred: false + installer: + target_dir: .ona/skills + + openclaw: + name: "OpenClaw" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + opencode: + name: "OpenCode" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + commands_target_dir: .opencode/commands + + openhands: + name: "OpenHands" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + pi: + name: "Pi" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + pochi: + name: "Pochi" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + qoder: + name: "Qoder" + preferred: false + installer: + target_dir: .qoder/skills + global_target_dir: ~/.qoder/skills + + qwen: + name: "QwenCoder" + preferred: false + installer: + target_dir: .qwen/skills + global_target_dir: ~/.qwen/skills + + replit: + name: "Replit Agent" + preferred: false + installer: + target_dir: .agents/skills + + roo: + name: "Roo Code" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + rovo-dev: + name: "Rovo Dev" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + trae: + name: "Trae" + preferred: false + installer: + target_dir: .trae/skills + + warp: + name: "Warp" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + windsurf: + name: "Windsurf" + preferred: false + installer: + target_dir: .agents/skills + global_target_dir: ~/.agents/skills + + zencoder: + name: "Zencoder" + preferred: false + installer: + target_dir: .zencoder/skills + global_target_dir: ~/.zencoder/skills diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/shims/project-root.cjs b/src/core-skills/bmad-module/scripts/lib/vendor/shims/project-root.cjs new file mode 100644 index 000000000..bf894c8f8 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/shims/project-root.cjs @@ -0,0 +1,18 @@ +// Build-time shim for tools/installer/project-root.js, injected into the +// ide-sync bundle. The full module reaches into custom-module-manager and the +// rest of the installer; the IDE engine only needs getProjectRoot() (to locate +// an optional project-level removals.txt). In the bundle, the project root is +// the cwd the bmad-module skill runs the bundle from. +'use strict'; + +const path = require('node:path'); + +function getProjectRoot() { + return process.cwd(); +} + +function getSourcePath(...segments) { + return path.join(process.cwd(), ...segments); +} + +module.exports = { getProjectRoot, getSourcePath }; diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/shims/prompts.cjs b/src/core-skills/bmad-module/scripts/lib/vendor/shims/prompts.cjs new file mode 100644 index 000000000..07d08f5ad --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/shims/prompts.cjs @@ -0,0 +1,53 @@ +// Build-time shim for tools/installer/prompts.js, injected into the ide-sync +// bundle so the heavyweight interactive @clack/prompts dependency is never +// pulled in. The IDE engine only uses `prompts.log.*` for status output; that +// maps to plain stdout/stderr here. Interactive helpers throw if reached (they +// must not be during non-interactive distribution). +'use strict'; + +const out = (m) => process.stdout.write(`${m}\n`); +const err = (m) => process.stderr.write(`${m}\n`); + +const log = { + info: async (m) => out(m), + success: async (m) => out(m), + message: async (m) => out(m), + step: async (m) => out(m), + warn: async (m) => err(m), + error: async (m) => err(m), +}; + +const notInteractive = () => { + throw new Error('interactive prompt is not available in the ide-sync bundle'); +}; + +// Identity color helper: every method returns its input unchanged. +const identityColor = new Proxy( + {}, + { + get: () => (s) => s, + }, +); + +module.exports = { + log, + getColor: async () => identityColor, + spinner: () => ({ start() {}, stop() {}, message() {} }), + tasks: async () => {}, + note: async (m) => out(m), + box: async (m) => out(m), + intro: async () => {}, + outro: async () => {}, + cancel: async () => {}, + handleCancel: async () => {}, + getClack: notInteractive, + select: notInteractive, + multiselect: notInteractive, + autocomplete: notInteractive, + autocompleteMultiselect: notInteractive, + directory: notInteractive, + confirm: notInteractive, + text: notInteractive, + password: notInteractive, + prompt: notInteractive, +}; diff --git a/src/core-skills/bmad-module/scripts/lib/vendor/yaml.mjs b/src/core-skills/bmad-module/scripts/lib/vendor/yaml.mjs new file mode 100644 index 000000000..23c9fd648 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/lib/vendor/yaml.mjs @@ -0,0 +1,7362 @@ +// ============================================================================ +// GENERATED — DO NOT EDIT BY HAND. Run `npm run vendor:build` to regenerate. +// Vendored, self-contained bundle of the `yaml` npm package (eemeli/yaml). +// +// yaml : 2.8.4 +// bundler : esbuild 0.25.12 +// +// 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 +// `stringify` are re-exported (tree-shaken). Upstream license retained below. +// ============================================================================ +import { createRequire as __createRequire } from 'node:module'; +const require = __createRequire(import.meta.url); + +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] +}) : x)(function(x) { + if (typeof require !== "undefined") return require.apply(this, arguments); + throw Error('Dynamic require of "' + x + '" is not supported'); +}); +var __commonJS = (cb, mod) => function __require2() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// node_modules/yaml/dist/nodes/identity.js +var require_identity = __commonJS({ + "node_modules/yaml/dist/nodes/identity.js"(exports) { + "use strict"; + var ALIAS = Symbol.for("yaml.alias"); + var DOC = Symbol.for("yaml.document"); + var MAP = Symbol.for("yaml.map"); + var PAIR = Symbol.for("yaml.pair"); + var SCALAR = Symbol.for("yaml.scalar"); + var SEQ = Symbol.for("yaml.seq"); + var NODE_TYPE = Symbol.for("yaml.node.type"); + var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS; + var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC; + var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP; + var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR; + var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR; + var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ; + function isCollection(node) { + if (node && typeof node === "object") + switch (node[NODE_TYPE]) { + case MAP: + case SEQ: + return true; + } + return false; + } + function isNode(node) { + if (node && typeof node === "object") + switch (node[NODE_TYPE]) { + case ALIAS: + case MAP: + case SCALAR: + case SEQ: + return true; + } + return false; + } + var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor; + exports.ALIAS = ALIAS; + exports.DOC = DOC; + exports.MAP = MAP; + exports.NODE_TYPE = NODE_TYPE; + exports.PAIR = PAIR; + exports.SCALAR = SCALAR; + exports.SEQ = SEQ; + exports.hasAnchor = hasAnchor; + exports.isAlias = isAlias; + exports.isCollection = isCollection; + exports.isDocument = isDocument; + exports.isMap = isMap; + exports.isNode = isNode; + exports.isPair = isPair; + exports.isScalar = isScalar; + exports.isSeq = isSeq; + } +}); + +// node_modules/yaml/dist/visit.js +var require_visit = __commonJS({ + "node_modules/yaml/dist/visit.js"(exports) { + "use strict"; + var identity = require_identity(); + var BREAK = Symbol("break visit"); + var SKIP = Symbol("skip children"); + var REMOVE = Symbol("remove node"); + function visit(node, visitor) { + const visitor_ = initVisitor(visitor); + if (identity.isDocument(node)) { + const cd = visit_(null, node.contents, visitor_, Object.freeze([node])); + if (cd === REMOVE) + node.contents = null; + } else + visit_(null, node, visitor_, Object.freeze([])); + } + visit.BREAK = BREAK; + visit.SKIP = SKIP; + visit.REMOVE = REMOVE; + function visit_(key, node, visitor, path) { + const ctrl = callVisitor(key, node, visitor, path); + if (identity.isNode(ctrl) || identity.isPair(ctrl)) { + replaceNode(key, path, ctrl); + return visit_(key, ctrl, visitor, path); + } + if (typeof ctrl !== "symbol") { + if (identity.isCollection(node)) { + path = Object.freeze(path.concat(node)); + for (let i = 0; i < node.items.length; ++i) { + const ci = visit_(i, node.items[i], visitor, path); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + node.items.splice(i, 1); + i -= 1; + } + } + } else if (identity.isPair(node)) { + path = Object.freeze(path.concat(node)); + const ck = visit_("key", node.key, visitor, path); + if (ck === BREAK) + return BREAK; + else if (ck === REMOVE) + node.key = null; + const cv = visit_("value", node.value, visitor, path); + if (cv === BREAK) + return BREAK; + else if (cv === REMOVE) + node.value = null; + } + } + return ctrl; + } + async function visitAsync(node, visitor) { + const visitor_ = initVisitor(visitor); + if (identity.isDocument(node)) { + const cd = await visitAsync_(null, node.contents, visitor_, Object.freeze([node])); + if (cd === REMOVE) + node.contents = null; + } else + await visitAsync_(null, node, visitor_, Object.freeze([])); + } + visitAsync.BREAK = BREAK; + visitAsync.SKIP = SKIP; + visitAsync.REMOVE = REMOVE; + async function visitAsync_(key, node, visitor, path) { + const ctrl = await callVisitor(key, node, visitor, path); + if (identity.isNode(ctrl) || identity.isPair(ctrl)) { + replaceNode(key, path, ctrl); + return visitAsync_(key, ctrl, visitor, path); + } + if (typeof ctrl !== "symbol") { + if (identity.isCollection(node)) { + path = Object.freeze(path.concat(node)); + for (let i = 0; i < node.items.length; ++i) { + const ci = await visitAsync_(i, node.items[i], visitor, path); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + node.items.splice(i, 1); + i -= 1; + } + } + } else if (identity.isPair(node)) { + path = Object.freeze(path.concat(node)); + const ck = await visitAsync_("key", node.key, visitor, path); + if (ck === BREAK) + return BREAK; + else if (ck === REMOVE) + node.key = null; + const cv = await visitAsync_("value", node.value, visitor, path); + if (cv === BREAK) + return BREAK; + else if (cv === REMOVE) + node.value = null; + } + } + return ctrl; + } + function initVisitor(visitor) { + if (typeof visitor === "object" && (visitor.Collection || visitor.Node || visitor.Value)) { + return Object.assign({ + Alias: visitor.Node, + Map: visitor.Node, + Scalar: visitor.Node, + Seq: visitor.Node + }, visitor.Value && { + Map: visitor.Value, + Scalar: visitor.Value, + Seq: visitor.Value + }, visitor.Collection && { + Map: visitor.Collection, + Seq: visitor.Collection + }, visitor); + } + return visitor; + } + function callVisitor(key, node, visitor, path) { + if (typeof visitor === "function") + return visitor(key, node, path); + if (identity.isMap(node)) + return visitor.Map?.(key, node, path); + if (identity.isSeq(node)) + return visitor.Seq?.(key, node, path); + if (identity.isPair(node)) + return visitor.Pair?.(key, node, path); + if (identity.isScalar(node)) + return visitor.Scalar?.(key, node, path); + if (identity.isAlias(node)) + return visitor.Alias?.(key, node, path); + return void 0; + } + function replaceNode(key, path, node) { + const parent = path[path.length - 1]; + if (identity.isCollection(parent)) { + parent.items[key] = node; + } else if (identity.isPair(parent)) { + if (key === "key") + parent.key = node; + else + parent.value = node; + } else if (identity.isDocument(parent)) { + parent.contents = node; + } else { + const pt = identity.isAlias(parent) ? "alias" : "scalar"; + throw new Error(`Cannot replace node with ${pt} parent`); + } + } + exports.visit = visit; + exports.visitAsync = visitAsync; + } +}); + +// node_modules/yaml/dist/doc/directives.js +var require_directives = __commonJS({ + "node_modules/yaml/dist/doc/directives.js"(exports) { + "use strict"; + var identity = require_identity(); + var visit = require_visit(); + var escapeChars = { + "!": "%21", + ",": "%2C", + "[": "%5B", + "]": "%5D", + "{": "%7B", + "}": "%7D" + }; + var escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, (ch) => escapeChars[ch]); + var Directives = class _Directives { + constructor(yaml, tags) { + this.docStart = null; + this.docEnd = false; + this.yaml = Object.assign({}, _Directives.defaultYaml, yaml); + this.tags = Object.assign({}, _Directives.defaultTags, tags); + } + clone() { + const copy = new _Directives(this.yaml, this.tags); + copy.docStart = this.docStart; + return copy; + } + /** + * During parsing, get a Directives instance for the current document and + * update the stream state according to the current version's spec. + */ + atDocument() { + const res = new _Directives(this.yaml, this.tags); + switch (this.yaml.version) { + case "1.1": + this.atNextDocument = true; + break; + case "1.2": + this.atNextDocument = false; + this.yaml = { + explicit: _Directives.defaultYaml.explicit, + version: "1.2" + }; + this.tags = Object.assign({}, _Directives.defaultTags); + break; + } + return res; + } + /** + * @param onError - May be called even if the action was successful + * @returns `true` on success + */ + add(line, onError) { + if (this.atNextDocument) { + this.yaml = { explicit: _Directives.defaultYaml.explicit, version: "1.1" }; + this.tags = Object.assign({}, _Directives.defaultTags); + this.atNextDocument = false; + } + const parts = line.trim().split(/[ \t]+/); + const name = parts.shift(); + switch (name) { + case "%TAG": { + if (parts.length !== 2) { + onError(0, "%TAG directive should contain exactly two parts"); + if (parts.length < 2) + return false; + } + const [handle, prefix] = parts; + this.tags[handle] = prefix; + return true; + } + case "%YAML": { + this.yaml.explicit = true; + if (parts.length !== 1) { + onError(0, "%YAML directive should contain exactly one part"); + return false; + } + const [version] = parts; + if (version === "1.1" || version === "1.2") { + this.yaml.version = version; + return true; + } else { + const isValid = /^\d+\.\d+$/.test(version); + onError(6, `Unsupported YAML version ${version}`, isValid); + return false; + } + } + default: + onError(0, `Unknown directive ${name}`, true); + return false; + } + } + /** + * Resolves a tag, matching handles to those defined in %TAG directives. + * + * @returns Resolved tag, which may also be the non-specific tag `'!'` or a + * `'!local'` tag, or `null` if unresolvable. + */ + tagName(source, onError) { + if (source === "!") + return "!"; + if (source[0] !== "!") { + onError(`Not a valid tag: ${source}`); + return null; + } + if (source[1] === "<") { + const verbatim = source.slice(2, -1); + if (verbatim === "!" || verbatim === "!!") { + onError(`Verbatim tags aren't resolved, so ${source} is invalid.`); + return null; + } + if (source[source.length - 1] !== ">") + onError("Verbatim tags must end with a >"); + return verbatim; + } + const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s); + if (!suffix) + onError(`The ${source} tag has no suffix`); + const prefix = this.tags[handle]; + if (prefix) { + try { + return prefix + decodeURIComponent(suffix); + } catch (error) { + onError(String(error)); + return null; + } + } + if (handle === "!") + return source; + onError(`Could not resolve tag: ${source}`); + return null; + } + /** + * Given a fully resolved tag, returns its printable string form, + * taking into account current tag prefixes and defaults. + */ + tagString(tag) { + for (const [handle, prefix] of Object.entries(this.tags)) { + if (tag.startsWith(prefix)) + return handle + escapeTagName(tag.substring(prefix.length)); + } + return tag[0] === "!" ? tag : `!<${tag}>`; + } + toString(doc) { + const lines = this.yaml.explicit ? [`%YAML ${this.yaml.version || "1.2"}`] : []; + const tagEntries = Object.entries(this.tags); + let tagNames; + if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) { + const tags = {}; + visit.visit(doc.contents, (_key, node) => { + if (identity.isNode(node) && node.tag) + tags[node.tag] = true; + }); + tagNames = Object.keys(tags); + } else + tagNames = []; + for (const [handle, prefix] of tagEntries) { + if (handle === "!!" && prefix === "tag:yaml.org,2002:") + continue; + if (!doc || tagNames.some((tn) => tn.startsWith(prefix))) + lines.push(`%TAG ${handle} ${prefix}`); + } + return lines.join("\n"); + } + }; + Directives.defaultYaml = { explicit: false, version: "1.2" }; + Directives.defaultTags = { "!!": "tag:yaml.org,2002:" }; + exports.Directives = Directives; + } +}); + +// node_modules/yaml/dist/doc/anchors.js +var require_anchors = __commonJS({ + "node_modules/yaml/dist/doc/anchors.js"(exports) { + "use strict"; + var identity = require_identity(); + var visit = require_visit(); + function anchorIsValid(anchor) { + if (/[\x00-\x19\s,[\]{}]/.test(anchor)) { + const sa = JSON.stringify(anchor); + const msg = `Anchor must not contain whitespace or control characters: ${sa}`; + throw new Error(msg); + } + return true; + } + function anchorNames(root) { + const anchors = /* @__PURE__ */ new Set(); + visit.visit(root, { + Value(_key, node) { + if (node.anchor) + anchors.add(node.anchor); + } + }); + return anchors; + } + function findNewAnchor(prefix, exclude) { + for (let i = 1; true; ++i) { + const name = `${prefix}${i}`; + if (!exclude.has(name)) + return name; + } + } + function createNodeAnchors(doc, prefix) { + const aliasObjects = []; + const sourceObjects = /* @__PURE__ */ new Map(); + let prevAnchors = null; + return { + onAnchor: (source) => { + aliasObjects.push(source); + prevAnchors ?? (prevAnchors = anchorNames(doc)); + const anchor = findNewAnchor(prefix, prevAnchors); + prevAnchors.add(anchor); + return anchor; + }, + /** + * With circular references, the source node is only resolved after all + * of its child nodes are. This is why anchors are set only after all of + * the nodes have been created. + */ + setAnchors: () => { + for (const source of aliasObjects) { + const ref = sourceObjects.get(source); + if (typeof ref === "object" && ref.anchor && (identity.isScalar(ref.node) || identity.isCollection(ref.node))) { + ref.node.anchor = ref.anchor; + } else { + const error = new Error("Failed to resolve repeated object (this should not happen)"); + error.source = source; + throw error; + } + } + }, + sourceObjects + }; + } + exports.anchorIsValid = anchorIsValid; + exports.anchorNames = anchorNames; + exports.createNodeAnchors = createNodeAnchors; + exports.findNewAnchor = findNewAnchor; + } +}); + +// node_modules/yaml/dist/doc/applyReviver.js +var require_applyReviver = __commonJS({ + "node_modules/yaml/dist/doc/applyReviver.js"(exports) { + "use strict"; + function applyReviver(reviver, obj, key, val) { + if (val && typeof val === "object") { + if (Array.isArray(val)) { + for (let i = 0, len = val.length; i < len; ++i) { + const v0 = val[i]; + const v1 = applyReviver(reviver, val, String(i), v0); + if (v1 === void 0) + delete val[i]; + else if (v1 !== v0) + val[i] = v1; + } + } else if (val instanceof Map) { + for (const k of Array.from(val.keys())) { + const v0 = val.get(k); + const v1 = applyReviver(reviver, val, k, v0); + if (v1 === void 0) + val.delete(k); + else if (v1 !== v0) + val.set(k, v1); + } + } else if (val instanceof Set) { + for (const v0 of Array.from(val)) { + const v1 = applyReviver(reviver, val, v0, v0); + if (v1 === void 0) + val.delete(v0); + else if (v1 !== v0) { + val.delete(v0); + val.add(v1); + } + } + } else { + for (const [k, v0] of Object.entries(val)) { + const v1 = applyReviver(reviver, val, k, v0); + if (v1 === void 0) + delete val[k]; + else if (v1 !== v0) + val[k] = v1; + } + } + } + return reviver.call(obj, key, val); + } + exports.applyReviver = applyReviver; + } +}); + +// node_modules/yaml/dist/nodes/toJS.js +var require_toJS = __commonJS({ + "node_modules/yaml/dist/nodes/toJS.js"(exports) { + "use strict"; + var identity = require_identity(); + function toJS(value, arg, ctx) { + if (Array.isArray(value)) + return value.map((v, i) => toJS(v, String(i), ctx)); + if (value && typeof value.toJSON === "function") { + if (!ctx || !identity.hasAnchor(value)) + return value.toJSON(arg, ctx); + const data = { aliasCount: 0, count: 1, res: void 0 }; + ctx.anchors.set(value, data); + ctx.onCreate = (res2) => { + data.res = res2; + delete ctx.onCreate; + }; + const res = value.toJSON(arg, ctx); + if (ctx.onCreate) + ctx.onCreate(res); + return res; + } + if (typeof value === "bigint" && !ctx?.keep) + return Number(value); + return value; + } + exports.toJS = toJS; + } +}); + +// node_modules/yaml/dist/nodes/Node.js +var require_Node = __commonJS({ + "node_modules/yaml/dist/nodes/Node.js"(exports) { + "use strict"; + var applyReviver = require_applyReviver(); + var identity = require_identity(); + var toJS = require_toJS(); + var NodeBase = class { + constructor(type) { + Object.defineProperty(this, identity.NODE_TYPE, { value: type }); + } + /** Create a copy of this node. */ + clone() { + const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this)); + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** A plain JavaScript representation of this node. */ + toJS(doc, { mapAsMap, maxAliasCount, onAnchor, reviver } = {}) { + if (!identity.isDocument(doc)) + throw new TypeError("A document argument is required"); + const ctx = { + anchors: /* @__PURE__ */ new Map(), + doc, + keep: true, + mapAsMap: mapAsMap === true, + mapKeyWarned: false, + maxAliasCount: typeof maxAliasCount === "number" ? maxAliasCount : 100 + }; + const res = toJS.toJS(this, "", ctx); + if (typeof onAnchor === "function") + for (const { count, res: res2 } of ctx.anchors.values()) + onAnchor(res2, count); + return typeof reviver === "function" ? applyReviver.applyReviver(reviver, { "": res }, "", res) : res; + } + }; + exports.NodeBase = NodeBase; + } +}); + +// node_modules/yaml/dist/nodes/Alias.js +var require_Alias = __commonJS({ + "node_modules/yaml/dist/nodes/Alias.js"(exports) { + "use strict"; + var anchors = require_anchors(); + var visit = require_visit(); + var identity = require_identity(); + var Node = require_Node(); + var toJS = require_toJS(); + var Alias = class extends Node.NodeBase { + constructor(source) { + super(identity.ALIAS); + this.source = source; + Object.defineProperty(this, "tag", { + set() { + throw new Error("Alias nodes cannot have tags"); + } + }); + } + /** + * Resolve the value of this alias within `doc`, finding the last + * instance of the `source` anchor before this node. + */ + resolve(doc, ctx) { + if (ctx?.maxAliasCount === 0) + throw new ReferenceError("Alias resolution is disabled"); + let nodes; + if (ctx?.aliasResolveCache) { + nodes = ctx.aliasResolveCache; + } else { + nodes = []; + visit.visit(doc, { + Node: (_key, node) => { + if (identity.isAlias(node) || identity.hasAnchor(node)) + nodes.push(node); + } + }); + if (ctx) + ctx.aliasResolveCache = nodes; + } + let found = void 0; + for (const node of nodes) { + if (node === this) + break; + if (node.anchor === this.source) + found = node; + } + return found; + } + toJSON(_arg, ctx) { + if (!ctx) + return { source: this.source }; + const { anchors: anchors2, doc, maxAliasCount } = ctx; + const source = this.resolve(doc, ctx); + if (!source) { + const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`; + throw new ReferenceError(msg); + } + let data = anchors2.get(source); + if (!data) { + toJS.toJS(source, null, ctx); + data = anchors2.get(source); + } + if (data?.res === void 0) { + const msg = "This should not happen: Alias anchor was not resolved?"; + throw new ReferenceError(msg); + } + if (maxAliasCount >= 0) { + data.count += 1; + if (data.aliasCount === 0) + data.aliasCount = getAliasCount(doc, source, anchors2); + if (data.count * data.aliasCount > maxAliasCount) { + const msg = "Excessive alias count indicates a resource exhaustion attack"; + throw new ReferenceError(msg); + } + } + return data.res; + } + toString(ctx, _onComment, _onChompKeep) { + const src = `*${this.source}`; + if (ctx) { + anchors.anchorIsValid(this.source); + if (ctx.options.verifyAliasOrder && !ctx.anchors.has(this.source)) { + const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`; + throw new Error(msg); + } + if (ctx.implicitKey) + return `${src} `; + } + return src; + } + }; + function getAliasCount(doc, node, anchors2) { + if (identity.isAlias(node)) { + const source = node.resolve(doc); + const anchor = anchors2 && source && anchors2.get(source); + return anchor ? anchor.count * anchor.aliasCount : 0; + } else if (identity.isCollection(node)) { + let count = 0; + for (const item of node.items) { + const c = getAliasCount(doc, item, anchors2); + if (c > count) + count = c; + } + return count; + } else if (identity.isPair(node)) { + const kc = getAliasCount(doc, node.key, anchors2); + const vc = getAliasCount(doc, node.value, anchors2); + return Math.max(kc, vc); + } + return 1; + } + exports.Alias = Alias; + } +}); + +// node_modules/yaml/dist/nodes/Scalar.js +var require_Scalar = __commonJS({ + "node_modules/yaml/dist/nodes/Scalar.js"(exports) { + "use strict"; + var identity = require_identity(); + var Node = require_Node(); + var toJS = require_toJS(); + var isScalarValue = (value) => !value || typeof value !== "function" && typeof value !== "object"; + var Scalar = class extends Node.NodeBase { + constructor(value) { + super(identity.SCALAR); + this.value = value; + } + toJSON(arg, ctx) { + return ctx?.keep ? this.value : toJS.toJS(this.value, arg, ctx); + } + toString() { + return String(this.value); + } + }; + Scalar.BLOCK_FOLDED = "BLOCK_FOLDED"; + Scalar.BLOCK_LITERAL = "BLOCK_LITERAL"; + Scalar.PLAIN = "PLAIN"; + Scalar.QUOTE_DOUBLE = "QUOTE_DOUBLE"; + Scalar.QUOTE_SINGLE = "QUOTE_SINGLE"; + exports.Scalar = Scalar; + exports.isScalarValue = isScalarValue; + } +}); + +// node_modules/yaml/dist/doc/createNode.js +var require_createNode = __commonJS({ + "node_modules/yaml/dist/doc/createNode.js"(exports) { + "use strict"; + var Alias = require_Alias(); + var identity = require_identity(); + var Scalar = require_Scalar(); + var defaultTagPrefix = "tag:yaml.org,2002:"; + function findTagObject(value, tagName, tags) { + if (tagName) { + const match = tags.filter((t) => t.tag === tagName); + const tagObj = match.find((t) => !t.format) ?? match[0]; + if (!tagObj) + throw new Error(`Tag ${tagName} not found`); + return tagObj; + } + return tags.find((t) => t.identify?.(value) && !t.format); + } + function createNode(value, tagName, ctx) { + if (identity.isDocument(value)) + value = value.contents; + if (identity.isNode(value)) + return value; + if (identity.isPair(value)) { + const map = ctx.schema[identity.MAP].createNode?.(ctx.schema, null, ctx); + map.items.push(value); + return map; + } + if (value instanceof String || value instanceof Number || value instanceof Boolean || typeof BigInt !== "undefined" && value instanceof BigInt) { + value = value.valueOf(); + } + const { aliasDuplicateObjects, onAnchor, onTagObj, schema, sourceObjects } = ctx; + let ref = void 0; + if (aliasDuplicateObjects && value && typeof value === "object") { + ref = sourceObjects.get(value); + if (ref) { + ref.anchor ?? (ref.anchor = onAnchor(value)); + return new Alias.Alias(ref.anchor); + } else { + ref = { anchor: null, node: null }; + sourceObjects.set(value, ref); + } + } + if (tagName?.startsWith("!!")) + tagName = defaultTagPrefix + tagName.slice(2); + let tagObj = findTagObject(value, tagName, schema.tags); + if (!tagObj) { + if (value && typeof value.toJSON === "function") { + value = value.toJSON(); + } + if (!value || typeof value !== "object") { + const node2 = new Scalar.Scalar(value); + if (ref) + ref.node = node2; + return node2; + } + tagObj = value instanceof Map ? schema[identity.MAP] : Symbol.iterator in Object(value) ? schema[identity.SEQ] : schema[identity.MAP]; + } + if (onTagObj) { + onTagObj(tagObj); + delete ctx.onTagObj; + } + const node = tagObj?.createNode ? tagObj.createNode(ctx.schema, value, ctx) : typeof tagObj?.nodeClass?.from === "function" ? tagObj.nodeClass.from(ctx.schema, value, ctx) : new Scalar.Scalar(value); + if (tagName) + node.tag = tagName; + else if (!tagObj.default) + node.tag = tagObj.tag; + if (ref) + ref.node = node; + return node; + } + exports.createNode = createNode; + } +}); + +// node_modules/yaml/dist/nodes/Collection.js +var require_Collection = __commonJS({ + "node_modules/yaml/dist/nodes/Collection.js"(exports) { + "use strict"; + var createNode = require_createNode(); + var identity = require_identity(); + var Node = require_Node(); + function collectionFromPath(schema, path, value) { + let v = value; + for (let i = path.length - 1; i >= 0; --i) { + const k = path[i]; + if (typeof k === "number" && Number.isInteger(k) && k >= 0) { + const a = []; + a[k] = v; + v = a; + } else { + v = /* @__PURE__ */ new Map([[k, v]]); + } + } + return createNode.createNode(v, void 0, { + aliasDuplicateObjects: false, + keepUndefined: false, + onAnchor: () => { + throw new Error("This should not happen, please report a bug."); + }, + schema, + sourceObjects: /* @__PURE__ */ new Map() + }); + } + var isEmptyPath = (path) => path == null || typeof path === "object" && !!path[Symbol.iterator]().next().done; + var Collection = class extends Node.NodeBase { + constructor(type, schema) { + super(type); + Object.defineProperty(this, "schema", { + value: schema, + configurable: true, + enumerable: false, + writable: true + }); + } + /** + * Create a copy of this collection. + * + * @param schema - If defined, overwrites the original's schema + */ + clone(schema) { + const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this)); + if (schema) + copy.schema = schema; + copy.items = copy.items.map((it) => identity.isNode(it) || identity.isPair(it) ? it.clone(schema) : it); + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** + * Adds a value to the collection. For `!!map` and `!!omap` the value must + * be a Pair instance or a `{ key, value }` object, which may not have a key + * that already exists in the map. + */ + addIn(path, value) { + if (isEmptyPath(path)) + this.add(value); + else { + const [key, ...rest] = path; + const node = this.get(key, true); + if (identity.isCollection(node)) + node.addIn(rest, value); + else if (node === void 0 && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + } + /** + * Removes a value from the collection. + * @returns `true` if the item was found and removed. + */ + deleteIn(path) { + const [key, ...rest] = path; + if (rest.length === 0) + return this.delete(key); + const node = this.get(key, true); + if (identity.isCollection(node)) + return node.deleteIn(rest); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn(path, keepScalar) { + const [key, ...rest] = path; + const node = this.get(key, true); + if (rest.length === 0) + return !keepScalar && identity.isScalar(node) ? node.value : node; + else + return identity.isCollection(node) ? node.getIn(rest, keepScalar) : void 0; + } + hasAllNullValues(allowScalar) { + return this.items.every((node) => { + if (!identity.isPair(node)) + return false; + const n = node.value; + return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag; + }); + } + /** + * Checks if the collection includes a value with the key `key`. + */ + hasIn(path) { + const [key, ...rest] = path; + if (rest.length === 0) + return this.has(key); + const node = this.get(key, true); + return identity.isCollection(node) ? node.hasIn(rest) : false; + } + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn(path, value) { + const [key, ...rest] = path; + if (rest.length === 0) { + this.set(key, value); + } else { + const node = this.get(key, true); + if (identity.isCollection(node)) + node.setIn(rest, value); + else if (node === void 0 && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + } + }; + exports.Collection = Collection; + exports.collectionFromPath = collectionFromPath; + exports.isEmptyPath = isEmptyPath; + } +}); + +// node_modules/yaml/dist/stringify/stringifyComment.js +var require_stringifyComment = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyComment.js"(exports) { + "use strict"; + var stringifyComment = (str) => str.replace(/^(?!$)(?: $)?/gm, "#"); + function indentComment(comment, indent) { + if (/^\n+$/.test(comment)) + return comment.substring(1); + return indent ? comment.replace(/^(?! *$)/gm, indent) : comment; + } + var lineComment = (str, indent, comment) => str.endsWith("\n") ? indentComment(comment, indent) : comment.includes("\n") ? "\n" + indentComment(comment, indent) : (str.endsWith(" ") ? "" : " ") + comment; + exports.indentComment = indentComment; + exports.lineComment = lineComment; + exports.stringifyComment = stringifyComment; + } +}); + +// node_modules/yaml/dist/stringify/foldFlowLines.js +var require_foldFlowLines = __commonJS({ + "node_modules/yaml/dist/stringify/foldFlowLines.js"(exports) { + "use strict"; + var FOLD_FLOW = "flow"; + var FOLD_BLOCK = "block"; + var FOLD_QUOTED = "quoted"; + function foldFlowLines(text, indent, mode = "flow", { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) { + if (!lineWidth || lineWidth < 0) + return text; + if (lineWidth < minContentWidth) + minContentWidth = 0; + const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length); + if (text.length <= endStep) + return text; + const folds = []; + const escapedFolds = {}; + let end = lineWidth - indent.length; + if (typeof indentAtStart === "number") { + if (indentAtStart > lineWidth - Math.max(2, minContentWidth)) + folds.push(0); + else + end = lineWidth - indentAtStart; + } + let split = void 0; + let prev = void 0; + let overflow = false; + let i = -1; + let escStart = -1; + let escEnd = -1; + if (mode === FOLD_BLOCK) { + i = consumeMoreIndentedLines(text, i, indent.length); + if (i !== -1) + end = i + endStep; + } + for (let ch; ch = text[i += 1]; ) { + if (mode === FOLD_QUOTED && ch === "\\") { + escStart = i; + switch (text[i + 1]) { + case "x": + i += 3; + break; + case "u": + i += 5; + break; + case "U": + i += 9; + break; + default: + i += 1; + } + escEnd = i; + } + if (ch === "\n") { + if (mode === FOLD_BLOCK) + i = consumeMoreIndentedLines(text, i, indent.length); + end = i + indent.length + endStep; + split = void 0; + } else { + if (ch === " " && prev && prev !== " " && prev !== "\n" && prev !== " ") { + const next = text[i + 1]; + if (next && next !== " " && next !== "\n" && next !== " ") + split = i; + } + if (i >= end) { + if (split) { + folds.push(split); + end = split + endStep; + split = void 0; + } else if (mode === FOLD_QUOTED) { + while (prev === " " || prev === " ") { + prev = ch; + ch = text[i += 1]; + overflow = true; + } + const j = i > escEnd + 1 ? i - 2 : escStart - 1; + if (escapedFolds[j]) + return text; + folds.push(j); + escapedFolds[j] = true; + end = j + endStep; + split = void 0; + } else { + overflow = true; + } + } + } + prev = ch; + } + if (overflow && onOverflow) + onOverflow(); + if (folds.length === 0) + return text; + if (onFold) + onFold(); + let res = text.slice(0, folds[0]); + for (let i2 = 0; i2 < folds.length; ++i2) { + const fold = folds[i2]; + const end2 = folds[i2 + 1] || text.length; + if (fold === 0) + res = ` +${indent}${text.slice(0, end2)}`; + else { + if (mode === FOLD_QUOTED && escapedFolds[fold]) + res += `${text[fold]}\\`; + res += ` +${indent}${text.slice(fold + 1, end2)}`; + } + } + return res; + } + function consumeMoreIndentedLines(text, i, indent) { + let end = i; + let start = i + 1; + let ch = text[start]; + while (ch === " " || ch === " ") { + if (i < start + indent) { + ch = text[++i]; + } else { + do { + ch = text[++i]; + } while (ch && ch !== "\n"); + end = i; + start = i + 1; + ch = text[start]; + } + } + return end; + } + exports.FOLD_BLOCK = FOLD_BLOCK; + exports.FOLD_FLOW = FOLD_FLOW; + exports.FOLD_QUOTED = FOLD_QUOTED; + exports.foldFlowLines = foldFlowLines; + } +}); + +// node_modules/yaml/dist/stringify/stringifyString.js +var require_stringifyString = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyString.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var foldFlowLines = require_foldFlowLines(); + var getFoldOptions = (ctx, isBlock) => ({ + indentAtStart: isBlock ? ctx.indent.length : ctx.indentAtStart, + lineWidth: ctx.options.lineWidth, + minContentWidth: ctx.options.minContentWidth + }); + var containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str); + function lineLengthOverLimit(str, lineWidth, indentLength) { + if (!lineWidth || lineWidth < 0) + return false; + const limit = lineWidth - indentLength; + const strLen = str.length; + if (strLen <= limit) + return false; + for (let i = 0, start = 0; i < strLen; ++i) { + if (str[i] === "\n") { + if (i - start > limit) + return true; + start = i + 1; + if (strLen - start <= limit) + return false; + } + } + return true; + } + function doubleQuotedString(value, ctx) { + const json = JSON.stringify(value); + if (ctx.options.doubleQuotedAsJSON) + return json; + const { implicitKey } = ctx; + const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength; + const indent = ctx.indent || (containsDocumentMarker(value) ? " " : ""); + let str = ""; + let start = 0; + for (let i = 0, ch = json[i]; ch; ch = json[++i]) { + if (ch === " " && json[i + 1] === "\\" && json[i + 2] === "n") { + str += json.slice(start, i) + "\\ "; + i += 1; + start = i; + ch = "\\"; + } + if (ch === "\\") + switch (json[i + 1]) { + case "u": + { + str += json.slice(start, i); + const code = json.substr(i + 2, 4); + switch (code) { + case "0000": + str += "\\0"; + break; + case "0007": + str += "\\a"; + break; + case "000b": + str += "\\v"; + break; + case "001b": + str += "\\e"; + break; + case "0085": + str += "\\N"; + break; + case "00a0": + str += "\\_"; + break; + case "2028": + str += "\\L"; + break; + case "2029": + str += "\\P"; + break; + default: + if (code.substr(0, 2) === "00") + str += "\\x" + code.substr(2); + else + str += json.substr(i, 6); + } + i += 5; + start = i + 1; + } + break; + case "n": + if (implicitKey || json[i + 2] === '"' || json.length < minMultiLineLength) { + i += 1; + } else { + str += json.slice(start, i) + "\n\n"; + while (json[i + 2] === "\\" && json[i + 3] === "n" && json[i + 4] !== '"') { + str += "\n"; + i += 2; + } + str += indent; + if (json[i + 2] === " ") + str += "\\"; + i += 1; + start = i + 1; + } + break; + default: + i += 1; + } + } + str = start ? str + json.slice(start) : json; + return implicitKey ? str : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false)); + } + function singleQuotedString(value, ctx) { + if (ctx.options.singleQuote === false || ctx.implicitKey && value.includes("\n") || /[ \t]\n|\n[ \t]/.test(value)) + return doubleQuotedString(value, ctx); + const indent = ctx.indent || (containsDocumentMarker(value) ? " " : ""); + const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$& +${indent}`) + "'"; + return ctx.implicitKey ? res : foldFlowLines.foldFlowLines(res, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false)); + } + function quotedString(value, ctx) { + const { singleQuote } = ctx.options; + let qs; + if (singleQuote === false) + qs = doubleQuotedString; + else { + const hasDouble = value.includes('"'); + const hasSingle = value.includes("'"); + if (hasDouble && !hasSingle) + qs = singleQuotedString; + else if (hasSingle && !hasDouble) + qs = doubleQuotedString; + else + qs = singleQuote ? singleQuotedString : doubleQuotedString; + } + return qs(value, ctx); + } + var blockEndNewlines; + try { + blockEndNewlines = new RegExp("(^|(?\n"; + let chomp; + let endStart; + for (endStart = value.length; endStart > 0; --endStart) { + const ch = value[endStart - 1]; + if (ch !== "\n" && ch !== " " && ch !== " ") + break; + } + let end = value.substring(endStart); + const endNlPos = end.indexOf("\n"); + if (endNlPos === -1) { + chomp = "-"; + } else if (value === end || endNlPos !== end.length - 1) { + chomp = "+"; + if (onChompKeep) + onChompKeep(); + } else { + chomp = ""; + } + if (end) { + value = value.slice(0, -end.length); + if (end[end.length - 1] === "\n") + end = end.slice(0, -1); + end = end.replace(blockEndNewlines, `$&${indent}`); + } + let startWithSpace = false; + let startEnd; + let startNlPos = -1; + for (startEnd = 0; startEnd < value.length; ++startEnd) { + const ch = value[startEnd]; + if (ch === " ") + startWithSpace = true; + else if (ch === "\n") + startNlPos = startEnd; + else + break; + } + let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd); + if (start) { + value = value.substring(start.length); + start = start.replace(/\n+/g, `$&${indent}`); + } + const indentSize = indent ? "2" : "1"; + let header = (startWithSpace ? indentSize : "") + chomp; + if (comment) { + header += " " + commentString(comment.replace(/ ?[\r\n]+/g, " ")); + if (onComment) + onComment(); + } + if (!literal) { + const foldedValue = value.replace(/\n+/g, "\n$&").replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/\n+/g, `$&${indent}`); + let literalFallback = false; + const foldOptions = getFoldOptions(ctx, true); + if (blockQuote !== "folded" && type !== Scalar.Scalar.BLOCK_FOLDED) { + foldOptions.onOverflow = () => { + literalFallback = true; + }; + } + const body = foldFlowLines.foldFlowLines(`${start}${foldedValue}${end}`, indent, foldFlowLines.FOLD_BLOCK, foldOptions); + if (!literalFallback) + return `>${header} +${indent}${body}`; + } + value = value.replace(/\n+/g, `$&${indent}`); + return `|${header} +${indent}${start}${value}${end}`; + } + function plainString(item, ctx, onComment, onChompKeep) { + const { type, value } = item; + const { actualString, implicitKey, indent, indentStep, inFlow } = ctx; + if (implicitKey && value.includes("\n") || inFlow && /[[\]{},]/.test(value)) { + return quotedString(value, ctx); + } + if (/^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) { + return implicitKey || inFlow || !value.includes("\n") ? quotedString(value, ctx) : blockString(item, ctx, onComment, onChompKeep); + } + if (!implicitKey && !inFlow && type !== Scalar.Scalar.PLAIN && value.includes("\n")) { + return blockString(item, ctx, onComment, onChompKeep); + } + if (containsDocumentMarker(value)) { + if (indent === "") { + ctx.forceBlockIndent = true; + return blockString(item, ctx, onComment, onChompKeep); + } else if (implicitKey && indent === indentStep) { + return quotedString(value, ctx); + } + } + const str = value.replace(/\n+/g, `$& +${indent}`); + if (actualString) { + const test = (tag) => tag.default && tag.tag !== "tag:yaml.org,2002:str" && tag.test?.test(str); + const { compat, tags } = ctx.doc.schema; + if (tags.some(test) || compat?.some(test)) + return quotedString(value, ctx); + } + return implicitKey ? str : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false)); + } + function stringifyString(item, ctx, onComment, onChompKeep) { + const { implicitKey, inFlow } = ctx; + const ss = typeof item.value === "string" ? item : Object.assign({}, item, { value: String(item.value) }); + let { type } = item; + if (type !== Scalar.Scalar.QUOTE_DOUBLE) { + if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value)) + type = Scalar.Scalar.QUOTE_DOUBLE; + } + const _stringify = (_type) => { + switch (_type) { + case Scalar.Scalar.BLOCK_FOLDED: + case Scalar.Scalar.BLOCK_LITERAL: + return implicitKey || inFlow ? quotedString(ss.value, ctx) : blockString(ss, ctx, onComment, onChompKeep); + case Scalar.Scalar.QUOTE_DOUBLE: + return doubleQuotedString(ss.value, ctx); + case Scalar.Scalar.QUOTE_SINGLE: + return singleQuotedString(ss.value, ctx); + case Scalar.Scalar.PLAIN: + return plainString(ss, ctx, onComment, onChompKeep); + default: + return null; + } + }; + let res = _stringify(type); + if (res === null) { + const { defaultKeyType, defaultStringType } = ctx.options; + const t = implicitKey && defaultKeyType || defaultStringType; + res = _stringify(t); + if (res === null) + throw new Error(`Unsupported default string type ${t}`); + } + return res; + } + exports.stringifyString = stringifyString; + } +}); + +// node_modules/yaml/dist/stringify/stringify.js +var require_stringify = __commonJS({ + "node_modules/yaml/dist/stringify/stringify.js"(exports) { + "use strict"; + var anchors = require_anchors(); + var identity = require_identity(); + var stringifyComment = require_stringifyComment(); + var stringifyString = require_stringifyString(); + function createStringifyContext(doc, options) { + const opt = Object.assign({ + blockQuote: true, + commentString: stringifyComment.stringifyComment, + defaultKeyType: null, + defaultStringType: "PLAIN", + directives: null, + doubleQuotedAsJSON: false, + doubleQuotedMinMultiLineLength: 40, + falseStr: "false", + flowCollectionPadding: true, + indentSeq: true, + lineWidth: 80, + minContentWidth: 20, + nullStr: "null", + simpleKeys: false, + singleQuote: null, + trailingComma: false, + trueStr: "true", + verifyAliasOrder: true + }, doc.schema.toStringOptions, options); + let inFlow; + switch (opt.collectionStyle) { + case "block": + inFlow = false; + break; + case "flow": + inFlow = true; + break; + default: + inFlow = null; + } + return { + anchors: /* @__PURE__ */ new Set(), + doc, + flowCollectionPadding: opt.flowCollectionPadding ? " " : "", + indent: "", + indentStep: typeof opt.indent === "number" ? " ".repeat(opt.indent) : " ", + inFlow, + options: opt + }; + } + function getTagObject(tags, item) { + if (item.tag) { + const match = tags.filter((t) => t.tag === item.tag); + if (match.length > 0) + return match.find((t) => t.format === item.format) ?? match[0]; + } + let tagObj = void 0; + let obj; + if (identity.isScalar(item)) { + obj = item.value; + let match = tags.filter((t) => t.identify?.(obj)); + if (match.length > 1) { + const testMatch = match.filter((t) => t.test); + if (testMatch.length > 0) + match = testMatch; + } + tagObj = match.find((t) => t.format === item.format) ?? match.find((t) => !t.format); + } else { + obj = item; + tagObj = tags.find((t) => t.nodeClass && obj instanceof t.nodeClass); + } + if (!tagObj) { + const name = obj?.constructor?.name ?? (obj === null ? "null" : typeof obj); + throw new Error(`Tag not resolved for ${name} value`); + } + return tagObj; + } + function stringifyProps(node, tagObj, { anchors: anchors$1, doc }) { + if (!doc.directives) + return ""; + const props = []; + const anchor = (identity.isScalar(node) || identity.isCollection(node)) && node.anchor; + if (anchor && anchors.anchorIsValid(anchor)) { + anchors$1.add(anchor); + props.push(`&${anchor}`); + } + const tag = node.tag ?? (tagObj.default ? null : tagObj.tag); + if (tag) + props.push(doc.directives.tagString(tag)); + return props.join(" "); + } + function stringify2(item, ctx, onComment, onChompKeep) { + if (identity.isPair(item)) + return item.toString(ctx, onComment, onChompKeep); + if (identity.isAlias(item)) { + if (ctx.doc.directives) + return item.toString(ctx); + if (ctx.resolvedAliases?.has(item)) { + throw new TypeError(`Cannot stringify circular structure without alias nodes`); + } else { + if (ctx.resolvedAliases) + ctx.resolvedAliases.add(item); + else + ctx.resolvedAliases = /* @__PURE__ */ new Set([item]); + item = item.resolve(ctx.doc); + } + } + let tagObj = void 0; + const node = identity.isNode(item) ? item : ctx.doc.createNode(item, { onTagObj: (o) => tagObj = o }); + tagObj ?? (tagObj = getTagObject(ctx.doc.schema.tags, node)); + const props = stringifyProps(node, tagObj, ctx); + if (props.length > 0) + ctx.indentAtStart = (ctx.indentAtStart ?? 0) + props.length + 1; + const str = typeof tagObj.stringify === "function" ? tagObj.stringify(node, ctx, onComment, onChompKeep) : identity.isScalar(node) ? stringifyString.stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep); + if (!props) + return str; + return identity.isScalar(node) || str[0] === "{" || str[0] === "[" ? `${props} ${str}` : `${props} +${ctx.indent}${str}`; + } + exports.createStringifyContext = createStringifyContext; + exports.stringify = stringify2; + } +}); + +// node_modules/yaml/dist/stringify/stringifyPair.js +var require_stringifyPair = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyPair.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var stringify2 = require_stringify(); + var stringifyComment = require_stringifyComment(); + function stringifyPair({ key, value }, ctx, onComment, onChompKeep) { + const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx; + let keyComment = identity.isNode(key) && key.comment || null; + if (simpleKeys) { + if (keyComment) { + throw new Error("With simple keys, key nodes cannot have comments"); + } + if (identity.isCollection(key) || !identity.isNode(key) && typeof key === "object") { + const msg = "With simple keys, collection cannot be used as a key value"; + throw new Error(msg); + } + } + let explicitKey = !simpleKeys && (!key || keyComment && value == null && !ctx.inFlow || identity.isCollection(key) || (identity.isScalar(key) ? key.type === Scalar.Scalar.BLOCK_FOLDED || key.type === Scalar.Scalar.BLOCK_LITERAL : typeof key === "object")); + ctx = Object.assign({}, ctx, { + allNullValues: false, + implicitKey: !explicitKey && (simpleKeys || !allNullValues), + indent: indent + indentStep + }); + let keyCommentDone = false; + let chompKeep = false; + let str = stringify2.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true); + if (!explicitKey && !ctx.inFlow && str.length > 1024) { + if (simpleKeys) + throw new Error("With simple keys, single line scalar must not span more than 1024 characters"); + explicitKey = true; + } + if (ctx.inFlow) { + if (allNullValues || value == null) { + if (keyCommentDone && onComment) + onComment(); + return str === "" ? "?" : explicitKey ? `? ${str}` : str; + } + } else if (allNullValues && !simpleKeys || value == null && explicitKey) { + str = `? ${str}`; + if (keyComment && !keyCommentDone) { + str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment)); + } else if (chompKeep && onChompKeep) + onChompKeep(); + return str; + } + if (keyCommentDone) + keyComment = null; + if (explicitKey) { + if (keyComment) + str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment)); + str = `? ${str} +${indent}:`; + } else { + str = `${str}:`; + if (keyComment) + str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment)); + } + let vsb, vcb, valueComment; + if (identity.isNode(value)) { + vsb = !!value.spaceBefore; + vcb = value.commentBefore; + valueComment = value.comment; + } else { + vsb = false; + vcb = null; + valueComment = null; + if (value && typeof value === "object") + value = doc.createNode(value); + } + ctx.implicitKey = false; + if (!explicitKey && !keyComment && identity.isScalar(value)) + ctx.indentAtStart = str.length + 1; + chompKeep = false; + if (!indentSeq && indentStep.length >= 2 && !ctx.inFlow && !explicitKey && identity.isSeq(value) && !value.flow && !value.tag && !value.anchor) { + ctx.indent = ctx.indent.substring(2); + } + let valueCommentDone = false; + const valueStr = stringify2.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true); + let ws = " "; + if (keyComment || vsb || vcb) { + ws = vsb ? "\n" : ""; + if (vcb) { + const cs = commentString(vcb); + ws += ` +${stringifyComment.indentComment(cs, ctx.indent)}`; + } + if (valueStr === "" && !ctx.inFlow) { + if (ws === "\n" && valueComment) + ws = "\n\n"; + } else { + ws += ` +${ctx.indent}`; + } + } else if (!explicitKey && identity.isCollection(value)) { + const vs0 = valueStr[0]; + const nl0 = valueStr.indexOf("\n"); + const hasNewline = nl0 !== -1; + const flow = ctx.inFlow ?? value.flow ?? value.items.length === 0; + if (hasNewline || !flow) { + let hasPropsLine = false; + if (hasNewline && (vs0 === "&" || vs0 === "!")) { + let sp0 = valueStr.indexOf(" "); + if (vs0 === "&" && sp0 !== -1 && sp0 < nl0 && valueStr[sp0 + 1] === "!") { + sp0 = valueStr.indexOf(" ", sp0 + 1); + } + if (sp0 === -1 || nl0 < sp0) + hasPropsLine = true; + } + if (!hasPropsLine) + ws = ` +${ctx.indent}`; + } + } else if (valueStr === "" || valueStr[0] === "\n") { + ws = ""; + } + str += ws + valueStr; + if (ctx.inFlow) { + if (valueCommentDone && onComment) + onComment(); + } else if (valueComment && !valueCommentDone) { + str += stringifyComment.lineComment(str, ctx.indent, commentString(valueComment)); + } else if (chompKeep && onChompKeep) { + onChompKeep(); + } + return str; + } + exports.stringifyPair = stringifyPair; + } +}); + +// node_modules/yaml/dist/log.js +var require_log = __commonJS({ + "node_modules/yaml/dist/log.js"(exports) { + "use strict"; + var node_process = __require("process"); + function debug(logLevel, ...messages) { + if (logLevel === "debug") + console.log(...messages); + } + function warn(logLevel, warning) { + if (logLevel === "debug" || logLevel === "warn") { + if (typeof node_process.emitWarning === "function") + node_process.emitWarning(warning); + else + console.warn(warning); + } + } + exports.debug = debug; + exports.warn = warn; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/merge.js +var require_merge = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/merge.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var MERGE_KEY = "<<"; + var merge = { + identify: (value) => value === MERGE_KEY || typeof value === "symbol" && value.description === MERGE_KEY, + default: "key", + tag: "tag:yaml.org,2002:merge", + test: /^<<$/, + resolve: () => Object.assign(new Scalar.Scalar(Symbol(MERGE_KEY)), { + addToJSMap: addMergeToJSMap + }), + stringify: () => MERGE_KEY + }; + var isMergeKey = (ctx, key) => (merge.identify(key) || identity.isScalar(key) && (!key.type || key.type === Scalar.Scalar.PLAIN) && merge.identify(key.value)) && ctx?.doc.schema.tags.some((tag) => tag.tag === merge.tag && tag.default); + function addMergeToJSMap(ctx, map, value) { + const source = resolveAliasValue(ctx, value); + if (identity.isSeq(source)) + for (const it of source.items) + mergeValue(ctx, map, it); + else if (Array.isArray(source)) + for (const it of source) + mergeValue(ctx, map, it); + else + mergeValue(ctx, map, source); + } + function mergeValue(ctx, map, value) { + const source = resolveAliasValue(ctx, value); + if (!identity.isMap(source)) + throw new Error("Merge sources must be maps or map aliases"); + const srcMap = source.toJSON(null, ctx, Map); + for (const [key, value2] of srcMap) { + if (map instanceof Map) { + if (!map.has(key)) + map.set(key, value2); + } else if (map instanceof Set) { + map.add(key); + } else if (!Object.prototype.hasOwnProperty.call(map, key)) { + Object.defineProperty(map, key, { + value: value2, + writable: true, + enumerable: true, + configurable: true + }); + } + } + return map; + } + function resolveAliasValue(ctx, value) { + return ctx && identity.isAlias(value) ? value.resolve(ctx.doc, ctx) : value; + } + exports.addMergeToJSMap = addMergeToJSMap; + exports.isMergeKey = isMergeKey; + exports.merge = merge; + } +}); + +// node_modules/yaml/dist/nodes/addPairToJSMap.js +var require_addPairToJSMap = __commonJS({ + "node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports) { + "use strict"; + var log = require_log(); + var merge = require_merge(); + var stringify2 = require_stringify(); + var identity = require_identity(); + var toJS = require_toJS(); + function addPairToJSMap(ctx, map, { key, value }) { + if (identity.isNode(key) && key.addToJSMap) + key.addToJSMap(ctx, map, value); + else if (merge.isMergeKey(ctx, key)) + merge.addMergeToJSMap(ctx, map, value); + else { + const jsKey = toJS.toJS(key, "", ctx); + if (map instanceof Map) { + map.set(jsKey, toJS.toJS(value, jsKey, ctx)); + } else if (map instanceof Set) { + map.add(jsKey); + } else { + const stringKey = stringifyKey(key, jsKey, ctx); + const jsValue = toJS.toJS(value, stringKey, ctx); + if (stringKey in map) + Object.defineProperty(map, stringKey, { + value: jsValue, + writable: true, + enumerable: true, + configurable: true + }); + else + map[stringKey] = jsValue; + } + } + return map; + } + function stringifyKey(key, jsKey, ctx) { + if (jsKey === null) + return ""; + if (typeof jsKey !== "object") + return String(jsKey); + if (identity.isNode(key) && ctx?.doc) { + const strCtx = stringify2.createStringifyContext(ctx.doc, {}); + strCtx.anchors = /* @__PURE__ */ new Set(); + for (const node of ctx.anchors.keys()) + strCtx.anchors.add(node.anchor); + strCtx.inFlow = true; + strCtx.inStringifyKey = true; + const strKey = key.toString(strCtx); + if (!ctx.mapKeyWarned) { + let jsonStr = JSON.stringify(strKey); + if (jsonStr.length > 40) + jsonStr = jsonStr.substring(0, 36) + '..."'; + log.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`); + ctx.mapKeyWarned = true; + } + return strKey; + } + return JSON.stringify(jsKey); + } + exports.addPairToJSMap = addPairToJSMap; + } +}); + +// node_modules/yaml/dist/nodes/Pair.js +var require_Pair = __commonJS({ + "node_modules/yaml/dist/nodes/Pair.js"(exports) { + "use strict"; + var createNode = require_createNode(); + var stringifyPair = require_stringifyPair(); + var addPairToJSMap = require_addPairToJSMap(); + var identity = require_identity(); + function createPair(key, value, ctx) { + const k = createNode.createNode(key, void 0, ctx); + const v = createNode.createNode(value, void 0, ctx); + return new Pair(k, v); + } + var Pair = class _Pair { + constructor(key, value = null) { + Object.defineProperty(this, identity.NODE_TYPE, { value: identity.PAIR }); + this.key = key; + this.value = value; + } + clone(schema) { + let { key, value } = this; + if (identity.isNode(key)) + key = key.clone(schema); + if (identity.isNode(value)) + value = value.clone(schema); + return new _Pair(key, value); + } + toJSON(_, ctx) { + const pair = ctx?.mapAsMap ? /* @__PURE__ */ new Map() : {}; + return addPairToJSMap.addPairToJSMap(ctx, pair, this); + } + toString(ctx, onComment, onChompKeep) { + return ctx?.doc ? stringifyPair.stringifyPair(this, ctx, onComment, onChompKeep) : JSON.stringify(this); + } + }; + exports.Pair = Pair; + exports.createPair = createPair; + } +}); + +// node_modules/yaml/dist/stringify/stringifyCollection.js +var require_stringifyCollection = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyCollection.js"(exports) { + "use strict"; + var identity = require_identity(); + var stringify2 = require_stringify(); + var stringifyComment = require_stringifyComment(); + function stringifyCollection(collection, ctx, options) { + const flow = ctx.inFlow ?? collection.flow; + const stringify3 = flow ? stringifyFlowCollection : stringifyBlockCollection; + return stringify3(collection, ctx, options); + } + function stringifyBlockCollection({ comment, items }, ctx, { blockItemPrefix, flowChars, itemIndent, onChompKeep, onComment }) { + const { indent, options: { commentString } } = ctx; + const itemCtx = Object.assign({}, ctx, { indent: itemIndent, type: null }); + let chompKeep = false; + const lines = []; + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + let comment2 = null; + if (identity.isNode(item)) { + if (!chompKeep && item.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, item.commentBefore, chompKeep); + if (item.comment) + comment2 = item.comment; + } else if (identity.isPair(item)) { + const ik = identity.isNode(item.key) ? item.key : null; + if (ik) { + if (!chompKeep && ik.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, ik.commentBefore, chompKeep); + } + } + chompKeep = false; + let str2 = stringify2.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true); + if (comment2) + str2 += stringifyComment.lineComment(str2, itemIndent, commentString(comment2)); + if (chompKeep && comment2) + chompKeep = false; + lines.push(blockItemPrefix + str2); + } + let str; + if (lines.length === 0) { + str = flowChars.start + flowChars.end; + } else { + str = lines[0]; + for (let i = 1; i < lines.length; ++i) { + const line = lines[i]; + str += line ? ` +${indent}${line}` : "\n"; + } + } + if (comment) { + str += "\n" + stringifyComment.indentComment(commentString(comment), indent); + if (onComment) + onComment(); + } else if (chompKeep && onChompKeep) + onChompKeep(); + return str; + } + function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) { + const { indent, indentStep, flowCollectionPadding: fcPadding, options: { commentString } } = ctx; + itemIndent += indentStep; + const itemCtx = Object.assign({}, ctx, { + indent: itemIndent, + inFlow: true, + type: null + }); + let reqNewline = false; + let linesAtValue = 0; + const lines = []; + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + let comment = null; + if (identity.isNode(item)) { + if (item.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, item.commentBefore, false); + if (item.comment) + comment = item.comment; + } else if (identity.isPair(item)) { + const ik = identity.isNode(item.key) ? item.key : null; + if (ik) { + if (ik.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, ik.commentBefore, false); + if (ik.comment) + reqNewline = true; + } + const iv = identity.isNode(item.value) ? item.value : null; + if (iv) { + if (iv.comment) + comment = iv.comment; + if (iv.commentBefore) + reqNewline = true; + } else if (item.value == null && ik?.comment) { + comment = ik.comment; + } + } + if (comment) + reqNewline = true; + let str = stringify2.stringify(item, itemCtx, () => comment = null); + reqNewline || (reqNewline = lines.length > linesAtValue || str.includes("\n")); + if (i < items.length - 1) { + str += ","; + } else if (ctx.options.trailingComma) { + if (ctx.options.lineWidth > 0) { + reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth); + } + if (reqNewline) { + str += ","; + } + } + if (comment) + str += stringifyComment.lineComment(str, itemIndent, commentString(comment)); + lines.push(str); + linesAtValue = lines.length; + } + const { start, end } = flowChars; + if (lines.length === 0) { + return start + end; + } else { + if (!reqNewline) { + const len = lines.reduce((sum, line) => sum + line.length + 2, 2); + reqNewline = ctx.options.lineWidth > 0 && len > ctx.options.lineWidth; + } + if (reqNewline) { + let str = start; + for (const line of lines) + str += line ? ` +${indentStep}${indent}${line}` : "\n"; + return `${str} +${indent}${end}`; + } else { + return `${start}${fcPadding}${lines.join(" ")}${fcPadding}${end}`; + } + } + } + function addCommentBefore({ indent, options: { commentString } }, lines, comment, chompKeep) { + if (comment && chompKeep) + comment = comment.replace(/^\n+/, ""); + if (comment) { + const ic = stringifyComment.indentComment(commentString(comment), indent); + lines.push(ic.trimStart()); + } + } + exports.stringifyCollection = stringifyCollection; + } +}); + +// node_modules/yaml/dist/nodes/YAMLMap.js +var require_YAMLMap = __commonJS({ + "node_modules/yaml/dist/nodes/YAMLMap.js"(exports) { + "use strict"; + var stringifyCollection = require_stringifyCollection(); + var addPairToJSMap = require_addPairToJSMap(); + var Collection = require_Collection(); + var identity = require_identity(); + var Pair = require_Pair(); + var Scalar = require_Scalar(); + function findPair(items, key) { + const k = identity.isScalar(key) ? key.value : key; + for (const it of items) { + if (identity.isPair(it)) { + if (it.key === key || it.key === k) + return it; + if (identity.isScalar(it.key) && it.key.value === k) + return it; + } + } + return void 0; + } + var YAMLMap = class extends Collection.Collection { + static get tagName() { + return "tag:yaml.org,2002:map"; + } + constructor(schema) { + super(identity.MAP, schema); + this.items = []; + } + /** + * A generic collection parsing method that can be extended + * to other node classes that inherit from YAMLMap + */ + static from(schema, obj, ctx) { + const { keepUndefined, replacer } = ctx; + const map = new this(schema); + const add = (key, value) => { + if (typeof replacer === "function") + value = replacer.call(obj, key, value); + else if (Array.isArray(replacer) && !replacer.includes(key)) + return; + if (value !== void 0 || keepUndefined) + map.items.push(Pair.createPair(key, value, ctx)); + }; + if (obj instanceof Map) { + for (const [key, value] of obj) + add(key, value); + } else if (obj && typeof obj === "object") { + for (const key of Object.keys(obj)) + add(key, obj[key]); + } + if (typeof schema.sortMapEntries === "function") { + map.items.sort(schema.sortMapEntries); + } + return map; + } + /** + * Adds a value to the collection. + * + * @param overwrite - If not set `true`, using a key that is already in the + * collection will throw. Otherwise, overwrites the previous value. + */ + add(pair, overwrite) { + let _pair; + if (identity.isPair(pair)) + _pair = pair; + else if (!pair || typeof pair !== "object" || !("key" in pair)) { + _pair = new Pair.Pair(pair, pair?.value); + } else + _pair = new Pair.Pair(pair.key, pair.value); + const prev = findPair(this.items, _pair.key); + const sortEntries = this.schema?.sortMapEntries; + if (prev) { + if (!overwrite) + throw new Error(`Key ${_pair.key} already set`); + if (identity.isScalar(prev.value) && Scalar.isScalarValue(_pair.value)) + prev.value.value = _pair.value; + else + prev.value = _pair.value; + } else if (sortEntries) { + const i = this.items.findIndex((item) => sortEntries(_pair, item) < 0); + if (i === -1) + this.items.push(_pair); + else + this.items.splice(i, 0, _pair); + } else { + this.items.push(_pair); + } + } + delete(key) { + const it = findPair(this.items, key); + if (!it) + return false; + const del = this.items.splice(this.items.indexOf(it), 1); + return del.length > 0; + } + get(key, keepScalar) { + const it = findPair(this.items, key); + const node = it?.value; + return (!keepScalar && identity.isScalar(node) ? node.value : node) ?? void 0; + } + has(key) { + return !!findPair(this.items, key); + } + set(key, value) { + this.add(new Pair.Pair(key, value), true); + } + /** + * @param ctx - Conversion context, originally set in Document#toJS() + * @param {Class} Type - If set, forces the returned collection type + * @returns Instance of Type, Map, or Object + */ + toJSON(_, ctx, Type) { + const map = Type ? new Type() : ctx?.mapAsMap ? /* @__PURE__ */ new Map() : {}; + if (ctx?.onCreate) + ctx.onCreate(map); + for (const item of this.items) + addPairToJSMap.addPairToJSMap(ctx, map, item); + return map; + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + for (const item of this.items) { + if (!identity.isPair(item)) + throw new Error(`Map items must all be pairs; found ${JSON.stringify(item)} instead`); + } + if (!ctx.allNullValues && this.hasAllNullValues(false)) + ctx = Object.assign({}, ctx, { allNullValues: true }); + return stringifyCollection.stringifyCollection(this, ctx, { + blockItemPrefix: "", + flowChars: { start: "{", end: "}" }, + itemIndent: ctx.indent || "", + onChompKeep, + onComment + }); + } + }; + exports.YAMLMap = YAMLMap; + exports.findPair = findPair; + } +}); + +// node_modules/yaml/dist/schema/common/map.js +var require_map = __commonJS({ + "node_modules/yaml/dist/schema/common/map.js"(exports) { + "use strict"; + var identity = require_identity(); + var YAMLMap = require_YAMLMap(); + var map = { + collection: "map", + default: true, + nodeClass: YAMLMap.YAMLMap, + tag: "tag:yaml.org,2002:map", + resolve(map2, onError) { + if (!identity.isMap(map2)) + onError("Expected a mapping for this tag"); + return map2; + }, + createNode: (schema, obj, ctx) => YAMLMap.YAMLMap.from(schema, obj, ctx) + }; + exports.map = map; + } +}); + +// node_modules/yaml/dist/nodes/YAMLSeq.js +var require_YAMLSeq = __commonJS({ + "node_modules/yaml/dist/nodes/YAMLSeq.js"(exports) { + "use strict"; + var createNode = require_createNode(); + var stringifyCollection = require_stringifyCollection(); + var Collection = require_Collection(); + var identity = require_identity(); + var Scalar = require_Scalar(); + var toJS = require_toJS(); + var YAMLSeq = class extends Collection.Collection { + static get tagName() { + return "tag:yaml.org,2002:seq"; + } + constructor(schema) { + super(identity.SEQ, schema); + this.items = []; + } + add(value) { + this.items.push(value); + } + /** + * Removes a value from the collection. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + * + * @returns `true` if the item was found and removed. + */ + delete(key) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + return false; + const del = this.items.splice(idx, 1); + return del.length > 0; + } + get(key, keepScalar) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + return void 0; + const it = this.items[idx]; + return !keepScalar && identity.isScalar(it) ? it.value : it; + } + /** + * Checks if the collection includes a value with the key `key`. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + */ + has(key) { + const idx = asItemIndex(key); + return typeof idx === "number" && idx < this.items.length; + } + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + * + * If `key` does not contain a representation of an integer, this will throw. + * It may be wrapped in a `Scalar`. + */ + set(key, value) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + throw new Error(`Expected a valid index, not ${key}.`); + const prev = this.items[idx]; + if (identity.isScalar(prev) && Scalar.isScalarValue(value)) + prev.value = value; + else + this.items[idx] = value; + } + toJSON(_, ctx) { + const seq = []; + if (ctx?.onCreate) + ctx.onCreate(seq); + let i = 0; + for (const item of this.items) + seq.push(toJS.toJS(item, String(i++), ctx)); + return seq; + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + return stringifyCollection.stringifyCollection(this, ctx, { + blockItemPrefix: "- ", + flowChars: { start: "[", end: "]" }, + itemIndent: (ctx.indent || "") + " ", + onChompKeep, + onComment + }); + } + static from(schema, obj, ctx) { + const { replacer } = ctx; + const seq = new this(schema); + if (obj && Symbol.iterator in Object(obj)) { + let i = 0; + for (let it of obj) { + if (typeof replacer === "function") { + const key = obj instanceof Set ? it : String(i++); + it = replacer.call(obj, key, it); + } + seq.items.push(createNode.createNode(it, void 0, ctx)); + } + } + return seq; + } + }; + function asItemIndex(key) { + let idx = identity.isScalar(key) ? key.value : key; + if (idx && typeof idx === "string") + idx = Number(idx); + return typeof idx === "number" && Number.isInteger(idx) && idx >= 0 ? idx : null; + } + exports.YAMLSeq = YAMLSeq; + } +}); + +// node_modules/yaml/dist/schema/common/seq.js +var require_seq = __commonJS({ + "node_modules/yaml/dist/schema/common/seq.js"(exports) { + "use strict"; + var identity = require_identity(); + var YAMLSeq = require_YAMLSeq(); + var seq = { + collection: "seq", + default: true, + nodeClass: YAMLSeq.YAMLSeq, + tag: "tag:yaml.org,2002:seq", + resolve(seq2, onError) { + if (!identity.isSeq(seq2)) + onError("Expected a sequence for this tag"); + return seq2; + }, + createNode: (schema, obj, ctx) => YAMLSeq.YAMLSeq.from(schema, obj, ctx) + }; + exports.seq = seq; + } +}); + +// node_modules/yaml/dist/schema/common/string.js +var require_string = __commonJS({ + "node_modules/yaml/dist/schema/common/string.js"(exports) { + "use strict"; + var stringifyString = require_stringifyString(); + var string = { + identify: (value) => typeof value === "string", + default: true, + tag: "tag:yaml.org,2002:str", + resolve: (str) => str, + stringify(item, ctx, onComment, onChompKeep) { + ctx = Object.assign({ actualString: true }, ctx); + return stringifyString.stringifyString(item, ctx, onComment, onChompKeep); + } + }; + exports.string = string; + } +}); + +// node_modules/yaml/dist/schema/common/null.js +var require_null = __commonJS({ + "node_modules/yaml/dist/schema/common/null.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var nullTag = { + identify: (value) => value == null, + createNode: () => new Scalar.Scalar(null), + default: true, + tag: "tag:yaml.org,2002:null", + test: /^(?:~|[Nn]ull|NULL)?$/, + resolve: () => new Scalar.Scalar(null), + stringify: ({ source }, ctx) => typeof source === "string" && nullTag.test.test(source) ? source : ctx.options.nullStr + }; + exports.nullTag = nullTag; + } +}); + +// node_modules/yaml/dist/schema/core/bool.js +var require_bool = __commonJS({ + "node_modules/yaml/dist/schema/core/bool.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var boolTag = { + identify: (value) => typeof value === "boolean", + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/, + resolve: (str) => new Scalar.Scalar(str[0] === "t" || str[0] === "T"), + stringify({ source, value }, ctx) { + if (source && boolTag.test.test(source)) { + const sv = source[0] === "t" || source[0] === "T"; + if (value === sv) + return source; + } + return value ? ctx.options.trueStr : ctx.options.falseStr; + } + }; + exports.boolTag = boolTag; + } +}); + +// node_modules/yaml/dist/stringify/stringifyNumber.js +var require_stringifyNumber = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyNumber.js"(exports) { + "use strict"; + function stringifyNumber({ format, minFractionDigits, tag, value }) { + if (typeof value === "bigint") + return String(value); + const num = typeof value === "number" ? value : Number(value); + if (!isFinite(num)) + return isNaN(num) ? ".nan" : num < 0 ? "-.inf" : ".inf"; + let n = Object.is(value, -0) ? "-0" : JSON.stringify(value); + if (!format && minFractionDigits && (!tag || tag === "tag:yaml.org,2002:float") && /^-?\d/.test(n) && !n.includes("e")) { + let i = n.indexOf("."); + if (i < 0) { + i = n.length; + n += "."; + } + let d = minFractionDigits - (n.length - i - 1); + while (d-- > 0) + n += "0"; + } + return n; + } + exports.stringifyNumber = stringifyNumber; + } +}); + +// node_modules/yaml/dist/schema/core/float.js +var require_float = __commonJS({ + "node_modules/yaml/dist/schema/core/float.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var stringifyNumber = require_stringifyNumber(); + var floatNaN = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/, + resolve: (str) => str.slice(-3).toLowerCase() === "nan" ? NaN : str[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, + stringify: stringifyNumber.stringifyNumber + }; + var floatExp = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "EXP", + test: /^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)[eE][-+]?[0-9]+$/, + resolve: (str) => parseFloat(str), + stringify(node) { + const num = Number(node.value); + return isFinite(num) ? num.toExponential() : stringifyNumber.stringifyNumber(node); + } + }; + var float = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^[-+]?(?:\.[0-9]+|[0-9]+\.[0-9]*)$/, + resolve(str) { + const node = new Scalar.Scalar(parseFloat(str)); + const dot = str.indexOf("."); + if (dot !== -1 && str[str.length - 1] === "0") + node.minFractionDigits = str.length - dot - 1; + return node; + }, + stringify: stringifyNumber.stringifyNumber + }; + exports.float = float; + exports.floatExp = floatExp; + exports.floatNaN = floatNaN; + } +}); + +// node_modules/yaml/dist/schema/core/int.js +var require_int = __commonJS({ + "node_modules/yaml/dist/schema/core/int.js"(exports) { + "use strict"; + var stringifyNumber = require_stringifyNumber(); + var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value); + var intResolve = (str, offset, radix, { intAsBigInt }) => intAsBigInt ? BigInt(str) : parseInt(str.substring(offset), radix); + function intStringify(node, radix, prefix) { + const { value } = node; + if (intIdentify(value) && value >= 0) + return prefix + value.toString(radix); + return stringifyNumber.stringifyNumber(node); + } + var intOct = { + identify: (value) => intIdentify(value) && value >= 0, + default: true, + tag: "tag:yaml.org,2002:int", + format: "OCT", + test: /^0o[0-7]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 8, opt), + stringify: (node) => intStringify(node, 8, "0o") + }; + var int = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^[-+]?[0-9]+$/, + resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt), + stringify: stringifyNumber.stringifyNumber + }; + var intHex = { + identify: (value) => intIdentify(value) && value >= 0, + default: true, + tag: "tag:yaml.org,2002:int", + format: "HEX", + test: /^0x[0-9a-fA-F]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt), + stringify: (node) => intStringify(node, 16, "0x") + }; + exports.int = int; + exports.intHex = intHex; + exports.intOct = intOct; + } +}); + +// node_modules/yaml/dist/schema/core/schema.js +var require_schema = __commonJS({ + "node_modules/yaml/dist/schema/core/schema.js"(exports) { + "use strict"; + var map = require_map(); + var _null = require_null(); + var seq = require_seq(); + var string = require_string(); + var bool = require_bool(); + var float = require_float(); + var int = require_int(); + var schema = [ + map.map, + seq.seq, + string.string, + _null.nullTag, + bool.boolTag, + int.intOct, + int.int, + int.intHex, + float.floatNaN, + float.floatExp, + float.float + ]; + exports.schema = schema; + } +}); + +// node_modules/yaml/dist/schema/json/schema.js +var require_schema2 = __commonJS({ + "node_modules/yaml/dist/schema/json/schema.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var map = require_map(); + var seq = require_seq(); + function intIdentify(value) { + return typeof value === "bigint" || Number.isInteger(value); + } + var stringifyJSON = ({ value }) => JSON.stringify(value); + var jsonScalars = [ + { + identify: (value) => typeof value === "string", + default: true, + tag: "tag:yaml.org,2002:str", + resolve: (str) => str, + stringify: stringifyJSON + }, + { + identify: (value) => value == null, + createNode: () => new Scalar.Scalar(null), + default: true, + tag: "tag:yaml.org,2002:null", + test: /^null$/, + resolve: () => null, + stringify: stringifyJSON + }, + { + identify: (value) => typeof value === "boolean", + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^true$|^false$/, + resolve: (str) => str === "true", + stringify: stringifyJSON + }, + { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^-?(?:0|[1-9][0-9]*)$/, + resolve: (str, _onError, { intAsBigInt }) => intAsBigInt ? BigInt(str) : parseInt(str, 10), + stringify: ({ value }) => intIdentify(value) ? value.toString() : JSON.stringify(value) + }, + { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$/, + resolve: (str) => parseFloat(str), + stringify: stringifyJSON + } + ]; + var jsonError = { + default: true, + tag: "", + test: /^/, + resolve(str, onError) { + onError(`Unresolved plain scalar ${JSON.stringify(str)}`); + return str; + } + }; + var schema = [map.map, seq.seq].concat(jsonScalars, jsonError); + exports.schema = schema; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/binary.js +var require_binary = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/binary.js"(exports) { + "use strict"; + var node_buffer = __require("buffer"); + var Scalar = require_Scalar(); + var stringifyString = require_stringifyString(); + var binary = { + identify: (value) => value instanceof Uint8Array, + // Buffer inherits from Uint8Array + default: false, + tag: "tag:yaml.org,2002:binary", + /** + * Returns a Buffer in node and an Uint8Array in browsers + * + * To use the resulting buffer as an image, you'll want to do something like: + * + * const blob = new Blob([buffer], { type: 'image/jpeg' }) + * document.querySelector('#photo').src = URL.createObjectURL(blob) + */ + resolve(src, onError) { + if (typeof node_buffer.Buffer === "function") { + return node_buffer.Buffer.from(src, "base64"); + } else if (typeof atob === "function") { + const str = atob(src.replace(/[\n\r]/g, "")); + const buffer = new Uint8Array(str.length); + for (let i = 0; i < str.length; ++i) + buffer[i] = str.charCodeAt(i); + return buffer; + } else { + onError("This environment does not support reading binary tags; either Buffer or atob is required"); + return src; + } + }, + stringify({ comment, type, value }, ctx, onComment, onChompKeep) { + if (!value) + return ""; + const buf = value; + let str; + if (typeof node_buffer.Buffer === "function") { + str = buf instanceof node_buffer.Buffer ? buf.toString("base64") : node_buffer.Buffer.from(buf.buffer).toString("base64"); + } else if (typeof btoa === "function") { + let s = ""; + for (let i = 0; i < buf.length; ++i) + s += String.fromCharCode(buf[i]); + str = btoa(s); + } else { + throw new Error("This environment does not support writing binary tags; either Buffer or btoa is required"); + } + type ?? (type = Scalar.Scalar.BLOCK_LITERAL); + if (type !== Scalar.Scalar.QUOTE_DOUBLE) { + const lineWidth = Math.max(ctx.options.lineWidth - ctx.indent.length, ctx.options.minContentWidth); + const n = Math.ceil(str.length / lineWidth); + const lines = new Array(n); + for (let i = 0, o = 0; i < n; ++i, o += lineWidth) { + lines[i] = str.substr(o, lineWidth); + } + str = lines.join(type === Scalar.Scalar.BLOCK_LITERAL ? "\n" : " "); + } + return stringifyString.stringifyString({ comment, type, value: str }, ctx, onComment, onChompKeep); + } + }; + exports.binary = binary; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/pairs.js +var require_pairs = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/pairs.js"(exports) { + "use strict"; + var identity = require_identity(); + var Pair = require_Pair(); + var Scalar = require_Scalar(); + var YAMLSeq = require_YAMLSeq(); + function resolvePairs(seq, onError) { + if (identity.isSeq(seq)) { + for (let i = 0; i < seq.items.length; ++i) { + let item = seq.items[i]; + if (identity.isPair(item)) + continue; + else if (identity.isMap(item)) { + if (item.items.length > 1) + onError("Each pair must have its own sequence indicator"); + const pair = item.items[0] || new Pair.Pair(new Scalar.Scalar(null)); + if (item.commentBefore) + pair.key.commentBefore = pair.key.commentBefore ? `${item.commentBefore} +${pair.key.commentBefore}` : item.commentBefore; + if (item.comment) { + const cn = pair.value ?? pair.key; + cn.comment = cn.comment ? `${item.comment} +${cn.comment}` : item.comment; + } + item = pair; + } + seq.items[i] = identity.isPair(item) ? item : new Pair.Pair(item); + } + } else + onError("Expected a sequence for this tag"); + return seq; + } + function createPairs(schema, iterable, ctx) { + const { replacer } = ctx; + const pairs2 = new YAMLSeq.YAMLSeq(schema); + pairs2.tag = "tag:yaml.org,2002:pairs"; + let i = 0; + if (iterable && Symbol.iterator in Object(iterable)) + for (let it of iterable) { + if (typeof replacer === "function") + it = replacer.call(iterable, String(i++), it); + let key, value; + if (Array.isArray(it)) { + if (it.length === 2) { + key = it[0]; + value = it[1]; + } else + throw new TypeError(`Expected [key, value] tuple: ${it}`); + } else if (it && it instanceof Object) { + const keys = Object.keys(it); + if (keys.length === 1) { + key = keys[0]; + value = it[key]; + } else { + throw new TypeError(`Expected tuple with one key, not ${keys.length} keys`); + } + } else { + key = it; + } + pairs2.items.push(Pair.createPair(key, value, ctx)); + } + return pairs2; + } + var pairs = { + collection: "seq", + default: false, + tag: "tag:yaml.org,2002:pairs", + resolve: resolvePairs, + createNode: createPairs + }; + exports.createPairs = createPairs; + exports.pairs = pairs; + exports.resolvePairs = resolvePairs; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/omap.js +var require_omap = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/omap.js"(exports) { + "use strict"; + var identity = require_identity(); + var toJS = require_toJS(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var pairs = require_pairs(); + var YAMLOMap = class _YAMLOMap extends YAMLSeq.YAMLSeq { + constructor() { + super(); + this.add = YAMLMap.YAMLMap.prototype.add.bind(this); + this.delete = YAMLMap.YAMLMap.prototype.delete.bind(this); + this.get = YAMLMap.YAMLMap.prototype.get.bind(this); + this.has = YAMLMap.YAMLMap.prototype.has.bind(this); + this.set = YAMLMap.YAMLMap.prototype.set.bind(this); + this.tag = _YAMLOMap.tag; + } + /** + * If `ctx` is given, the return type is actually `Map`, + * but TypeScript won't allow widening the signature of a child method. + */ + toJSON(_, ctx) { + if (!ctx) + return super.toJSON(_); + const map = /* @__PURE__ */ new Map(); + if (ctx?.onCreate) + ctx.onCreate(map); + for (const pair of this.items) { + let key, value; + if (identity.isPair(pair)) { + key = toJS.toJS(pair.key, "", ctx); + value = toJS.toJS(pair.value, key, ctx); + } else { + key = toJS.toJS(pair, "", ctx); + } + if (map.has(key)) + throw new Error("Ordered maps must not include duplicate keys"); + map.set(key, value); + } + return map; + } + static from(schema, iterable, ctx) { + const pairs$1 = pairs.createPairs(schema, iterable, ctx); + const omap2 = new this(); + omap2.items = pairs$1.items; + return omap2; + } + }; + YAMLOMap.tag = "tag:yaml.org,2002:omap"; + var omap = { + collection: "seq", + identify: (value) => value instanceof Map, + nodeClass: YAMLOMap, + default: false, + tag: "tag:yaml.org,2002:omap", + resolve(seq, onError) { + const pairs$1 = pairs.resolvePairs(seq, onError); + const seenKeys = []; + for (const { key } of pairs$1.items) { + if (identity.isScalar(key)) { + if (seenKeys.includes(key.value)) { + onError(`Ordered maps must not include duplicate keys: ${key.value}`); + } else { + seenKeys.push(key.value); + } + } + } + return Object.assign(new YAMLOMap(), pairs$1); + }, + createNode: (schema, iterable, ctx) => YAMLOMap.from(schema, iterable, ctx) + }; + exports.YAMLOMap = YAMLOMap; + exports.omap = omap; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/bool.js +var require_bool2 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/bool.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + function boolStringify({ value, source }, ctx) { + const boolObj = value ? trueTag : falseTag; + if (source && boolObj.test.test(source)) + return source; + return value ? ctx.options.trueStr : ctx.options.falseStr; + } + var trueTag = { + identify: (value) => value === true, + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:Y|y|[Yy]es|YES|[Tt]rue|TRUE|[Oo]n|ON)$/, + resolve: () => new Scalar.Scalar(true), + stringify: boolStringify + }; + var falseTag = { + identify: (value) => value === false, + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:N|n|[Nn]o|NO|[Ff]alse|FALSE|[Oo]ff|OFF)$/, + resolve: () => new Scalar.Scalar(false), + stringify: boolStringify + }; + exports.falseTag = falseTag; + exports.trueTag = trueTag; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/float.js +var require_float2 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/float.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var stringifyNumber = require_stringifyNumber(); + var floatNaN = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/, + resolve: (str) => str.slice(-3).toLowerCase() === "nan" ? NaN : str[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, + stringify: stringifyNumber.stringifyNumber + }; + var floatExp = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "EXP", + test: /^[-+]?(?:[0-9][0-9_]*)?(?:\.[0-9_]*)?[eE][-+]?[0-9]+$/, + resolve: (str) => parseFloat(str.replace(/_/g, "")), + stringify(node) { + const num = Number(node.value); + return isFinite(num) ? num.toExponential() : stringifyNumber.stringifyNumber(node); + } + }; + var float = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*$/, + resolve(str) { + const node = new Scalar.Scalar(parseFloat(str.replace(/_/g, ""))); + const dot = str.indexOf("."); + if (dot !== -1) { + const f = str.substring(dot + 1).replace(/_/g, ""); + if (f[f.length - 1] === "0") + node.minFractionDigits = f.length; + } + return node; + }, + stringify: stringifyNumber.stringifyNumber + }; + exports.float = float; + exports.floatExp = floatExp; + exports.floatNaN = floatNaN; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/int.js +var require_int2 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/int.js"(exports) { + "use strict"; + var stringifyNumber = require_stringifyNumber(); + var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value); + function intResolve(str, offset, radix, { intAsBigInt }) { + const sign = str[0]; + if (sign === "-" || sign === "+") + offset += 1; + str = str.substring(offset).replace(/_/g, ""); + if (intAsBigInt) { + switch (radix) { + case 2: + str = `0b${str}`; + break; + case 8: + str = `0o${str}`; + break; + case 16: + str = `0x${str}`; + break; + } + const n2 = BigInt(str); + return sign === "-" ? BigInt(-1) * n2 : n2; + } + const n = parseInt(str, radix); + return sign === "-" ? -1 * n : n; + } + function intStringify(node, radix, prefix) { + const { value } = node; + if (intIdentify(value)) { + const str = value.toString(radix); + return value < 0 ? "-" + prefix + str.substr(1) : prefix + str; + } + return stringifyNumber.stringifyNumber(node); + } + var intBin = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + format: "BIN", + test: /^[-+]?0b[0-1_]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 2, opt), + stringify: (node) => intStringify(node, 2, "0b") + }; + var intOct = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + format: "OCT", + test: /^[-+]?0[0-7_]+$/, + resolve: (str, _onError, opt) => intResolve(str, 1, 8, opt), + stringify: (node) => intStringify(node, 8, "0") + }; + var int = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^[-+]?[0-9][0-9_]*$/, + resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt), + stringify: stringifyNumber.stringifyNumber + }; + var intHex = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + format: "HEX", + test: /^[-+]?0x[0-9a-fA-F_]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt), + stringify: (node) => intStringify(node, 16, "0x") + }; + exports.int = int; + exports.intBin = intBin; + exports.intHex = intHex; + exports.intOct = intOct; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/set.js +var require_set = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/set.js"(exports) { + "use strict"; + var identity = require_identity(); + var Pair = require_Pair(); + var YAMLMap = require_YAMLMap(); + var YAMLSet = class _YAMLSet extends YAMLMap.YAMLMap { + constructor(schema) { + super(schema); + this.tag = _YAMLSet.tag; + } + add(key) { + let pair; + if (identity.isPair(key)) + pair = key; + else if (key && typeof key === "object" && "key" in key && "value" in key && key.value === null) + pair = new Pair.Pair(key.key, null); + else + pair = new Pair.Pair(key, null); + const prev = YAMLMap.findPair(this.items, pair.key); + if (!prev) + this.items.push(pair); + } + /** + * If `keepPair` is `true`, returns the Pair matching `key`. + * Otherwise, returns the value of that Pair's key. + */ + get(key, keepPair) { + const pair = YAMLMap.findPair(this.items, key); + return !keepPair && identity.isPair(pair) ? identity.isScalar(pair.key) ? pair.key.value : pair.key : pair; + } + set(key, value) { + if (typeof value !== "boolean") + throw new Error(`Expected boolean value for set(key, value) in a YAML set, not ${typeof value}`); + const prev = YAMLMap.findPair(this.items, key); + if (prev && !value) { + this.items.splice(this.items.indexOf(prev), 1); + } else if (!prev && value) { + this.items.push(new Pair.Pair(key)); + } + } + toJSON(_, ctx) { + return super.toJSON(_, ctx, Set); + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + if (this.hasAllNullValues(true)) + return super.toString(Object.assign({}, ctx, { allNullValues: true }), onComment, onChompKeep); + else + throw new Error("Set items must all have null values"); + } + static from(schema, iterable, ctx) { + const { replacer } = ctx; + const set2 = new this(schema); + if (iterable && Symbol.iterator in Object(iterable)) + for (let value of iterable) { + if (typeof replacer === "function") + value = replacer.call(iterable, value, value); + set2.items.push(Pair.createPair(value, null, ctx)); + } + return set2; + } + }; + YAMLSet.tag = "tag:yaml.org,2002:set"; + var set = { + collection: "map", + identify: (value) => value instanceof Set, + nodeClass: YAMLSet, + default: false, + tag: "tag:yaml.org,2002:set", + createNode: (schema, iterable, ctx) => YAMLSet.from(schema, iterable, ctx), + resolve(map, onError) { + if (identity.isMap(map)) { + if (map.hasAllNullValues(true)) + return Object.assign(new YAMLSet(), map); + else + onError("Set items must all have null values"); + } else + onError("Expected a mapping for this tag"); + return map; + } + }; + exports.YAMLSet = YAMLSet; + exports.set = set; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +var require_timestamp = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/timestamp.js"(exports) { + "use strict"; + var stringifyNumber = require_stringifyNumber(); + function parseSexagesimal(str, asBigInt) { + const sign = str[0]; + const parts = sign === "-" || sign === "+" ? str.substring(1) : str; + const num = (n) => asBigInt ? BigInt(n) : Number(n); + const res = parts.replace(/_/g, "").split(":").reduce((res2, p) => res2 * num(60) + num(p), num(0)); + return sign === "-" ? num(-1) * res : res; + } + function stringifySexagesimal(node) { + let { value } = node; + let num = (n) => n; + if (typeof value === "bigint") + num = (n) => BigInt(n); + else if (isNaN(value) || !isFinite(value)) + return stringifyNumber.stringifyNumber(node); + let sign = ""; + if (value < 0) { + sign = "-"; + value *= num(-1); + } + const _60 = num(60); + const parts = [value % _60]; + if (value < 60) { + parts.unshift(0); + } else { + value = (value - parts[0]) / _60; + parts.unshift(value % _60); + if (value >= 60) { + value = (value - parts[0]) / _60; + parts.unshift(value); + } + } + return sign + parts.map((n) => String(n).padStart(2, "0")).join(":").replace(/000000\d*$/, ""); + } + var intTime = { + identify: (value) => typeof value === "bigint" || Number.isInteger(value), + default: true, + tag: "tag:yaml.org,2002:int", + format: "TIME", + test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/, + resolve: (str, _onError, { intAsBigInt }) => parseSexagesimal(str, intAsBigInt), + stringify: stringifySexagesimal + }; + var floatTime = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "TIME", + test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*$/, + resolve: (str) => parseSexagesimal(str, false), + stringify: stringifySexagesimal + }; + var timestamp = { + identify: (value) => value instanceof Date, + default: true, + tag: "tag:yaml.org,2002:timestamp", + // If the time zone is omitted, the timestamp is assumed to be specified in UTC. The time part + // may be omitted altogether, resulting in a date format. In such a case, the time part is + // assumed to be 00:00:00Z (start of day, UTC). + test: RegExp("^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})(?:(?:t|T|[ \\t]+)([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?)?$"), + resolve(str) { + const match = str.match(timestamp.test); + if (!match) + throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd"); + const [, year, month, day, hour, minute, second] = match.map(Number); + const millisec = match[7] ? Number((match[7] + "00").substr(1, 3)) : 0; + let date = Date.UTC(year, month - 1, day, hour || 0, minute || 0, second || 0, millisec); + const tz = match[8]; + if (tz && tz !== "Z") { + let d = parseSexagesimal(tz, false); + if (Math.abs(d) < 30) + d *= 60; + date -= 6e4 * d; + } + return new Date(date); + }, + stringify: ({ value }) => value?.toISOString().replace(/(T00:00:00)?\.000Z$/, "") ?? "" + }; + exports.floatTime = floatTime; + exports.intTime = intTime; + exports.timestamp = timestamp; + } +}); + +// node_modules/yaml/dist/schema/yaml-1.1/schema.js +var require_schema3 = __commonJS({ + "node_modules/yaml/dist/schema/yaml-1.1/schema.js"(exports) { + "use strict"; + var map = require_map(); + var _null = require_null(); + var seq = require_seq(); + var string = require_string(); + var binary = require_binary(); + var bool = require_bool2(); + var float = require_float2(); + var int = require_int2(); + var merge = require_merge(); + var omap = require_omap(); + var pairs = require_pairs(); + var set = require_set(); + var timestamp = require_timestamp(); + var schema = [ + map.map, + seq.seq, + string.string, + _null.nullTag, + bool.trueTag, + bool.falseTag, + int.intBin, + int.intOct, + int.int, + int.intHex, + float.floatNaN, + float.floatExp, + float.float, + binary.binary, + merge.merge, + omap.omap, + pairs.pairs, + set.set, + timestamp.intTime, + timestamp.floatTime, + timestamp.timestamp + ]; + exports.schema = schema; + } +}); + +// node_modules/yaml/dist/schema/tags.js +var require_tags = __commonJS({ + "node_modules/yaml/dist/schema/tags.js"(exports) { + "use strict"; + var map = require_map(); + var _null = require_null(); + var seq = require_seq(); + var string = require_string(); + var bool = require_bool(); + var float = require_float(); + var int = require_int(); + var schema = require_schema(); + var schema$1 = require_schema2(); + var binary = require_binary(); + var merge = require_merge(); + var omap = require_omap(); + var pairs = require_pairs(); + var schema$2 = require_schema3(); + var set = require_set(); + var timestamp = require_timestamp(); + var schemas = /* @__PURE__ */ new Map([ + ["core", schema.schema], + ["failsafe", [map.map, seq.seq, string.string]], + ["json", schema$1.schema], + ["yaml11", schema$2.schema], + ["yaml-1.1", schema$2.schema] + ]); + var tagsByName = { + binary: binary.binary, + bool: bool.boolTag, + float: float.float, + floatExp: float.floatExp, + floatNaN: float.floatNaN, + floatTime: timestamp.floatTime, + int: int.int, + intHex: int.intHex, + intOct: int.intOct, + intTime: timestamp.intTime, + map: map.map, + merge: merge.merge, + null: _null.nullTag, + omap: omap.omap, + pairs: pairs.pairs, + seq: seq.seq, + set: set.set, + timestamp: timestamp.timestamp + }; + var coreKnownTags = { + "tag:yaml.org,2002:binary": binary.binary, + "tag:yaml.org,2002:merge": merge.merge, + "tag:yaml.org,2002:omap": omap.omap, + "tag:yaml.org,2002:pairs": pairs.pairs, + "tag:yaml.org,2002:set": set.set, + "tag:yaml.org,2002:timestamp": timestamp.timestamp + }; + function getTags(customTags, schemaName, addMergeTag) { + const schemaTags = schemas.get(schemaName); + if (schemaTags && !customTags) { + return addMergeTag && !schemaTags.includes(merge.merge) ? schemaTags.concat(merge.merge) : schemaTags.slice(); + } + let tags = schemaTags; + if (!tags) { + if (Array.isArray(customTags)) + tags = []; + else { + const keys = Array.from(schemas.keys()).filter((key) => key !== "yaml11").map((key) => JSON.stringify(key)).join(", "); + throw new Error(`Unknown schema "${schemaName}"; use one of ${keys} or define customTags array`); + } + } + if (Array.isArray(customTags)) { + for (const tag of customTags) + tags = tags.concat(tag); + } else if (typeof customTags === "function") { + tags = customTags(tags.slice()); + } + if (addMergeTag) + tags = tags.concat(merge.merge); + return tags.reduce((tags2, tag) => { + const tagObj = typeof tag === "string" ? tagsByName[tag] : tag; + if (!tagObj) { + const tagName = JSON.stringify(tag); + const keys = Object.keys(tagsByName).map((key) => JSON.stringify(key)).join(", "); + throw new Error(`Unknown custom tag ${tagName}; use one of ${keys}`); + } + if (!tags2.includes(tagObj)) + tags2.push(tagObj); + return tags2; + }, []); + } + exports.coreKnownTags = coreKnownTags; + exports.getTags = getTags; + } +}); + +// node_modules/yaml/dist/schema/Schema.js +var require_Schema = __commonJS({ + "node_modules/yaml/dist/schema/Schema.js"(exports) { + "use strict"; + var identity = require_identity(); + var map = require_map(); + var seq = require_seq(); + var string = require_string(); + var tags = require_tags(); + var sortMapEntriesByKey = (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0; + var Schema = class _Schema { + constructor({ compat, customTags, merge, resolveKnownTags, schema, sortMapEntries, toStringDefaults }) { + this.compat = Array.isArray(compat) ? tags.getTags(compat, "compat") : compat ? tags.getTags(null, compat) : null; + this.name = typeof schema === "string" && schema || "core"; + this.knownTags = resolveKnownTags ? tags.coreKnownTags : {}; + this.tags = tags.getTags(customTags, this.name, merge); + this.toStringOptions = toStringDefaults ?? null; + Object.defineProperty(this, identity.MAP, { value: map.map }); + Object.defineProperty(this, identity.SCALAR, { value: string.string }); + Object.defineProperty(this, identity.SEQ, { value: seq.seq }); + this.sortMapEntries = typeof sortMapEntries === "function" ? sortMapEntries : sortMapEntries === true ? sortMapEntriesByKey : null; + } + clone() { + const copy = Object.create(_Schema.prototype, Object.getOwnPropertyDescriptors(this)); + copy.tags = this.tags.slice(); + return copy; + } + }; + exports.Schema = Schema; + } +}); + +// node_modules/yaml/dist/stringify/stringifyDocument.js +var require_stringifyDocument = __commonJS({ + "node_modules/yaml/dist/stringify/stringifyDocument.js"(exports) { + "use strict"; + var identity = require_identity(); + var stringify2 = require_stringify(); + var stringifyComment = require_stringifyComment(); + function stringifyDocument(doc, options) { + const lines = []; + let hasDirectives = options.directives === true; + if (options.directives !== false && doc.directives) { + const dir = doc.directives.toString(doc); + if (dir) { + lines.push(dir); + hasDirectives = true; + } else if (doc.directives.docStart) + hasDirectives = true; + } + if (hasDirectives) + lines.push("---"); + const ctx = stringify2.createStringifyContext(doc, options); + const { commentString } = ctx.options; + if (doc.commentBefore) { + if (lines.length !== 1) + lines.unshift(""); + const cs = commentString(doc.commentBefore); + lines.unshift(stringifyComment.indentComment(cs, "")); + } + let chompKeep = false; + let contentComment = null; + if (doc.contents) { + if (identity.isNode(doc.contents)) { + if (doc.contents.spaceBefore && hasDirectives) + lines.push(""); + if (doc.contents.commentBefore) { + const cs = commentString(doc.contents.commentBefore); + lines.push(stringifyComment.indentComment(cs, "")); + } + ctx.forceBlockIndent = !!doc.comment; + contentComment = doc.contents.comment; + } + const onChompKeep = contentComment ? void 0 : () => chompKeep = true; + let body = stringify2.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep); + if (contentComment) + body += stringifyComment.lineComment(body, "", commentString(contentComment)); + if ((body[0] === "|" || body[0] === ">") && lines[lines.length - 1] === "---") { + lines[lines.length - 1] = `--- ${body}`; + } else + lines.push(body); + } else { + lines.push(stringify2.stringify(doc.contents, ctx)); + } + if (doc.directives?.docEnd) { + if (doc.comment) { + const cs = commentString(doc.comment); + if (cs.includes("\n")) { + lines.push("..."); + lines.push(stringifyComment.indentComment(cs, "")); + } else { + lines.push(`... ${cs}`); + } + } else { + lines.push("..."); + } + } else { + let dc = doc.comment; + if (dc && chompKeep) + dc = dc.replace(/^\n+/, ""); + if (dc) { + if ((!chompKeep || contentComment) && lines[lines.length - 1] !== "") + lines.push(""); + lines.push(stringifyComment.indentComment(commentString(dc), "")); + } + } + return lines.join("\n") + "\n"; + } + exports.stringifyDocument = stringifyDocument; + } +}); + +// node_modules/yaml/dist/doc/Document.js +var require_Document = __commonJS({ + "node_modules/yaml/dist/doc/Document.js"(exports) { + "use strict"; + var Alias = require_Alias(); + var Collection = require_Collection(); + var identity = require_identity(); + var Pair = require_Pair(); + var toJS = require_toJS(); + var Schema = require_Schema(); + var stringifyDocument = require_stringifyDocument(); + var anchors = require_anchors(); + var applyReviver = require_applyReviver(); + var createNode = require_createNode(); + var directives = require_directives(); + var Document = class _Document { + constructor(value, replacer, options) { + this.commentBefore = null; + this.comment = null; + this.errors = []; + this.warnings = []; + Object.defineProperty(this, identity.NODE_TYPE, { value: identity.DOC }); + let _replacer = null; + if (typeof replacer === "function" || Array.isArray(replacer)) { + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + replacer = void 0; + } + const opt = Object.assign({ + intAsBigInt: false, + keepSourceTokens: false, + logLevel: "warn", + prettyErrors: true, + strict: true, + stringKeys: false, + uniqueKeys: true, + version: "1.2" + }, options); + this.options = opt; + let { version } = opt; + if (options?._directives) { + this.directives = options._directives.atDocument(); + if (this.directives.yaml.explicit) + version = this.directives.yaml.version; + } else + this.directives = new directives.Directives({ version }); + this.setSchema(version, options); + this.contents = value === void 0 ? null : this.createNode(value, _replacer, options); + } + /** + * Create a deep copy of this Document and its contents. + * + * Custom Node values that inherit from `Object` still refer to their original instances. + */ + clone() { + const copy = Object.create(_Document.prototype, { + [identity.NODE_TYPE]: { value: identity.DOC } + }); + copy.commentBefore = this.commentBefore; + copy.comment = this.comment; + copy.errors = this.errors.slice(); + copy.warnings = this.warnings.slice(); + copy.options = Object.assign({}, this.options); + if (this.directives) + copy.directives = this.directives.clone(); + copy.schema = this.schema.clone(); + copy.contents = identity.isNode(this.contents) ? this.contents.clone(copy.schema) : this.contents; + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** Adds a value to the document. */ + add(value) { + if (assertCollection(this.contents)) + this.contents.add(value); + } + /** Adds a value to the document. */ + addIn(path, value) { + if (assertCollection(this.contents)) + this.contents.addIn(path, value); + } + /** + * Create a new `Alias` node, ensuring that the target `node` has the required anchor. + * + * If `node` already has an anchor, `name` is ignored. + * Otherwise, the `node.anchor` value will be set to `name`, + * or if an anchor with that name is already present in the document, + * `name` will be used as a prefix for a new unique anchor. + * If `name` is undefined, the generated anchor will use 'a' as a prefix. + */ + createAlias(node, name) { + if (!node.anchor) { + const prev = anchors.anchorNames(this); + node.anchor = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + !name || prev.has(name) ? anchors.findNewAnchor(name || "a", prev) : name; + } + return new Alias.Alias(node.anchor); + } + createNode(value, replacer, options) { + let _replacer = void 0; + if (typeof replacer === "function") { + value = replacer.call({ "": value }, "", value); + _replacer = replacer; + } else if (Array.isArray(replacer)) { + const keyToStr = (v) => typeof v === "number" || v instanceof String || v instanceof Number; + const asStr = replacer.filter(keyToStr).map(String); + if (asStr.length > 0) + replacer = replacer.concat(asStr); + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + replacer = void 0; + } + const { aliasDuplicateObjects, anchorPrefix, flow, keepUndefined, onTagObj, tag } = options ?? {}; + const { onAnchor, setAnchors, sourceObjects } = anchors.createNodeAnchors( + this, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + anchorPrefix || "a" + ); + const ctx = { + aliasDuplicateObjects: aliasDuplicateObjects ?? true, + keepUndefined: keepUndefined ?? false, + onAnchor, + onTagObj, + replacer: _replacer, + schema: this.schema, + sourceObjects + }; + const node = createNode.createNode(value, tag, ctx); + if (flow && identity.isCollection(node)) + node.flow = true; + setAnchors(); + return node; + } + /** + * Convert a key and a value into a `Pair` using the current schema, + * recursively wrapping all values as `Scalar` or `Collection` nodes. + */ + createPair(key, value, options = {}) { + const k = this.createNode(key, null, options); + const v = this.createNode(value, null, options); + return new Pair.Pair(k, v); + } + /** + * Removes a value from the document. + * @returns `true` if the item was found and removed. + */ + delete(key) { + return assertCollection(this.contents) ? this.contents.delete(key) : false; + } + /** + * Removes a value from the document. + * @returns `true` if the item was found and removed. + */ + deleteIn(path) { + if (Collection.isEmptyPath(path)) { + if (this.contents == null) + return false; + this.contents = null; + return true; + } + return assertCollection(this.contents) ? this.contents.deleteIn(path) : false; + } + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + get(key, keepScalar) { + return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : void 0; + } + /** + * Returns item at `path`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn(path, keepScalar) { + if (Collection.isEmptyPath(path)) + return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents; + return identity.isCollection(this.contents) ? this.contents.getIn(path, keepScalar) : void 0; + } + /** + * Checks if the document includes a value with the key `key`. + */ + has(key) { + return identity.isCollection(this.contents) ? this.contents.has(key) : false; + } + /** + * Checks if the document includes a value at `path`. + */ + hasIn(path) { + if (Collection.isEmptyPath(path)) + return this.contents !== void 0; + return identity.isCollection(this.contents) ? this.contents.hasIn(path) : false; + } + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + set(key, value) { + if (this.contents == null) { + this.contents = Collection.collectionFromPath(this.schema, [key], value); + } else if (assertCollection(this.contents)) { + this.contents.set(key, value); + } + } + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn(path, value) { + if (Collection.isEmptyPath(path)) { + this.contents = value; + } else if (this.contents == null) { + this.contents = Collection.collectionFromPath(this.schema, Array.from(path), value); + } else if (assertCollection(this.contents)) { + this.contents.setIn(path, value); + } + } + /** + * Change the YAML version and schema used by the document. + * A `null` version disables support for directives, explicit tags, anchors, and aliases. + * It also requires the `schema` option to be given as a `Schema` instance value. + * + * Overrides all previously set schema options. + */ + setSchema(version, options = {}) { + if (typeof version === "number") + version = String(version); + let opt; + switch (version) { + case "1.1": + if (this.directives) + this.directives.yaml.version = "1.1"; + else + this.directives = new directives.Directives({ version: "1.1" }); + opt = { resolveKnownTags: false, schema: "yaml-1.1" }; + break; + case "1.2": + case "next": + if (this.directives) + this.directives.yaml.version = version; + else + this.directives = new directives.Directives({ version }); + opt = { resolveKnownTags: true, schema: "core" }; + break; + case null: + if (this.directives) + delete this.directives; + opt = null; + break; + default: { + const sv = JSON.stringify(version); + throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`); + } + } + if (options.schema instanceof Object) + this.schema = options.schema; + else if (opt) + this.schema = new Schema.Schema(Object.assign(opt, options)); + else + throw new Error(`With a null YAML version, the { schema: Schema } option is required`); + } + // json & jsonArg are only used from toJSON() + toJS({ json, jsonArg, mapAsMap, maxAliasCount, onAnchor, reviver } = {}) { + const ctx = { + anchors: /* @__PURE__ */ new Map(), + doc: this, + keep: !json, + mapAsMap: mapAsMap === true, + mapKeyWarned: false, + maxAliasCount: typeof maxAliasCount === "number" ? maxAliasCount : 100 + }; + const res = toJS.toJS(this.contents, jsonArg ?? "", ctx); + if (typeof onAnchor === "function") + for (const { count, res: res2 } of ctx.anchors.values()) + onAnchor(res2, count); + return typeof reviver === "function" ? applyReviver.applyReviver(reviver, { "": res }, "", res) : res; + } + /** + * A JSON representation of the document `contents`. + * + * @param jsonArg Used by `JSON.stringify` to indicate the array index or + * property name. + */ + toJSON(jsonArg, onAnchor) { + return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor }); + } + /** A YAML representation of the document. */ + toString(options = {}) { + if (this.errors.length > 0) + throw new Error("Document with errors cannot be stringified"); + if ("indent" in options && (!Number.isInteger(options.indent) || Number(options.indent) <= 0)) { + const s = JSON.stringify(options.indent); + throw new Error(`"indent" option must be a positive integer, not ${s}`); + } + return stringifyDocument.stringifyDocument(this, options); + } + }; + function assertCollection(contents) { + if (identity.isCollection(contents)) + return true; + throw new Error("Expected a YAML collection as document contents"); + } + exports.Document = Document; + } +}); + +// node_modules/yaml/dist/errors.js +var require_errors = __commonJS({ + "node_modules/yaml/dist/errors.js"(exports) { + "use strict"; + var YAMLError = class extends Error { + constructor(name, pos, code, message) { + super(); + this.name = name; + this.code = code; + this.message = message; + this.pos = pos; + } + }; + var YAMLParseError = class extends YAMLError { + constructor(pos, code, message) { + super("YAMLParseError", pos, code, message); + } + }; + var YAMLWarning = class extends YAMLError { + constructor(pos, code, message) { + super("YAMLWarning", pos, code, message); + } + }; + var prettifyError = (src, lc) => (error) => { + if (error.pos[0] === -1) + return; + error.linePos = error.pos.map((pos) => lc.linePos(pos)); + const { line, col } = error.linePos[0]; + error.message += ` at line ${line}, column ${col}`; + let ci = col - 1; + let lineStr = src.substring(lc.lineStarts[line - 1], lc.lineStarts[line]).replace(/[\n\r]+$/, ""); + if (ci >= 60 && lineStr.length > 80) { + const trimStart = Math.min(ci - 39, lineStr.length - 79); + lineStr = "…" + lineStr.substring(trimStart); + ci -= trimStart - 1; + } + if (lineStr.length > 80) + lineStr = lineStr.substring(0, 79) + "…"; + if (line > 1 && /^ *$/.test(lineStr.substring(0, ci))) { + let prev = src.substring(lc.lineStarts[line - 2], lc.lineStarts[line - 1]); + if (prev.length > 80) + prev = prev.substring(0, 79) + "…\n"; + lineStr = prev + lineStr; + } + if (/[^ ]/.test(lineStr)) { + let count = 1; + const end = error.linePos[1]; + if (end?.line === line && end.col > col) { + count = Math.max(1, Math.min(end.col - col, 80 - ci)); + } + const pointer = " ".repeat(ci) + "^".repeat(count); + error.message += `: + +${lineStr} +${pointer} +`; + } + }; + exports.YAMLError = YAMLError; + exports.YAMLParseError = YAMLParseError; + exports.YAMLWarning = YAMLWarning; + exports.prettifyError = prettifyError; + } +}); + +// node_modules/yaml/dist/compose/resolve-props.js +var require_resolve_props = __commonJS({ + "node_modules/yaml/dist/compose/resolve-props.js"(exports) { + "use strict"; + function resolveProps(tokens, { flow, indicator, next, offset, onError, parentIndent, startOnNewline }) { + let spaceBefore = false; + let atNewline = startOnNewline; + let hasSpace = startOnNewline; + let comment = ""; + let commentSep = ""; + let hasNewline = false; + let reqSpace = false; + let tab = null; + let anchor = null; + let tag = null; + let newlineAfterProp = null; + let comma = null; + let found = null; + let start = null; + for (const token of tokens) { + if (reqSpace) { + if (token.type !== "space" && token.type !== "newline" && token.type !== "comma") + onError(token.offset, "MISSING_CHAR", "Tags and anchors must be separated from the next token by white space"); + reqSpace = false; + } + if (tab) { + if (atNewline && token.type !== "comment" && token.type !== "newline") { + onError(tab, "TAB_AS_INDENT", "Tabs are not allowed as indentation"); + } + tab = null; + } + switch (token.type) { + case "space": + if (!flow && (indicator !== "doc-start" || next?.type !== "flow-collection") && token.source.includes(" ")) { + tab = token; + } + hasSpace = true; + break; + case "comment": { + if (!hasSpace) + onError(token, "MISSING_CHAR", "Comments must be separated from other tokens by white space characters"); + const cb = token.source.substring(1) || " "; + if (!comment) + comment = cb; + else + comment += commentSep + cb; + commentSep = ""; + atNewline = false; + break; + } + case "newline": + if (atNewline) { + if (comment) + comment += token.source; + else if (!found || indicator !== "seq-item-ind") + spaceBefore = true; + } else + commentSep += token.source; + atNewline = true; + hasNewline = true; + if (anchor || tag) + newlineAfterProp = token; + hasSpace = true; + break; + case "anchor": + if (anchor) + onError(token, "MULTIPLE_ANCHORS", "A node can have at most one anchor"); + if (token.source.endsWith(":")) + onError(token.offset + token.source.length - 1, "BAD_ALIAS", "Anchor ending in : is ambiguous", true); + anchor = token; + start ?? (start = token.offset); + atNewline = false; + hasSpace = false; + reqSpace = true; + break; + case "tag": { + if (tag) + onError(token, "MULTIPLE_TAGS", "A node can have at most one tag"); + tag = token; + start ?? (start = token.offset); + atNewline = false; + hasSpace = false; + reqSpace = true; + break; + } + case indicator: + if (anchor || tag) + onError(token, "BAD_PROP_ORDER", `Anchors and tags must be after the ${token.source} indicator`); + if (found) + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${token.source} in ${flow ?? "collection"}`); + found = token; + atNewline = indicator === "seq-item-ind" || indicator === "explicit-key-ind"; + hasSpace = false; + break; + case "comma": + if (flow) { + if (comma) + onError(token, "UNEXPECTED_TOKEN", `Unexpected , in ${flow}`); + comma = token; + atNewline = false; + hasSpace = false; + break; + } + // else fallthrough + default: + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${token.type} token`); + atNewline = false; + hasSpace = false; + } + } + const last = tokens[tokens.length - 1]; + const end = last ? last.offset + last.source.length : offset; + if (reqSpace && next && next.type !== "space" && next.type !== "newline" && next.type !== "comma" && (next.type !== "scalar" || next.source !== "")) { + onError(next.offset, "MISSING_CHAR", "Tags and anchors must be separated from the next token by white space"); + } + if (tab && (atNewline && tab.indent <= parentIndent || next?.type === "block-map" || next?.type === "block-seq")) + onError(tab, "TAB_AS_INDENT", "Tabs are not allowed as indentation"); + return { + comma, + found, + spaceBefore, + comment, + hasNewline, + anchor, + tag, + newlineAfterProp, + end, + start: start ?? end + }; + } + exports.resolveProps = resolveProps; + } +}); + +// node_modules/yaml/dist/compose/util-contains-newline.js +var require_util_contains_newline = __commonJS({ + "node_modules/yaml/dist/compose/util-contains-newline.js"(exports) { + "use strict"; + function containsNewline(key) { + if (!key) + return null; + switch (key.type) { + case "alias": + case "scalar": + case "double-quoted-scalar": + case "single-quoted-scalar": + if (key.source.includes("\n")) + return true; + if (key.end) { + for (const st of key.end) + if (st.type === "newline") + return true; + } + return false; + case "flow-collection": + for (const it of key.items) { + for (const st of it.start) + if (st.type === "newline") + return true; + if (it.sep) { + for (const st of it.sep) + if (st.type === "newline") + return true; + } + if (containsNewline(it.key) || containsNewline(it.value)) + return true; + } + return false; + default: + return true; + } + } + exports.containsNewline = containsNewline; + } +}); + +// node_modules/yaml/dist/compose/util-flow-indent-check.js +var require_util_flow_indent_check = __commonJS({ + "node_modules/yaml/dist/compose/util-flow-indent-check.js"(exports) { + "use strict"; + var utilContainsNewline = require_util_contains_newline(); + function flowIndentCheck(indent, fc, onError) { + if (fc?.type === "flow-collection") { + const end = fc.end[0]; + if (end.indent === indent && (end.source === "]" || end.source === "}") && utilContainsNewline.containsNewline(fc)) { + const msg = "Flow end indicator should be more indented than parent"; + onError(end, "BAD_INDENT", msg, true); + } + } + } + exports.flowIndentCheck = flowIndentCheck; + } +}); + +// node_modules/yaml/dist/compose/util-map-includes.js +var require_util_map_includes = __commonJS({ + "node_modules/yaml/dist/compose/util-map-includes.js"(exports) { + "use strict"; + var identity = require_identity(); + function mapIncludes(ctx, items, search) { + const { uniqueKeys } = ctx.options; + if (uniqueKeys === false) + return false; + const isEqual = typeof uniqueKeys === "function" ? uniqueKeys : (a, b) => a === b || identity.isScalar(a) && identity.isScalar(b) && a.value === b.value; + return items.some((pair) => isEqual(pair.key, search)); + } + exports.mapIncludes = mapIncludes; + } +}); + +// node_modules/yaml/dist/compose/resolve-block-map.js +var require_resolve_block_map = __commonJS({ + "node_modules/yaml/dist/compose/resolve-block-map.js"(exports) { + "use strict"; + var Pair = require_Pair(); + var YAMLMap = require_YAMLMap(); + var resolveProps = require_resolve_props(); + var utilContainsNewline = require_util_contains_newline(); + var utilFlowIndentCheck = require_util_flow_indent_check(); + var utilMapIncludes = require_util_map_includes(); + var startColMsg = "All mapping items must start at the same column"; + function resolveBlockMap({ composeNode, composeEmptyNode }, ctx, bm, onError, tag) { + const NodeClass = tag?.nodeClass ?? YAMLMap.YAMLMap; + const map = new NodeClass(ctx.schema); + if (ctx.atRoot) + ctx.atRoot = false; + let offset = bm.offset; + let commentEnd = null; + for (const collItem of bm.items) { + const { start, key, sep, value } = collItem; + const keyProps = resolveProps.resolveProps(start, { + indicator: "explicit-key-ind", + next: key ?? sep?.[0], + offset, + onError, + parentIndent: bm.indent, + startOnNewline: true + }); + const implicitKey = !keyProps.found; + if (implicitKey) { + if (key) { + if (key.type === "block-seq") + onError(offset, "BLOCK_AS_IMPLICIT_KEY", "A block sequence may not be used as an implicit map key"); + else if ("indent" in key && key.indent !== bm.indent) + onError(offset, "BAD_INDENT", startColMsg); + } + if (!keyProps.anchor && !keyProps.tag && !sep) { + commentEnd = keyProps.end; + if (keyProps.comment) { + if (map.comment) + map.comment += "\n" + keyProps.comment; + else + map.comment = keyProps.comment; + } + continue; + } + if (keyProps.newlineAfterProp || utilContainsNewline.containsNewline(key)) { + onError(key ?? start[start.length - 1], "MULTILINE_IMPLICIT_KEY", "Implicit keys need to be on a single line"); + } + } else if (keyProps.found?.indent !== bm.indent) { + onError(offset, "BAD_INDENT", startColMsg); + } + ctx.atKey = true; + const keyStart = keyProps.end; + const keyNode = key ? composeNode(ctx, key, keyProps, onError) : composeEmptyNode(ctx, keyStart, start, null, keyProps, onError); + if (ctx.schema.compat) + utilFlowIndentCheck.flowIndentCheck(bm.indent, key, onError); + ctx.atKey = false; + if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode)) + onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique"); + const valueProps = resolveProps.resolveProps(sep ?? [], { + indicator: "map-value-ind", + next: value, + offset: keyNode.range[2], + onError, + parentIndent: bm.indent, + startOnNewline: !key || key.type === "block-scalar" + }); + offset = valueProps.end; + if (valueProps.found) { + if (implicitKey) { + if (value?.type === "block-map" && !valueProps.hasNewline) + onError(offset, "BLOCK_AS_IMPLICIT_KEY", "Nested mappings are not allowed in compact mappings"); + if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024) + onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key"); + } + const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep, null, valueProps, onError); + if (ctx.schema.compat) + utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError); + offset = valueNode.range[2]; + const pair = new Pair.Pair(keyNode, valueNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + map.items.push(pair); + } else { + if (implicitKey) + onError(keyNode.range, "MISSING_CHAR", "Implicit map keys need to be followed by map values"); + if (valueProps.comment) { + if (keyNode.comment) + keyNode.comment += "\n" + valueProps.comment; + else + keyNode.comment = valueProps.comment; + } + const pair = new Pair.Pair(keyNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + map.items.push(pair); + } + } + if (commentEnd && commentEnd < offset) + onError(commentEnd, "IMPOSSIBLE", "Map comment with trailing content"); + map.range = [bm.offset, offset, commentEnd ?? offset]; + return map; + } + exports.resolveBlockMap = resolveBlockMap; + } +}); + +// node_modules/yaml/dist/compose/resolve-block-seq.js +var require_resolve_block_seq = __commonJS({ + "node_modules/yaml/dist/compose/resolve-block-seq.js"(exports) { + "use strict"; + var YAMLSeq = require_YAMLSeq(); + var resolveProps = require_resolve_props(); + var utilFlowIndentCheck = require_util_flow_indent_check(); + function resolveBlockSeq({ composeNode, composeEmptyNode }, ctx, bs, onError, tag) { + const NodeClass = tag?.nodeClass ?? YAMLSeq.YAMLSeq; + const seq = new NodeClass(ctx.schema); + if (ctx.atRoot) + ctx.atRoot = false; + if (ctx.atKey) + ctx.atKey = false; + let offset = bs.offset; + let commentEnd = null; + for (const { start, value } of bs.items) { + const props = resolveProps.resolveProps(start, { + indicator: "seq-item-ind", + next: value, + offset, + onError, + parentIndent: bs.indent, + startOnNewline: true + }); + if (!props.found) { + if (props.anchor || props.tag || value) { + if (value?.type === "block-seq") + onError(props.end, "BAD_INDENT", "All sequence items must start at the same column"); + else + onError(offset, "MISSING_CHAR", "Sequence item without - indicator"); + } else { + commentEnd = props.end; + if (props.comment) + seq.comment = props.comment; + continue; + } + } + const node = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, start, null, props, onError); + if (ctx.schema.compat) + utilFlowIndentCheck.flowIndentCheck(bs.indent, value, onError); + offset = node.range[2]; + seq.items.push(node); + } + seq.range = [bs.offset, offset, commentEnd ?? offset]; + return seq; + } + exports.resolveBlockSeq = resolveBlockSeq; + } +}); + +// node_modules/yaml/dist/compose/resolve-end.js +var require_resolve_end = __commonJS({ + "node_modules/yaml/dist/compose/resolve-end.js"(exports) { + "use strict"; + function resolveEnd(end, offset, reqSpace, onError) { + let comment = ""; + if (end) { + let hasSpace = false; + let sep = ""; + for (const token of end) { + const { source, type } = token; + switch (type) { + case "space": + hasSpace = true; + break; + case "comment": { + if (reqSpace && !hasSpace) + onError(token, "MISSING_CHAR", "Comments must be separated from other tokens by white space characters"); + const cb = source.substring(1) || " "; + if (!comment) + comment = cb; + else + comment += sep + cb; + sep = ""; + break; + } + case "newline": + if (comment) + sep += source; + hasSpace = true; + break; + default: + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${type} at node end`); + } + offset += source.length; + } + } + return { comment, offset }; + } + exports.resolveEnd = resolveEnd; + } +}); + +// node_modules/yaml/dist/compose/resolve-flow-collection.js +var require_resolve_flow_collection = __commonJS({ + "node_modules/yaml/dist/compose/resolve-flow-collection.js"(exports) { + "use strict"; + var identity = require_identity(); + var Pair = require_Pair(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var resolveEnd = require_resolve_end(); + var resolveProps = require_resolve_props(); + var utilContainsNewline = require_util_contains_newline(); + var utilMapIncludes = require_util_map_includes(); + var blockMsg = "Block collections are not allowed within flow collections"; + var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq"); + function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) { + const isMap = fc.start.source === "{"; + const fcName = isMap ? "flow map" : "flow sequence"; + const NodeClass = tag?.nodeClass ?? (isMap ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq); + const coll = new NodeClass(ctx.schema); + coll.flow = true; + const atRoot = ctx.atRoot; + if (atRoot) + ctx.atRoot = false; + if (ctx.atKey) + ctx.atKey = false; + let offset = fc.offset + fc.start.source.length; + for (let i = 0; i < fc.items.length; ++i) { + const collItem = fc.items[i]; + const { start, key, sep, value } = collItem; + const props = resolveProps.resolveProps(start, { + flow: fcName, + indicator: "explicit-key-ind", + next: key ?? sep?.[0], + offset, + onError, + parentIndent: fc.indent, + startOnNewline: false + }); + if (!props.found) { + if (!props.anchor && !props.tag && !sep && !value) { + if (i === 0 && props.comma) + onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`); + else if (i < fc.items.length - 1) + onError(props.start, "UNEXPECTED_TOKEN", `Unexpected empty item in ${fcName}`); + if (props.comment) { + if (coll.comment) + coll.comment += "\n" + props.comment; + else + coll.comment = props.comment; + } + offset = props.end; + continue; + } + if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key)) + onError( + key, + // checked by containsNewline() + "MULTILINE_IMPLICIT_KEY", + "Implicit keys of flow sequence pairs need to be on a single line" + ); + } + if (i === 0) { + if (props.comma) + onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`); + } else { + if (!props.comma) + onError(props.start, "MISSING_CHAR", `Missing , between ${fcName} items`); + if (props.comment) { + let prevItemComment = ""; + loop: for (const st of start) { + switch (st.type) { + case "comma": + case "space": + break; + case "comment": + prevItemComment = st.source.substring(1); + break loop; + default: + break loop; + } + } + if (prevItemComment) { + let prev = coll.items[coll.items.length - 1]; + if (identity.isPair(prev)) + prev = prev.value ?? prev.key; + if (prev.comment) + prev.comment += "\n" + prevItemComment; + else + prev.comment = prevItemComment; + props.comment = props.comment.substring(prevItemComment.length + 1); + } + } + } + if (!isMap && !sep && !props.found) { + const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError); + coll.items.push(valueNode); + offset = valueNode.range[2]; + if (isBlock(value)) + onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg); + } else { + ctx.atKey = true; + const keyStart = props.end; + const keyNode = key ? composeNode(ctx, key, props, onError) : composeEmptyNode(ctx, keyStart, start, null, props, onError); + if (isBlock(key)) + onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg); + ctx.atKey = false; + const valueProps = resolveProps.resolveProps(sep ?? [], { + flow: fcName, + indicator: "map-value-ind", + next: value, + offset: keyNode.range[2], + onError, + parentIndent: fc.indent, + startOnNewline: false + }); + if (valueProps.found) { + if (!isMap && !props.found && ctx.options.strict) { + if (sep) + for (const st of sep) { + if (st === valueProps.found) + break; + if (st.type === "newline") { + onError(st, "MULTILINE_IMPLICIT_KEY", "Implicit keys of flow sequence pairs need to be on a single line"); + break; + } + } + if (props.start < valueProps.found.offset - 1024) + onError(valueProps.found, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit flow sequence key"); + } + } else if (value) { + if ("source" in value && value.source?.[0] === ":") + onError(value, "MISSING_CHAR", `Missing space after : in ${fcName}`); + else + onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`); + } + const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep, null, valueProps, onError) : null; + if (valueNode) { + if (isBlock(value)) + onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg); + } else if (valueProps.comment) { + if (keyNode.comment) + keyNode.comment += "\n" + valueProps.comment; + else + keyNode.comment = valueProps.comment; + } + const pair = new Pair.Pair(keyNode, valueNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + if (isMap) { + const map = coll; + if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode)) + onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique"); + map.items.push(pair); + } else { + const map = new YAMLMap.YAMLMap(ctx.schema); + map.flow = true; + map.items.push(pair); + const endRange = (valueNode ?? keyNode).range; + map.range = [keyNode.range[0], endRange[1], endRange[2]]; + coll.items.push(map); + } + offset = valueNode ? valueNode.range[2] : valueProps.end; + } + } + const expectedEnd = isMap ? "}" : "]"; + const [ce, ...ee] = fc.end; + let cePos = offset; + if (ce?.source === expectedEnd) + cePos = ce.offset + ce.source.length; + else { + const name = fcName[0].toUpperCase() + fcName.substring(1); + const msg = atRoot ? `${name} must end with a ${expectedEnd}` : `${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`; + onError(offset, atRoot ? "MISSING_CHAR" : "BAD_INDENT", msg); + if (ce && ce.source.length !== 1) + ee.unshift(ce); + } + if (ee.length > 0) { + const end = resolveEnd.resolveEnd(ee, cePos, ctx.options.strict, onError); + if (end.comment) { + if (coll.comment) + coll.comment += "\n" + end.comment; + else + coll.comment = end.comment; + } + coll.range = [fc.offset, cePos, end.offset]; + } else { + coll.range = [fc.offset, cePos, cePos]; + } + return coll; + } + exports.resolveFlowCollection = resolveFlowCollection; + } +}); + +// node_modules/yaml/dist/compose/compose-collection.js +var require_compose_collection = __commonJS({ + "node_modules/yaml/dist/compose/compose-collection.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var resolveBlockMap = require_resolve_block_map(); + var resolveBlockSeq = require_resolve_block_seq(); + var resolveFlowCollection = require_resolve_flow_collection(); + function resolveCollection(CN, ctx, token, onError, tagName, tag) { + const coll = token.type === "block-map" ? resolveBlockMap.resolveBlockMap(CN, ctx, token, onError, tag) : token.type === "block-seq" ? resolveBlockSeq.resolveBlockSeq(CN, ctx, token, onError, tag) : resolveFlowCollection.resolveFlowCollection(CN, ctx, token, onError, tag); + const Coll = coll.constructor; + if (tagName === "!" || tagName === Coll.tagName) { + coll.tag = Coll.tagName; + return coll; + } + if (tagName) + coll.tag = tagName; + return coll; + } + function composeCollection(CN, ctx, token, props, onError) { + const tagToken = props.tag; + const tagName = !tagToken ? null : ctx.directives.tagName(tagToken.source, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg)); + if (token.type === "block-seq") { + const { anchor, newlineAfterProp: nl } = props; + const lastProp = anchor && tagToken ? anchor.offset > tagToken.offset ? anchor : tagToken : anchor ?? tagToken; + if (lastProp && (!nl || nl.offset < lastProp.offset)) { + const message = "Missing newline after block sequence props"; + onError(lastProp, "MISSING_CHAR", message); + } + } + const expType = token.type === "block-map" ? "map" : token.type === "block-seq" ? "seq" : token.start.source === "{" ? "map" : "seq"; + if (!tagToken || !tagName || tagName === "!" || tagName === YAMLMap.YAMLMap.tagName && expType === "map" || tagName === YAMLSeq.YAMLSeq.tagName && expType === "seq") { + return resolveCollection(CN, ctx, token, onError, tagName); + } + let tag = ctx.schema.tags.find((t) => t.tag === tagName && t.collection === expType); + if (!tag) { + const kt = ctx.schema.knownTags[tagName]; + if (kt?.collection === expType) { + ctx.schema.tags.push(Object.assign({}, kt, { default: false })); + tag = kt; + } else { + if (kt) { + onError(tagToken, "BAD_COLLECTION_TYPE", `${kt.tag} used for ${expType} collection, but expects ${kt.collection ?? "scalar"}`, true); + } else { + onError(tagToken, "TAG_RESOLVE_FAILED", `Unresolved tag: ${tagName}`, true); + } + return resolveCollection(CN, ctx, token, onError, tagName); + } + } + const coll = resolveCollection(CN, ctx, token, onError, tagName, tag); + const res = tag.resolve?.(coll, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg), ctx.options) ?? coll; + const node = identity.isNode(res) ? res : new Scalar.Scalar(res); + node.range = coll.range; + node.tag = tagName; + if (tag?.format) + node.format = tag.format; + return node; + } + exports.composeCollection = composeCollection; + } +}); + +// node_modules/yaml/dist/compose/resolve-block-scalar.js +var require_resolve_block_scalar = __commonJS({ + "node_modules/yaml/dist/compose/resolve-block-scalar.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + function resolveBlockScalar(ctx, scalar, onError) { + const start = scalar.offset; + const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError); + if (!header) + return { value: "", type: null, comment: "", range: [start, start, start] }; + const type = header.mode === ">" ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL; + const lines = scalar.source ? splitLines(scalar.source) : []; + let chompStart = lines.length; + for (let i = lines.length - 1; i >= 0; --i) { + const content = lines[i][1]; + if (content === "" || content === "\r") + chompStart = i; + else + break; + } + if (chompStart === 0) { + const value2 = header.chomp === "+" && lines.length > 0 ? "\n".repeat(Math.max(1, lines.length - 1)) : ""; + let end2 = start + header.length; + if (scalar.source) + end2 += scalar.source.length; + return { value: value2, type, comment: header.comment, range: [start, end2, end2] }; + } + let trimIndent = scalar.indent + header.indent; + let offset = scalar.offset + header.length; + let contentStart = 0; + for (let i = 0; i < chompStart; ++i) { + const [indent, content] = lines[i]; + if (content === "" || content === "\r") { + if (header.indent === 0 && indent.length > trimIndent) + trimIndent = indent.length; + } else { + if (indent.length < trimIndent) { + const message = "Block scalars with more-indented leading empty lines must use an explicit indentation indicator"; + onError(offset + indent.length, "MISSING_CHAR", message); + } + if (header.indent === 0) + trimIndent = indent.length; + contentStart = i; + if (trimIndent === 0 && !ctx.atRoot) { + const message = "Block scalar values in collections must be indented"; + onError(offset, "BAD_INDENT", message); + } + break; + } + offset += indent.length + content.length + 1; + } + for (let i = lines.length - 1; i >= chompStart; --i) { + if (lines[i][0].length > trimIndent) + chompStart = i + 1; + } + let value = ""; + let sep = ""; + let prevMoreIndented = false; + for (let i = 0; i < contentStart; ++i) + value += lines[i][0].slice(trimIndent) + "\n"; + for (let i = contentStart; i < chompStart; ++i) { + let [indent, content] = lines[i]; + offset += indent.length + content.length + 1; + const crlf = content[content.length - 1] === "\r"; + if (crlf) + content = content.slice(0, -1); + if (content && indent.length < trimIndent) { + const src = header.indent ? "explicit indentation indicator" : "first line"; + const message = `Block scalar lines must not be less indented than their ${src}`; + onError(offset - content.length - (crlf ? 2 : 1), "BAD_INDENT", message); + indent = ""; + } + if (type === Scalar.Scalar.BLOCK_LITERAL) { + value += sep + indent.slice(trimIndent) + content; + sep = "\n"; + } else if (indent.length > trimIndent || content[0] === " ") { + if (sep === " ") + sep = "\n"; + else if (!prevMoreIndented && sep === "\n") + sep = "\n\n"; + value += sep + indent.slice(trimIndent) + content; + sep = "\n"; + prevMoreIndented = true; + } else if (content === "") { + if (sep === "\n") + value += "\n"; + else + sep = "\n"; + } else { + value += sep + content; + sep = " "; + prevMoreIndented = false; + } + } + switch (header.chomp) { + case "-": + break; + case "+": + for (let i = chompStart; i < lines.length; ++i) + value += "\n" + lines[i][0].slice(trimIndent); + if (value[value.length - 1] !== "\n") + value += "\n"; + break; + default: + value += "\n"; + } + const end = start + header.length + scalar.source.length; + return { value, type, comment: header.comment, range: [start, end, end] }; + } + function parseBlockScalarHeader({ offset, props }, strict, onError) { + if (props[0].type !== "block-scalar-header") { + onError(props[0], "IMPOSSIBLE", "Block scalar header not found"); + return null; + } + const { source } = props[0]; + const mode = source[0]; + let indent = 0; + let chomp = ""; + let error = -1; + for (let i = 1; i < source.length; ++i) { + const ch = source[i]; + if (!chomp && (ch === "-" || ch === "+")) + chomp = ch; + else { + const n = Number(ch); + if (!indent && n) + indent = n; + else if (error === -1) + error = offset + i; + } + } + if (error !== -1) + onError(error, "UNEXPECTED_TOKEN", `Block scalar header includes extra characters: ${source}`); + let hasSpace = false; + let comment = ""; + let length = source.length; + for (let i = 1; i < props.length; ++i) { + const token = props[i]; + switch (token.type) { + case "space": + hasSpace = true; + // fallthrough + case "newline": + length += token.source.length; + break; + case "comment": + if (strict && !hasSpace) { + const message = "Comments must be separated from other tokens by white space characters"; + onError(token, "MISSING_CHAR", message); + } + length += token.source.length; + comment = token.source.substring(1); + break; + case "error": + onError(token, "UNEXPECTED_TOKEN", token.message); + length += token.source.length; + break; + /* istanbul ignore next should not happen */ + default: { + const message = `Unexpected token in block scalar header: ${token.type}`; + onError(token, "UNEXPECTED_TOKEN", message); + const ts = token.source; + if (ts && typeof ts === "string") + length += ts.length; + } + } + } + return { mode, indent, chomp, comment, length }; + } + function splitLines(source) { + const split = source.split(/\n( *)/); + const first = split[0]; + const m = first.match(/^( *)/); + const line0 = m?.[1] ? [m[1], first.slice(m[1].length)] : ["", first]; + const lines = [line0]; + for (let i = 1; i < split.length; i += 2) + lines.push([split[i], split[i + 1]]); + return lines; + } + exports.resolveBlockScalar = resolveBlockScalar; + } +}); + +// node_modules/yaml/dist/compose/resolve-flow-scalar.js +var require_resolve_flow_scalar = __commonJS({ + "node_modules/yaml/dist/compose/resolve-flow-scalar.js"(exports) { + "use strict"; + var Scalar = require_Scalar(); + var resolveEnd = require_resolve_end(); + function resolveFlowScalar(scalar, strict, onError) { + const { offset, type, source, end } = scalar; + let _type; + let value; + const _onError = (rel, code, msg) => onError(offset + rel, code, msg); + switch (type) { + case "scalar": + _type = Scalar.Scalar.PLAIN; + value = plainValue(source, _onError); + break; + case "single-quoted-scalar": + _type = Scalar.Scalar.QUOTE_SINGLE; + value = singleQuotedValue(source, _onError); + break; + case "double-quoted-scalar": + _type = Scalar.Scalar.QUOTE_DOUBLE; + value = doubleQuotedValue(source, _onError); + break; + /* istanbul ignore next should not happen */ + default: + onError(scalar, "UNEXPECTED_TOKEN", `Expected a flow scalar value, but found: ${type}`); + return { + value: "", + type: null, + comment: "", + range: [offset, offset + source.length, offset + source.length] + }; + } + const valueEnd = offset + source.length; + const re = resolveEnd.resolveEnd(end, valueEnd, strict, onError); + return { + value, + type: _type, + comment: re.comment, + range: [offset, valueEnd, re.offset] + }; + } + function plainValue(source, onError) { + let badChar = ""; + switch (source[0]) { + /* istanbul ignore next should not happen */ + case " ": + badChar = "a tab character"; + break; + case ",": + badChar = "flow indicator character ,"; + break; + case "%": + badChar = "directive indicator character %"; + break; + case "|": + case ">": { + badChar = `block scalar indicator ${source[0]}`; + break; + } + case "@": + case "`": { + badChar = `reserved character ${source[0]}`; + break; + } + } + if (badChar) + onError(0, "BAD_SCALAR_START", `Plain value cannot start with ${badChar}`); + return foldLines(source); + } + function singleQuotedValue(source, onError) { + if (source[source.length - 1] !== "'" || source.length === 1) + onError(source.length, "MISSING_CHAR", "Missing closing 'quote"); + return foldLines(source.slice(1, -1)).replace(/''/g, "'"); + } + function foldLines(source) { + let first, line; + try { + first = new RegExp("(.*?)(? wsStart ? source.slice(wsStart, i + 1) : ch; + } else { + res += ch; + } + } + if (source[source.length - 1] !== '"' || source.length === 1) + onError(source.length, "MISSING_CHAR", 'Missing closing "quote'); + return res; + } + function foldNewline(source, offset) { + let fold = ""; + let ch = source[offset + 1]; + while (ch === " " || ch === " " || ch === "\n" || ch === "\r") { + if (ch === "\r" && source[offset + 2] !== "\n") + break; + if (ch === "\n") + fold += "\n"; + offset += 1; + ch = source[offset + 1]; + } + if (!fold) + fold = " "; + return { fold, offset }; + } + var escapeCodes = { + "0": "\0", + // null character + a: "\x07", + // bell character + b: "\b", + // backspace + e: "\x1B", + // escape character + f: "\f", + // form feed + n: "\n", + // line feed + r: "\r", + // carriage return + t: " ", + // horizontal tab + v: "\v", + // vertical tab + N: "…", + // Unicode next line + _: " ", + // Unicode non-breaking space + L: "\u2028", + // Unicode line separator + P: "\u2029", + // Unicode paragraph separator + " ": " ", + '"': '"', + "/": "/", + "\\": "\\", + " ": " " + }; + function parseCharCode(source, offset, length, onError) { + const cc = source.substr(offset, length); + const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc); + const code = ok ? parseInt(cc, 16) : NaN; + try { + return String.fromCodePoint(code); + } catch { + const raw = source.substr(offset - 2, length + 2); + onError(offset - 2, "BAD_DQ_ESCAPE", `Invalid escape sequence ${raw}`); + return raw; + } + } + exports.resolveFlowScalar = resolveFlowScalar; + } +}); + +// node_modules/yaml/dist/compose/compose-scalar.js +var require_compose_scalar = __commonJS({ + "node_modules/yaml/dist/compose/compose-scalar.js"(exports) { + "use strict"; + var identity = require_identity(); + var Scalar = require_Scalar(); + var resolveBlockScalar = require_resolve_block_scalar(); + var resolveFlowScalar = require_resolve_flow_scalar(); + function composeScalar(ctx, token, tagToken, onError) { + const { value, type, comment, range } = token.type === "block-scalar" ? resolveBlockScalar.resolveBlockScalar(ctx, token, onError) : resolveFlowScalar.resolveFlowScalar(token, ctx.options.strict, onError); + const tagName = tagToken ? ctx.directives.tagName(tagToken.source, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg)) : null; + let tag; + if (ctx.options.stringKeys && ctx.atKey) { + tag = ctx.schema[identity.SCALAR]; + } else if (tagName) + tag = findScalarTagByName(ctx.schema, value, tagName, tagToken, onError); + else if (token.type === "scalar") + tag = findScalarTagByTest(ctx, value, token, onError); + else + tag = ctx.schema[identity.SCALAR]; + let scalar; + try { + const res = tag.resolve(value, (msg) => onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg), ctx.options); + scalar = identity.isScalar(res) ? res : new Scalar.Scalar(res); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg); + scalar = new Scalar.Scalar(value); + } + scalar.range = range; + scalar.source = value; + if (type) + scalar.type = type; + if (tagName) + scalar.tag = tagName; + if (tag.format) + scalar.format = tag.format; + if (comment) + scalar.comment = comment; + return scalar; + } + function findScalarTagByName(schema, value, tagName, tagToken, onError) { + if (tagName === "!") + return schema[identity.SCALAR]; + const matchWithTest = []; + for (const tag of schema.tags) { + if (!tag.collection && tag.tag === tagName) { + if (tag.default && tag.test) + matchWithTest.push(tag); + else + return tag; + } + } + for (const tag of matchWithTest) + if (tag.test?.test(value)) + return tag; + const kt = schema.knownTags[tagName]; + if (kt && !kt.collection) { + schema.tags.push(Object.assign({}, kt, { default: false, test: void 0 })); + return kt; + } + onError(tagToken, "TAG_RESOLVE_FAILED", `Unresolved tag: ${tagName}`, tagName !== "tag:yaml.org,2002:str"); + return schema[identity.SCALAR]; + } + function findScalarTagByTest({ atKey, directives, schema }, value, token, onError) { + const tag = schema.tags.find((tag2) => (tag2.default === true || atKey && tag2.default === "key") && tag2.test?.test(value)) || schema[identity.SCALAR]; + if (schema.compat) { + const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR]; + if (tag.tag !== compat.tag) { + const ts = directives.tagString(tag.tag); + const cs = directives.tagString(compat.tag); + const msg = `Value may be parsed as either ${ts} or ${cs}`; + onError(token, "TAG_RESOLVE_FAILED", msg, true); + } + } + return tag; + } + exports.composeScalar = composeScalar; + } +}); + +// node_modules/yaml/dist/compose/util-empty-scalar-position.js +var require_util_empty_scalar_position = __commonJS({ + "node_modules/yaml/dist/compose/util-empty-scalar-position.js"(exports) { + "use strict"; + function emptyScalarPosition(offset, before, pos) { + if (before) { + pos ?? (pos = before.length); + for (let i = pos - 1; i >= 0; --i) { + let st = before[i]; + switch (st.type) { + case "space": + case "comment": + case "newline": + offset -= st.source.length; + continue; + } + st = before[++i]; + while (st?.type === "space") { + offset += st.source.length; + st = before[++i]; + } + break; + } + } + return offset; + } + exports.emptyScalarPosition = emptyScalarPosition; + } +}); + +// node_modules/yaml/dist/compose/compose-node.js +var require_compose_node = __commonJS({ + "node_modules/yaml/dist/compose/compose-node.js"(exports) { + "use strict"; + var Alias = require_Alias(); + var identity = require_identity(); + var composeCollection = require_compose_collection(); + var composeScalar = require_compose_scalar(); + var resolveEnd = require_resolve_end(); + var utilEmptyScalarPosition = require_util_empty_scalar_position(); + var CN = { composeNode, composeEmptyNode }; + function composeNode(ctx, token, props, onError) { + const atKey = ctx.atKey; + const { spaceBefore, comment, anchor, tag } = props; + let node; + let isSrcToken = true; + switch (token.type) { + case "alias": + node = composeAlias(ctx, token, onError); + if (anchor || tag) + onError(token, "ALIAS_PROPS", "An alias node must not specify any properties"); + break; + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + case "block-scalar": + node = composeScalar.composeScalar(ctx, token, tag, onError); + if (anchor) + node.anchor = anchor.source.substring(1); + break; + case "block-map": + case "block-seq": + case "flow-collection": + try { + node = composeCollection.composeCollection(CN, ctx, token, props, onError); + if (anchor) + node.anchor = anchor.source.substring(1); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + onError(token, "RESOURCE_EXHAUSTION", message); + } + break; + default: { + const message = token.type === "error" ? token.message : `Unsupported token (type: ${token.type})`; + onError(token, "UNEXPECTED_TOKEN", message); + isSrcToken = false; + } + } + node ?? (node = composeEmptyNode(ctx, token.offset, void 0, null, props, onError)); + if (anchor && node.anchor === "") + onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string"); + if (atKey && ctx.options.stringKeys && (!identity.isScalar(node) || typeof node.value !== "string" || node.tag && node.tag !== "tag:yaml.org,2002:str")) { + const msg = "With stringKeys, all keys must be strings"; + onError(tag ?? token, "NON_STRING_KEY", msg); + } + if (spaceBefore) + node.spaceBefore = true; + if (comment) { + if (token.type === "scalar" && token.source === "") + node.comment = comment; + else + node.commentBefore = comment; + } + if (ctx.options.keepSourceTokens && isSrcToken) + node.srcToken = token; + return node; + } + function composeEmptyNode(ctx, offset, before, pos, { spaceBefore, comment, anchor, tag, end }, onError) { + const token = { + type: "scalar", + offset: utilEmptyScalarPosition.emptyScalarPosition(offset, before, pos), + indent: -1, + source: "" + }; + const node = composeScalar.composeScalar(ctx, token, tag, onError); + if (anchor) { + node.anchor = anchor.source.substring(1); + if (node.anchor === "") + onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string"); + } + if (spaceBefore) + node.spaceBefore = true; + if (comment) { + node.comment = comment; + node.range[2] = end; + } + return node; + } + function composeAlias({ options }, { offset, source, end }, onError) { + const alias = new Alias.Alias(source.substring(1)); + if (alias.source === "") + onError(offset, "BAD_ALIAS", "Alias cannot be an empty string"); + if (alias.source.endsWith(":")) + onError(offset + source.length - 1, "BAD_ALIAS", "Alias ending in : is ambiguous", true); + const valueEnd = offset + source.length; + const re = resolveEnd.resolveEnd(end, valueEnd, options.strict, onError); + alias.range = [offset, valueEnd, re.offset]; + if (re.comment) + alias.comment = re.comment; + return alias; + } + exports.composeEmptyNode = composeEmptyNode; + exports.composeNode = composeNode; + } +}); + +// node_modules/yaml/dist/compose/compose-doc.js +var require_compose_doc = __commonJS({ + "node_modules/yaml/dist/compose/compose-doc.js"(exports) { + "use strict"; + var Document = require_Document(); + var composeNode = require_compose_node(); + var resolveEnd = require_resolve_end(); + var resolveProps = require_resolve_props(); + function composeDoc(options, directives, { offset, start, value, end }, onError) { + const opts = Object.assign({ _directives: directives }, options); + const doc = new Document.Document(void 0, opts); + const ctx = { + atKey: false, + atRoot: true, + directives: doc.directives, + options: doc.options, + schema: doc.schema + }; + const props = resolveProps.resolveProps(start, { + indicator: "doc-start", + next: value ?? end?.[0], + offset, + onError, + parentIndent: 0, + startOnNewline: true + }); + if (props.found) { + doc.directives.docStart = true; + if (value && (value.type === "block-map" || value.type === "block-seq") && !props.hasNewline) + onError(props.end, "MISSING_CHAR", "Block collection cannot start on same line with directives-end marker"); + } + doc.contents = value ? composeNode.composeNode(ctx, value, props, onError) : composeNode.composeEmptyNode(ctx, props.end, start, null, props, onError); + const contentEnd = doc.contents.range[2]; + const re = resolveEnd.resolveEnd(end, contentEnd, false, onError); + if (re.comment) + doc.comment = re.comment; + doc.range = [offset, contentEnd, re.offset]; + return doc; + } + exports.composeDoc = composeDoc; + } +}); + +// node_modules/yaml/dist/compose/composer.js +var require_composer = __commonJS({ + "node_modules/yaml/dist/compose/composer.js"(exports) { + "use strict"; + var node_process = __require("process"); + var directives = require_directives(); + var Document = require_Document(); + var errors = require_errors(); + var identity = require_identity(); + var composeDoc = require_compose_doc(); + var resolveEnd = require_resolve_end(); + function getErrorPos(src) { + if (typeof src === "number") + return [src, src + 1]; + if (Array.isArray(src)) + return src.length === 2 ? src : [src[0], src[1]]; + const { offset, source } = src; + return [offset, offset + (typeof source === "string" ? source.length : 1)]; + } + function parsePrelude(prelude) { + let comment = ""; + let atComment = false; + let afterEmptyLine = false; + for (let i = 0; i < prelude.length; ++i) { + const source = prelude[i]; + switch (source[0]) { + case "#": + comment += (comment === "" ? "" : afterEmptyLine ? "\n\n" : "\n") + (source.substring(1) || " "); + atComment = true; + afterEmptyLine = false; + break; + case "%": + if (prelude[i + 1]?.[0] !== "#") + i += 1; + atComment = false; + break; + default: + if (!atComment) + afterEmptyLine = true; + atComment = false; + } + } + return { comment, afterEmptyLine }; + } + var Composer = class { + constructor(options = {}) { + this.doc = null; + this.atDirectives = false; + this.prelude = []; + this.errors = []; + this.warnings = []; + this.onError = (source, code, message, warning) => { + const pos = getErrorPos(source); + if (warning) + this.warnings.push(new errors.YAMLWarning(pos, code, message)); + else + this.errors.push(new errors.YAMLParseError(pos, code, message)); + }; + this.directives = new directives.Directives({ version: options.version || "1.2" }); + this.options = options; + } + decorate(doc, afterDoc) { + const { comment, afterEmptyLine } = parsePrelude(this.prelude); + if (comment) { + const dc = doc.contents; + if (afterDoc) { + doc.comment = doc.comment ? `${doc.comment} +${comment}` : comment; + } else if (afterEmptyLine || doc.directives.docStart || !dc) { + doc.commentBefore = comment; + } else if (identity.isCollection(dc) && !dc.flow && dc.items.length > 0) { + let it = dc.items[0]; + if (identity.isPair(it)) + it = it.key; + const cb = it.commentBefore; + it.commentBefore = cb ? `${comment} +${cb}` : comment; + } else { + const cb = dc.commentBefore; + dc.commentBefore = cb ? `${comment} +${cb}` : comment; + } + } + if (afterDoc) { + Array.prototype.push.apply(doc.errors, this.errors); + Array.prototype.push.apply(doc.warnings, this.warnings); + } else { + doc.errors = this.errors; + doc.warnings = this.warnings; + } + this.prelude = []; + this.errors = []; + this.warnings = []; + } + /** + * Current stream status information. + * + * Mostly useful at the end of input for an empty stream. + */ + streamInfo() { + return { + comment: parsePrelude(this.prelude).comment, + directives: this.directives, + errors: this.errors, + warnings: this.warnings + }; + } + /** + * Compose tokens into documents. + * + * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document. + * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly. + */ + *compose(tokens, forceDoc = false, endOffset = -1) { + for (const token of tokens) + yield* this.next(token); + yield* this.end(forceDoc, endOffset); + } + /** Advance the composer by one CST token. */ + *next(token) { + if (node_process.env.LOG_STREAM) + console.dir(token, { depth: null }); + switch (token.type) { + case "directive": + this.directives.add(token.source, (offset, message, warning) => { + const pos = getErrorPos(token); + pos[0] += offset; + this.onError(pos, "BAD_DIRECTIVE", message, warning); + }); + this.prelude.push(token.source); + this.atDirectives = true; + break; + case "document": { + const doc = composeDoc.composeDoc(this.options, this.directives, token, this.onError); + if (this.atDirectives && !doc.directives.docStart) + this.onError(token, "MISSING_CHAR", "Missing directives-end/doc-start indicator line"); + this.decorate(doc, false); + if (this.doc) + yield this.doc; + this.doc = doc; + this.atDirectives = false; + break; + } + case "byte-order-mark": + case "space": + break; + case "comment": + case "newline": + this.prelude.push(token.source); + break; + case "error": { + const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message; + const error = new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg); + if (this.atDirectives || !this.doc) + this.errors.push(error); + else + this.doc.errors.push(error); + break; + } + case "doc-end": { + if (!this.doc) { + const msg = "Unexpected doc-end without preceding document"; + this.errors.push(new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg)); + break; + } + this.doc.directives.docEnd = true; + const end = resolveEnd.resolveEnd(token.end, token.offset + token.source.length, this.doc.options.strict, this.onError); + this.decorate(this.doc, true); + if (end.comment) { + const dc = this.doc.comment; + this.doc.comment = dc ? `${dc} +${end.comment}` : end.comment; + } + this.doc.range[2] = end.offset; + break; + } + default: + this.errors.push(new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`)); + } + } + /** + * Call at end of input to yield any remaining document. + * + * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document. + * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly. + */ + *end(forceDoc = false, endOffset = -1) { + if (this.doc) { + this.decorate(this.doc, true); + yield this.doc; + this.doc = null; + } else if (forceDoc) { + const opts = Object.assign({ _directives: this.directives }, this.options); + const doc = new Document.Document(void 0, opts); + if (this.atDirectives) + this.onError(endOffset, "MISSING_CHAR", "Missing directives-end indicator line"); + doc.range = [0, endOffset, endOffset]; + this.decorate(doc, false); + yield doc; + } + } + }; + exports.Composer = Composer; + } +}); + +// node_modules/yaml/dist/parse/cst-scalar.js +var require_cst_scalar = __commonJS({ + "node_modules/yaml/dist/parse/cst-scalar.js"(exports) { + "use strict"; + var resolveBlockScalar = require_resolve_block_scalar(); + var resolveFlowScalar = require_resolve_flow_scalar(); + var errors = require_errors(); + var stringifyString = require_stringifyString(); + function resolveAsScalar(token, strict = true, onError) { + if (token) { + const _onError = (pos, code, message) => { + const offset = typeof pos === "number" ? pos : Array.isArray(pos) ? pos[0] : pos.offset; + if (onError) + onError(offset, code, message); + else + throw new errors.YAMLParseError([offset, offset + 1], code, message); + }; + switch (token.type) { + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return resolveFlowScalar.resolveFlowScalar(token, strict, _onError); + case "block-scalar": + return resolveBlockScalar.resolveBlockScalar({ options: { strict } }, token, _onError); + } + } + return null; + } + function createScalarToken(value, context) { + const { implicitKey = false, indent, inFlow = false, offset = -1, type = "PLAIN" } = context; + const source = stringifyString.stringifyString({ type, value }, { + implicitKey, + indent: indent > 0 ? " ".repeat(indent) : "", + inFlow, + options: { blockQuote: true, lineWidth: -1 } + }); + const end = context.end ?? [ + { type: "newline", offset: -1, indent, source: "\n" } + ]; + switch (source[0]) { + case "|": + case ">": { + const he = source.indexOf("\n"); + const head = source.substring(0, he); + const body = source.substring(he + 1) + "\n"; + const props = [ + { type: "block-scalar-header", offset, indent, source: head } + ]; + if (!addEndtoBlockProps(props, end)) + props.push({ type: "newline", offset: -1, indent, source: "\n" }); + return { type: "block-scalar", offset, indent, props, source: body }; + } + case '"': + return { type: "double-quoted-scalar", offset, indent, source, end }; + case "'": + return { type: "single-quoted-scalar", offset, indent, source, end }; + default: + return { type: "scalar", offset, indent, source, end }; + } + } + function setScalarValue(token, value, context = {}) { + let { afterKey = false, implicitKey = false, inFlow = false, type } = context; + let indent = "indent" in token ? token.indent : null; + if (afterKey && typeof indent === "number") + indent += 2; + if (!type) + switch (token.type) { + case "single-quoted-scalar": + type = "QUOTE_SINGLE"; + break; + case "double-quoted-scalar": + type = "QUOTE_DOUBLE"; + break; + case "block-scalar": { + const header = token.props[0]; + if (header.type !== "block-scalar-header") + throw new Error("Invalid block scalar header"); + type = header.source[0] === ">" ? "BLOCK_FOLDED" : "BLOCK_LITERAL"; + break; + } + default: + type = "PLAIN"; + } + const source = stringifyString.stringifyString({ type, value }, { + implicitKey: implicitKey || indent === null, + indent: indent !== null && indent > 0 ? " ".repeat(indent) : "", + inFlow, + options: { blockQuote: true, lineWidth: -1 } + }); + switch (source[0]) { + case "|": + case ">": + setBlockScalarValue(token, source); + break; + case '"': + setFlowScalarValue(token, source, "double-quoted-scalar"); + break; + case "'": + setFlowScalarValue(token, source, "single-quoted-scalar"); + break; + default: + setFlowScalarValue(token, source, "scalar"); + } + } + function setBlockScalarValue(token, source) { + const he = source.indexOf("\n"); + const head = source.substring(0, he); + const body = source.substring(he + 1) + "\n"; + if (token.type === "block-scalar") { + const header = token.props[0]; + if (header.type !== "block-scalar-header") + throw new Error("Invalid block scalar header"); + header.source = head; + token.source = body; + } else { + const { offset } = token; + const indent = "indent" in token ? token.indent : -1; + const props = [ + { type: "block-scalar-header", offset, indent, source: head } + ]; + if (!addEndtoBlockProps(props, "end" in token ? token.end : void 0)) + props.push({ type: "newline", offset: -1, indent, source: "\n" }); + for (const key of Object.keys(token)) + if (key !== "type" && key !== "offset") + delete token[key]; + Object.assign(token, { type: "block-scalar", indent, props, source: body }); + } + } + function addEndtoBlockProps(props, end) { + if (end) + for (const st of end) + switch (st.type) { + case "space": + case "comment": + props.push(st); + break; + case "newline": + props.push(st); + return true; + } + return false; + } + function setFlowScalarValue(token, source, type) { + switch (token.type) { + case "scalar": + case "double-quoted-scalar": + case "single-quoted-scalar": + token.type = type; + token.source = source; + break; + case "block-scalar": { + const end = token.props.slice(1); + let oa = source.length; + if (token.props[0].type === "block-scalar-header") + oa -= token.props[0].source.length; + for (const tok of end) + tok.offset += oa; + delete token.props; + Object.assign(token, { type, source, end }); + break; + } + case "block-map": + case "block-seq": { + const offset = token.offset + source.length; + const nl = { type: "newline", offset, indent: token.indent, source: "\n" }; + delete token.items; + Object.assign(token, { type, source, end: [nl] }); + break; + } + default: { + const indent = "indent" in token ? token.indent : -1; + const end = "end" in token && Array.isArray(token.end) ? token.end.filter((st) => st.type === "space" || st.type === "comment" || st.type === "newline") : []; + for (const key of Object.keys(token)) + if (key !== "type" && key !== "offset") + delete token[key]; + Object.assign(token, { type, indent, source, end }); + } + } + } + exports.createScalarToken = createScalarToken; + exports.resolveAsScalar = resolveAsScalar; + exports.setScalarValue = setScalarValue; + } +}); + +// node_modules/yaml/dist/parse/cst-stringify.js +var require_cst_stringify = __commonJS({ + "node_modules/yaml/dist/parse/cst-stringify.js"(exports) { + "use strict"; + var stringify2 = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst); + function stringifyToken(token) { + switch (token.type) { + case "block-scalar": { + let res = ""; + for (const tok of token.props) + res += stringifyToken(tok); + return res + token.source; + } + case "block-map": + case "block-seq": { + let res = ""; + for (const item of token.items) + res += stringifyItem(item); + return res; + } + case "flow-collection": { + let res = token.start.source; + for (const item of token.items) + res += stringifyItem(item); + for (const st of token.end) + res += st.source; + return res; + } + case "document": { + let res = stringifyItem(token); + if (token.end) + for (const st of token.end) + res += st.source; + return res; + } + default: { + let res = token.source; + if ("end" in token && token.end) + for (const st of token.end) + res += st.source; + return res; + } + } + } + function stringifyItem({ start, key, sep, value }) { + let res = ""; + for (const st of start) + res += st.source; + if (key) + res += stringifyToken(key); + if (sep) + for (const st of sep) + res += st.source; + if (value) + res += stringifyToken(value); + return res; + } + exports.stringify = stringify2; + } +}); + +// node_modules/yaml/dist/parse/cst-visit.js +var require_cst_visit = __commonJS({ + "node_modules/yaml/dist/parse/cst-visit.js"(exports) { + "use strict"; + var BREAK = Symbol("break visit"); + var SKIP = Symbol("skip children"); + var REMOVE = Symbol("remove item"); + function visit(cst, visitor) { + if ("type" in cst && cst.type === "document") + cst = { start: cst.start, value: cst.value }; + _visit(Object.freeze([]), cst, visitor); + } + visit.BREAK = BREAK; + visit.SKIP = SKIP; + visit.REMOVE = REMOVE; + visit.itemAtPath = (cst, path) => { + let item = cst; + for (const [field, index] of path) { + const tok = item?.[field]; + if (tok && "items" in tok) { + item = tok.items[index]; + } else + return void 0; + } + return item; + }; + visit.parentCollection = (cst, path) => { + const parent = visit.itemAtPath(cst, path.slice(0, -1)); + const field = path[path.length - 1][0]; + const coll = parent?.[field]; + if (coll && "items" in coll) + return coll; + throw new Error("Parent collection not found"); + }; + function _visit(path, item, visitor) { + let ctrl = visitor(item, path); + if (typeof ctrl === "symbol") + return ctrl; + for (const field of ["key", "value"]) { + const token = item[field]; + if (token && "items" in token) { + for (let i = 0; i < token.items.length; ++i) { + const ci = _visit(Object.freeze(path.concat([[field, i]])), token.items[i], visitor); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + token.items.splice(i, 1); + i -= 1; + } + } + if (typeof ctrl === "function" && field === "key") + ctrl = ctrl(item, path); + } + } + return typeof ctrl === "function" ? ctrl(item, path) : ctrl; + } + exports.visit = visit; + } +}); + +// node_modules/yaml/dist/parse/cst.js +var require_cst = __commonJS({ + "node_modules/yaml/dist/parse/cst.js"(exports) { + "use strict"; + var cstScalar = require_cst_scalar(); + var cstStringify = require_cst_stringify(); + var cstVisit = require_cst_visit(); + var BOM = "\uFEFF"; + var DOCUMENT = ""; + var FLOW_END = ""; + var SCALAR = ""; + var isCollection = (token) => !!token && "items" in token; + var isScalar = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar"); + function prettyToken(token) { + switch (token) { + case BOM: + return ""; + case DOCUMENT: + return ""; + case FLOW_END: + return ""; + case SCALAR: + return ""; + default: + return JSON.stringify(token); + } + } + function tokenType(source) { + switch (source) { + case BOM: + return "byte-order-mark"; + case DOCUMENT: + return "doc-mode"; + case FLOW_END: + return "flow-error-end"; + case SCALAR: + return "scalar"; + case "---": + return "doc-start"; + case "...": + return "doc-end"; + case "": + case "\n": + case "\r\n": + return "newline"; + case "-": + return "seq-item-ind"; + case "?": + return "explicit-key-ind"; + case ":": + return "map-value-ind"; + case "{": + return "flow-map-start"; + case "}": + return "flow-map-end"; + case "[": + return "flow-seq-start"; + case "]": + return "flow-seq-end"; + case ",": + return "comma"; + } + switch (source[0]) { + case " ": + case " ": + return "space"; + case "#": + return "comment"; + case "%": + return "directive-line"; + case "*": + return "alias"; + case "&": + return "anchor"; + case "!": + return "tag"; + case "'": + return "single-quoted-scalar"; + case '"': + return "double-quoted-scalar"; + case "|": + case ">": + return "block-scalar-header"; + } + return null; + } + exports.createScalarToken = cstScalar.createScalarToken; + exports.resolveAsScalar = cstScalar.resolveAsScalar; + exports.setScalarValue = cstScalar.setScalarValue; + exports.stringify = cstStringify.stringify; + exports.visit = cstVisit.visit; + exports.BOM = BOM; + exports.DOCUMENT = DOCUMENT; + exports.FLOW_END = FLOW_END; + exports.SCALAR = SCALAR; + exports.isCollection = isCollection; + exports.isScalar = isScalar; + exports.prettyToken = prettyToken; + exports.tokenType = tokenType; + } +}); + +// node_modules/yaml/dist/parse/lexer.js +var require_lexer = __commonJS({ + "node_modules/yaml/dist/parse/lexer.js"(exports) { + "use strict"; + var cst = require_cst(); + function isEmpty(ch) { + switch (ch) { + case void 0: + case " ": + case "\n": + case "\r": + case " ": + return true; + default: + return false; + } + } + var hexDigits = new Set("0123456789ABCDEFabcdef"); + var tagChars = new Set("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()"); + var flowIndicatorChars = new Set(",[]{}"); + var invalidAnchorChars = new Set(" ,[]{}\n\r "); + var isNotAnchorChar = (ch) => !ch || invalidAnchorChars.has(ch); + var Lexer = class { + constructor() { + this.atEnd = false; + this.blockScalarIndent = -1; + this.blockScalarKeep = false; + this.buffer = ""; + this.flowKey = false; + this.flowLevel = 0; + this.indentNext = 0; + this.indentValue = 0; + this.lineEndPos = null; + this.next = null; + this.pos = 0; + } + /** + * Generate YAML tokens from the `source` string. If `incomplete`, + * a part of the last line may be left as a buffer for the next call. + * + * @returns A generator of lexical tokens + */ + *lex(source, incomplete = false) { + if (source) { + if (typeof source !== "string") + throw TypeError("source is not a string"); + this.buffer = this.buffer ? this.buffer + source : source; + this.lineEndPos = null; + } + this.atEnd = !incomplete; + let next = this.next ?? "stream"; + while (next && (incomplete || this.hasChars(1))) + next = yield* this.parseNext(next); + } + atLineEnd() { + let i = this.pos; + let ch = this.buffer[i]; + while (ch === " " || ch === " ") + ch = this.buffer[++i]; + if (!ch || ch === "#" || ch === "\n") + return true; + if (ch === "\r") + return this.buffer[i + 1] === "\n"; + return false; + } + charAt(n) { + return this.buffer[this.pos + n]; + } + continueScalar(offset) { + let ch = this.buffer[offset]; + if (this.indentNext > 0) { + let indent = 0; + while (ch === " ") + ch = this.buffer[++indent + offset]; + if (ch === "\r") { + const next = this.buffer[indent + offset + 1]; + if (next === "\n" || !next && !this.atEnd) + return offset + indent + 1; + } + return ch === "\n" || indent >= this.indentNext || !ch && !this.atEnd ? offset + indent : -1; + } + if (ch === "-" || ch === ".") { + const dt = this.buffer.substr(offset, 3); + if ((dt === "---" || dt === "...") && isEmpty(this.buffer[offset + 3])) + return -1; + } + return offset; + } + getLine() { + let end = this.lineEndPos; + if (typeof end !== "number" || end !== -1 && end < this.pos) { + end = this.buffer.indexOf("\n", this.pos); + this.lineEndPos = end; + } + if (end === -1) + return this.atEnd ? this.buffer.substring(this.pos) : null; + if (this.buffer[end - 1] === "\r") + end -= 1; + return this.buffer.substring(this.pos, end); + } + hasChars(n) { + return this.pos + n <= this.buffer.length; + } + setNext(state) { + this.buffer = this.buffer.substring(this.pos); + this.pos = 0; + this.lineEndPos = null; + this.next = state; + return null; + } + peek(n) { + return this.buffer.substr(this.pos, n); + } + *parseNext(next) { + switch (next) { + case "stream": + return yield* this.parseStream(); + case "line-start": + return yield* this.parseLineStart(); + case "block-start": + return yield* this.parseBlockStart(); + case "doc": + return yield* this.parseDocument(); + case "flow": + return yield* this.parseFlowCollection(); + case "quoted-scalar": + return yield* this.parseQuotedScalar(); + case "block-scalar": + return yield* this.parseBlockScalar(); + case "plain-scalar": + return yield* this.parsePlainScalar(); + } + } + *parseStream() { + let line = this.getLine(); + if (line === null) + return this.setNext("stream"); + if (line[0] === cst.BOM) { + yield* this.pushCount(1); + line = line.substring(1); + } + if (line[0] === "%") { + let dirEnd = line.length; + let cs = line.indexOf("#"); + while (cs !== -1) { + const ch = line[cs - 1]; + if (ch === " " || ch === " ") { + dirEnd = cs - 1; + break; + } else { + cs = line.indexOf("#", cs + 1); + } + } + while (true) { + const ch = line[dirEnd - 1]; + if (ch === " " || ch === " ") + dirEnd -= 1; + else + break; + } + const n = (yield* this.pushCount(dirEnd)) + (yield* this.pushSpaces(true)); + yield* this.pushCount(line.length - n); + this.pushNewline(); + return "stream"; + } + if (this.atLineEnd()) { + const sp = yield* this.pushSpaces(true); + yield* this.pushCount(line.length - sp); + yield* this.pushNewline(); + return "stream"; + } + yield cst.DOCUMENT; + return yield* this.parseLineStart(); + } + *parseLineStart() { + const ch = this.charAt(0); + if (!ch && !this.atEnd) + return this.setNext("line-start"); + if (ch === "-" || ch === ".") { + if (!this.atEnd && !this.hasChars(4)) + return this.setNext("line-start"); + const s = this.peek(3); + if ((s === "---" || s === "...") && isEmpty(this.charAt(3))) { + yield* this.pushCount(3); + this.indentValue = 0; + this.indentNext = 0; + return s === "---" ? "doc" : "stream"; + } + } + this.indentValue = yield* this.pushSpaces(false); + if (this.indentNext > this.indentValue && !isEmpty(this.charAt(1))) + this.indentNext = this.indentValue; + return yield* this.parseBlockStart(); + } + *parseBlockStart() { + const [ch0, ch1] = this.peek(2); + if (!ch1 && !this.atEnd) + return this.setNext("block-start"); + if ((ch0 === "-" || ch0 === "?" || ch0 === ":") && isEmpty(ch1)) { + const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true)); + this.indentNext = this.indentValue + 1; + this.indentValue += n; + return yield* this.parseBlockStart(); + } + return "doc"; + } + *parseDocument() { + yield* this.pushSpaces(true); + const line = this.getLine(); + if (line === null) + return this.setNext("doc"); + let n = yield* this.pushIndicators(); + switch (line[n]) { + case "#": + yield* this.pushCount(line.length - n); + // fallthrough + case void 0: + yield* this.pushNewline(); + return yield* this.parseLineStart(); + case "{": + case "[": + yield* this.pushCount(1); + this.flowKey = false; + this.flowLevel = 1; + return "flow"; + case "}": + case "]": + yield* this.pushCount(1); + return "doc"; + case "*": + yield* this.pushUntil(isNotAnchorChar); + return "doc"; + case '"': + case "'": + return yield* this.parseQuotedScalar(); + case "|": + case ">": + n += yield* this.parseBlockScalarHeader(); + n += yield* this.pushSpaces(true); + yield* this.pushCount(line.length - n); + yield* this.pushNewline(); + return yield* this.parseBlockScalar(); + default: + return yield* this.parsePlainScalar(); + } + } + *parseFlowCollection() { + let nl, sp; + let indent = -1; + do { + nl = yield* this.pushNewline(); + if (nl > 0) { + sp = yield* this.pushSpaces(false); + this.indentValue = indent = sp; + } else { + sp = 0; + } + sp += yield* this.pushSpaces(true); + } while (nl + sp > 0); + const line = this.getLine(); + if (line === null) + return this.setNext("flow"); + if (indent !== -1 && indent < this.indentNext && line[0] !== "#" || indent === 0 && (line.startsWith("---") || line.startsWith("...")) && isEmpty(line[3])) { + const atFlowEndMarker = indent === this.indentNext - 1 && this.flowLevel === 1 && (line[0] === "]" || line[0] === "}"); + if (!atFlowEndMarker) { + this.flowLevel = 0; + yield cst.FLOW_END; + return yield* this.parseLineStart(); + } + } + let n = 0; + while (line[n] === ",") { + n += yield* this.pushCount(1); + n += yield* this.pushSpaces(true); + this.flowKey = false; + } + n += yield* this.pushIndicators(); + switch (line[n]) { + case void 0: + return "flow"; + case "#": + yield* this.pushCount(line.length - n); + return "flow"; + case "{": + case "[": + yield* this.pushCount(1); + this.flowKey = false; + this.flowLevel += 1; + return "flow"; + case "}": + case "]": + yield* this.pushCount(1); + this.flowKey = true; + this.flowLevel -= 1; + return this.flowLevel ? "flow" : "doc"; + case "*": + yield* this.pushUntil(isNotAnchorChar); + return "flow"; + case '"': + case "'": + this.flowKey = true; + return yield* this.parseQuotedScalar(); + case ":": { + const next = this.charAt(1); + if (this.flowKey || isEmpty(next) || next === ",") { + this.flowKey = false; + yield* this.pushCount(1); + yield* this.pushSpaces(true); + return "flow"; + } + } + // fallthrough + default: + this.flowKey = false; + return yield* this.parsePlainScalar(); + } + } + *parseQuotedScalar() { + const quote = this.charAt(0); + let end = this.buffer.indexOf(quote, this.pos + 1); + if (quote === "'") { + while (end !== -1 && this.buffer[end + 1] === "'") + end = this.buffer.indexOf("'", end + 2); + } else { + while (end !== -1) { + let n = 0; + while (this.buffer[end - 1 - n] === "\\") + n += 1; + if (n % 2 === 0) + break; + end = this.buffer.indexOf('"', end + 1); + } + } + const qb = this.buffer.substring(0, end); + let nl = qb.indexOf("\n", this.pos); + if (nl !== -1) { + while (nl !== -1) { + const cs = this.continueScalar(nl + 1); + if (cs === -1) + break; + nl = qb.indexOf("\n", cs); + } + if (nl !== -1) { + end = nl - (qb[nl - 1] === "\r" ? 2 : 1); + } + } + if (end === -1) { + if (!this.atEnd) + return this.setNext("quoted-scalar"); + end = this.buffer.length; + } + yield* this.pushToIndex(end + 1, false); + return this.flowLevel ? "flow" : "doc"; + } + *parseBlockScalarHeader() { + this.blockScalarIndent = -1; + this.blockScalarKeep = false; + let i = this.pos; + while (true) { + const ch = this.buffer[++i]; + if (ch === "+") + this.blockScalarKeep = true; + else if (ch > "0" && ch <= "9") + this.blockScalarIndent = Number(ch) - 1; + else if (ch !== "-") + break; + } + return yield* this.pushUntil((ch) => isEmpty(ch) || ch === "#"); + } + *parseBlockScalar() { + let nl = this.pos - 1; + let indent = 0; + let ch; + loop: for (let i2 = this.pos; ch = this.buffer[i2]; ++i2) { + switch (ch) { + case " ": + indent += 1; + break; + case "\n": + nl = i2; + indent = 0; + break; + case "\r": { + const next = this.buffer[i2 + 1]; + if (!next && !this.atEnd) + return this.setNext("block-scalar"); + if (next === "\n") + break; + } + // fallthrough + default: + break loop; + } + } + if (!ch && !this.atEnd) + return this.setNext("block-scalar"); + if (indent >= this.indentNext) { + if (this.blockScalarIndent === -1) + this.indentNext = indent; + else { + this.indentNext = this.blockScalarIndent + (this.indentNext === 0 ? 1 : this.indentNext); + } + do { + const cs = this.continueScalar(nl + 1); + if (cs === -1) + break; + nl = this.buffer.indexOf("\n", cs); + } while (nl !== -1); + if (nl === -1) { + if (!this.atEnd) + return this.setNext("block-scalar"); + nl = this.buffer.length; + } + } + let i = nl + 1; + ch = this.buffer[i]; + while (ch === " ") + ch = this.buffer[++i]; + if (ch === " ") { + while (ch === " " || ch === " " || ch === "\r" || ch === "\n") + ch = this.buffer[++i]; + nl = i - 1; + } else if (!this.blockScalarKeep) { + do { + let i2 = nl - 1; + let ch2 = this.buffer[i2]; + if (ch2 === "\r") + ch2 = this.buffer[--i2]; + const lastChar = i2; + while (ch2 === " ") + ch2 = this.buffer[--i2]; + if (ch2 === "\n" && i2 >= this.pos && i2 + 1 + indent > lastChar) + nl = i2; + else + break; + } while (true); + } + yield cst.SCALAR; + yield* this.pushToIndex(nl + 1, true); + return yield* this.parseLineStart(); + } + *parsePlainScalar() { + const inFlow = this.flowLevel > 0; + let end = this.pos - 1; + let i = this.pos - 1; + let ch; + while (ch = this.buffer[++i]) { + if (ch === ":") { + const next = this.buffer[i + 1]; + if (isEmpty(next) || inFlow && flowIndicatorChars.has(next)) + break; + end = i; + } else if (isEmpty(ch)) { + let next = this.buffer[i + 1]; + if (ch === "\r") { + if (next === "\n") { + i += 1; + ch = "\n"; + next = this.buffer[i + 1]; + } else + end = i; + } + if (next === "#" || inFlow && flowIndicatorChars.has(next)) + break; + if (ch === "\n") { + const cs = this.continueScalar(i + 1); + if (cs === -1) + break; + i = Math.max(i, cs - 2); + } + } else { + if (inFlow && flowIndicatorChars.has(ch)) + break; + end = i; + } + } + if (!ch && !this.atEnd) + return this.setNext("plain-scalar"); + yield cst.SCALAR; + yield* this.pushToIndex(end + 1, true); + return inFlow ? "flow" : "doc"; + } + *pushCount(n) { + if (n > 0) { + yield this.buffer.substr(this.pos, n); + this.pos += n; + return n; + } + return 0; + } + *pushToIndex(i, allowEmpty) { + const s = this.buffer.slice(this.pos, i); + if (s) { + yield s; + this.pos += s.length; + return s.length; + } else if (allowEmpty) + yield ""; + return 0; + } + *pushIndicators() { + switch (this.charAt(0)) { + case "!": + return (yield* this.pushTag()) + (yield* this.pushSpaces(true)) + (yield* this.pushIndicators()); + case "&": + return (yield* this.pushUntil(isNotAnchorChar)) + (yield* this.pushSpaces(true)) + (yield* this.pushIndicators()); + case "-": + // this is an error + case "?": + // this is an error outside flow collections + case ":": { + const inFlow = this.flowLevel > 0; + const ch1 = this.charAt(1); + if (isEmpty(ch1) || inFlow && flowIndicatorChars.has(ch1)) { + if (!inFlow) + this.indentNext = this.indentValue + 1; + else if (this.flowKey) + this.flowKey = false; + return (yield* this.pushCount(1)) + (yield* this.pushSpaces(true)) + (yield* this.pushIndicators()); + } + } + } + return 0; + } + *pushTag() { + if (this.charAt(1) === "<") { + let i = this.pos + 2; + let ch = this.buffer[i]; + while (!isEmpty(ch) && ch !== ">") + ch = this.buffer[++i]; + return yield* this.pushToIndex(ch === ">" ? i + 1 : i, false); + } else { + let i = this.pos + 1; + let ch = this.buffer[i]; + while (ch) { + if (tagChars.has(ch)) + ch = this.buffer[++i]; + else if (ch === "%" && hexDigits.has(this.buffer[i + 1]) && hexDigits.has(this.buffer[i + 2])) { + ch = this.buffer[i += 3]; + } else + break; + } + return yield* this.pushToIndex(i, false); + } + } + *pushNewline() { + const ch = this.buffer[this.pos]; + if (ch === "\n") + return yield* this.pushCount(1); + else if (ch === "\r" && this.charAt(1) === "\n") + return yield* this.pushCount(2); + else + return 0; + } + *pushSpaces(allowTabs) { + let i = this.pos - 1; + let ch; + do { + ch = this.buffer[++i]; + } while (ch === " " || allowTabs && ch === " "); + const n = i - this.pos; + if (n > 0) { + yield this.buffer.substr(this.pos, n); + this.pos = i; + } + return n; + } + *pushUntil(test) { + let i = this.pos; + let ch = this.buffer[i]; + while (!test(ch)) + ch = this.buffer[++i]; + return yield* this.pushToIndex(i, false); + } + }; + exports.Lexer = Lexer; + } +}); + +// node_modules/yaml/dist/parse/line-counter.js +var require_line_counter = __commonJS({ + "node_modules/yaml/dist/parse/line-counter.js"(exports) { + "use strict"; + var LineCounter = class { + constructor() { + this.lineStarts = []; + this.addNewLine = (offset) => this.lineStarts.push(offset); + this.linePos = (offset) => { + let low = 0; + let high = this.lineStarts.length; + while (low < high) { + const mid = low + high >> 1; + if (this.lineStarts[mid] < offset) + low = mid + 1; + else + high = mid; + } + if (this.lineStarts[low] === offset) + return { line: low + 1, col: 1 }; + if (low === 0) + return { line: 0, col: offset }; + const start = this.lineStarts[low - 1]; + return { line: low, col: offset - start + 1 }; + }; + } + }; + exports.LineCounter = LineCounter; + } +}); + +// node_modules/yaml/dist/parse/parser.js +var require_parser = __commonJS({ + "node_modules/yaml/dist/parse/parser.js"(exports) { + "use strict"; + var node_process = __require("process"); + var cst = require_cst(); + var lexer = require_lexer(); + function includesToken(list, type) { + for (let i = 0; i < list.length; ++i) + if (list[i].type === type) + return true; + return false; + } + function findNonEmptyIndex(list) { + for (let i = 0; i < list.length; ++i) { + switch (list[i].type) { + case "space": + case "comment": + case "newline": + break; + default: + return i; + } + } + return -1; + } + function isFlowToken(token) { + switch (token?.type) { + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + case "flow-collection": + return true; + default: + return false; + } + } + function getPrevProps(parent) { + switch (parent.type) { + case "document": + return parent.start; + case "block-map": { + const it = parent.items[parent.items.length - 1]; + return it.sep ?? it.start; + } + case "block-seq": + return parent.items[parent.items.length - 1].start; + /* istanbul ignore next should not happen */ + default: + return []; + } + } + function getFirstKeyStartProps(prev) { + if (prev.length === 0) + return []; + let i = prev.length; + loop: while (--i >= 0) { + switch (prev[i].type) { + case "doc-start": + case "explicit-key-ind": + case "map-value-ind": + case "seq-item-ind": + case "newline": + break loop; + } + } + while (prev[++i]?.type === "space") { + } + return prev.splice(i, prev.length); + } + function fixFlowSeqItems(fc) { + if (fc.start.type === "flow-seq-start") { + for (const it of fc.items) { + if (it.sep && !it.value && !includesToken(it.start, "explicit-key-ind") && !includesToken(it.sep, "map-value-ind")) { + if (it.key) + it.value = it.key; + delete it.key; + if (isFlowToken(it.value)) { + if (it.value.end) + Array.prototype.push.apply(it.value.end, it.sep); + else + it.value.end = it.sep; + } else + Array.prototype.push.apply(it.start, it.sep); + delete it.sep; + } + } + } + } + var Parser = class { + /** + * @param onNewLine - If defined, called separately with the start position of + * each new line (in `parse()`, including the start of input). + */ + constructor(onNewLine) { + this.atNewLine = true; + this.atScalar = false; + this.indent = 0; + this.offset = 0; + this.onKeyLine = false; + this.stack = []; + this.source = ""; + this.type = ""; + this.lexer = new lexer.Lexer(); + this.onNewLine = onNewLine; + } + /** + * Parse `source` as a YAML stream. + * If `incomplete`, a part of the last line may be left as a buffer for the next call. + * + * Errors are not thrown, but yielded as `{ type: 'error', message }` tokens. + * + * @returns A generator of tokens representing each directive, document, and other structure. + */ + *parse(source, incomplete = false) { + if (this.onNewLine && this.offset === 0) + this.onNewLine(0); + for (const lexeme of this.lexer.lex(source, incomplete)) + yield* this.next(lexeme); + if (!incomplete) + yield* this.end(); + } + /** + * Advance the parser by the `source` of one lexical token. + */ + *next(source) { + this.source = source; + if (node_process.env.LOG_TOKENS) + console.log("|", cst.prettyToken(source)); + if (this.atScalar) { + this.atScalar = false; + yield* this.step(); + this.offset += source.length; + return; + } + const type = cst.tokenType(source); + if (!type) { + const message = `Not a YAML token: ${source}`; + yield* this.pop({ type: "error", offset: this.offset, message, source }); + this.offset += source.length; + } else if (type === "scalar") { + this.atNewLine = false; + this.atScalar = true; + this.type = "scalar"; + } else { + this.type = type; + yield* this.step(); + switch (type) { + case "newline": + this.atNewLine = true; + this.indent = 0; + if (this.onNewLine) + this.onNewLine(this.offset + source.length); + break; + case "space": + if (this.atNewLine && source[0] === " ") + this.indent += source.length; + break; + case "explicit-key-ind": + case "map-value-ind": + case "seq-item-ind": + if (this.atNewLine) + this.indent += source.length; + break; + case "doc-mode": + case "flow-error-end": + return; + default: + this.atNewLine = false; + } + this.offset += source.length; + } + } + /** Call at end of input to push out any remaining constructions */ + *end() { + while (this.stack.length > 0) + yield* this.pop(); + } + get sourceToken() { + const st = { + type: this.type, + offset: this.offset, + indent: this.indent, + source: this.source + }; + return st; + } + *step() { + const top = this.peek(1); + if (this.type === "doc-end" && top?.type !== "doc-end") { + while (this.stack.length > 0) + yield* this.pop(); + this.stack.push({ + type: "doc-end", + offset: this.offset, + source: this.source + }); + return; + } + if (!top) + return yield* this.stream(); + switch (top.type) { + case "document": + return yield* this.document(top); + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return yield* this.scalar(top); + case "block-scalar": + return yield* this.blockScalar(top); + case "block-map": + return yield* this.blockMap(top); + case "block-seq": + return yield* this.blockSequence(top); + case "flow-collection": + return yield* this.flowCollection(top); + case "doc-end": + return yield* this.documentEnd(top); + } + yield* this.pop(); + } + peek(n) { + return this.stack[this.stack.length - n]; + } + *pop(error) { + const token = error ?? this.stack.pop(); + if (!token) { + const message = "Tried to pop an empty stack"; + yield { type: "error", offset: this.offset, source: "", message }; + } else if (this.stack.length === 0) { + yield token; + } else { + const top = this.peek(1); + if (token.type === "block-scalar") { + token.indent = "indent" in top ? top.indent : 0; + } else if (token.type === "flow-collection" && top.type === "document") { + token.indent = 0; + } + if (token.type === "flow-collection") + fixFlowSeqItems(token); + switch (top.type) { + case "document": + top.value = token; + break; + case "block-scalar": + top.props.push(token); + break; + case "block-map": { + const it = top.items[top.items.length - 1]; + if (it.value) { + top.items.push({ start: [], key: token, sep: [] }); + this.onKeyLine = true; + return; + } else if (it.sep) { + it.value = token; + } else { + Object.assign(it, { key: token, sep: [] }); + this.onKeyLine = !it.explicitKey; + return; + } + break; + } + case "block-seq": { + const it = top.items[top.items.length - 1]; + if (it.value) + top.items.push({ start: [], value: token }); + else + it.value = token; + break; + } + case "flow-collection": { + const it = top.items[top.items.length - 1]; + if (!it || it.value) + top.items.push({ start: [], key: token, sep: [] }); + else if (it.sep) + it.value = token; + else + Object.assign(it, { key: token, sep: [] }); + return; + } + /* istanbul ignore next should not happen */ + default: + yield* this.pop(); + yield* this.pop(token); + } + if ((top.type === "document" || top.type === "block-map" || top.type === "block-seq") && (token.type === "block-map" || token.type === "block-seq")) { + const last = token.items[token.items.length - 1]; + if (last && !last.sep && !last.value && last.start.length > 0 && findNonEmptyIndex(last.start) === -1 && (token.indent === 0 || last.start.every((st) => st.type !== "comment" || st.indent < token.indent))) { + if (top.type === "document") + top.end = last.start; + else + top.items.push({ start: last.start }); + token.items.splice(-1, 1); + } + } + } + } + *stream() { + switch (this.type) { + case "directive-line": + yield { type: "directive", offset: this.offset, source: this.source }; + return; + case "byte-order-mark": + case "space": + case "comment": + case "newline": + yield this.sourceToken; + return; + case "doc-mode": + case "doc-start": { + const doc = { + type: "document", + offset: this.offset, + start: [] + }; + if (this.type === "doc-start") + doc.start.push(this.sourceToken); + this.stack.push(doc); + return; + } + } + yield { + type: "error", + offset: this.offset, + message: `Unexpected ${this.type} token in YAML stream`, + source: this.source + }; + } + *document(doc) { + if (doc.value) + return yield* this.lineEnd(doc); + switch (this.type) { + case "doc-start": { + if (findNonEmptyIndex(doc.start) !== -1) { + yield* this.pop(); + yield* this.step(); + } else + doc.start.push(this.sourceToken); + return; + } + case "anchor": + case "tag": + case "space": + case "comment": + case "newline": + doc.start.push(this.sourceToken); + return; + } + const bv = this.startBlockValue(doc); + if (bv) + this.stack.push(bv); + else { + yield { + type: "error", + offset: this.offset, + message: `Unexpected ${this.type} token in YAML document`, + source: this.source + }; + } + } + *scalar(scalar) { + if (this.type === "map-value-ind") { + const prev = getPrevProps(this.peek(2)); + const start = getFirstKeyStartProps(prev); + let sep; + if (scalar.end) { + sep = scalar.end; + sep.push(this.sourceToken); + delete scalar.end; + } else + sep = [this.sourceToken]; + const map = { + type: "block-map", + offset: scalar.offset, + indent: scalar.indent, + items: [{ start, key: scalar, sep }] + }; + this.onKeyLine = true; + this.stack[this.stack.length - 1] = map; + } else + yield* this.lineEnd(scalar); + } + *blockScalar(scalar) { + switch (this.type) { + case "space": + case "comment": + case "newline": + scalar.props.push(this.sourceToken); + return; + case "scalar": + scalar.source = this.source; + this.atNewLine = true; + this.indent = 0; + if (this.onNewLine) { + let nl = this.source.indexOf("\n") + 1; + while (nl !== 0) { + this.onNewLine(this.offset + nl); + nl = this.source.indexOf("\n", nl) + 1; + } + } + yield* this.pop(); + break; + /* istanbul ignore next should not happen */ + default: + yield* this.pop(); + yield* this.step(); + } + } + *blockMap(map) { + const it = map.items[map.items.length - 1]; + switch (this.type) { + case "newline": + this.onKeyLine = false; + if (it.value) { + const end = "end" in it.value ? it.value.end : void 0; + const last = Array.isArray(end) ? end[end.length - 1] : void 0; + if (last?.type === "comment") + end?.push(this.sourceToken); + else + map.items.push({ start: [this.sourceToken] }); + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + it.start.push(this.sourceToken); + } + return; + case "space": + case "comment": + if (it.value) { + map.items.push({ start: [this.sourceToken] }); + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + if (this.atIndentedComment(it.start, map.indent)) { + const prev = map.items[map.items.length - 2]; + const end = prev?.value?.end; + if (Array.isArray(end)) { + Array.prototype.push.apply(end, it.start); + end.push(this.sourceToken); + map.items.pop(); + return; + } + } + it.start.push(this.sourceToken); + } + return; + } + if (this.indent >= map.indent) { + const atMapIndent = !this.onKeyLine && this.indent === map.indent; + const atNextItem = atMapIndent && (it.sep || it.explicitKey) && this.type !== "seq-item-ind"; + let start = []; + if (atNextItem && it.sep && !it.value) { + const nl = []; + for (let i = 0; i < it.sep.length; ++i) { + const st = it.sep[i]; + switch (st.type) { + case "newline": + nl.push(i); + break; + case "space": + break; + case "comment": + if (st.indent > map.indent) + nl.length = 0; + break; + default: + nl.length = 0; + } + } + if (nl.length >= 2) + start = it.sep.splice(nl[1]); + } + switch (this.type) { + case "anchor": + case "tag": + if (atNextItem || it.value) { + start.push(this.sourceToken); + map.items.push({ start }); + this.onKeyLine = true; + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + it.start.push(this.sourceToken); + } + return; + case "explicit-key-ind": + if (!it.sep && !it.explicitKey) { + it.start.push(this.sourceToken); + it.explicitKey = true; + } else if (atNextItem || it.value) { + start.push(this.sourceToken); + map.items.push({ start, explicitKey: true }); + } else { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: [this.sourceToken], explicitKey: true }] + }); + } + this.onKeyLine = true; + return; + case "map-value-ind": + if (it.explicitKey) { + if (!it.sep) { + if (includesToken(it.start, "newline")) { + Object.assign(it, { key: null, sep: [this.sourceToken] }); + } else { + const start2 = getFirstKeyStartProps(it.start); + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: start2, key: null, sep: [this.sourceToken] }] + }); + } + } else if (it.value) { + map.items.push({ start: [], key: null, sep: [this.sourceToken] }); + } else if (includesToken(it.sep, "map-value-ind")) { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, key: null, sep: [this.sourceToken] }] + }); + } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) { + const start2 = getFirstKeyStartProps(it.start); + const key = it.key; + const sep = it.sep; + sep.push(this.sourceToken); + delete it.key; + delete it.sep; + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: start2, key, sep }] + }); + } else if (start.length > 0) { + it.sep = it.sep.concat(start, this.sourceToken); + } else { + it.sep.push(this.sourceToken); + } + } else { + if (!it.sep) { + Object.assign(it, { key: null, sep: [this.sourceToken] }); + } else if (it.value || atNextItem) { + map.items.push({ start, key: null, sep: [this.sourceToken] }); + } else if (includesToken(it.sep, "map-value-ind")) { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: [], key: null, sep: [this.sourceToken] }] + }); + } else { + it.sep.push(this.sourceToken); + } + } + this.onKeyLine = true; + return; + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": { + const fs = this.flowScalar(this.type); + if (atNextItem || it.value) { + map.items.push({ start, key: fs, sep: [] }); + this.onKeyLine = true; + } else if (it.sep) { + this.stack.push(fs); + } else { + Object.assign(it, { key: fs, sep: [] }); + this.onKeyLine = true; + } + return; + } + default: { + const bv = this.startBlockValue(map); + if (bv) { + if (bv.type === "block-seq") { + if (!it.explicitKey && it.sep && !includesToken(it.sep, "newline")) { + yield* this.pop({ + type: "error", + offset: this.offset, + message: "Unexpected block-seq-ind on same line with key", + source: this.source + }); + return; + } + } else if (atMapIndent) { + map.items.push({ start }); + } + this.stack.push(bv); + return; + } + } + } + } + yield* this.pop(); + yield* this.step(); + } + *blockSequence(seq) { + const it = seq.items[seq.items.length - 1]; + switch (this.type) { + case "newline": + if (it.value) { + const end = "end" in it.value ? it.value.end : void 0; + const last = Array.isArray(end) ? end[end.length - 1] : void 0; + if (last?.type === "comment") + end?.push(this.sourceToken); + else + seq.items.push({ start: [this.sourceToken] }); + } else + it.start.push(this.sourceToken); + return; + case "space": + case "comment": + if (it.value) + seq.items.push({ start: [this.sourceToken] }); + else { + if (this.atIndentedComment(it.start, seq.indent)) { + const prev = seq.items[seq.items.length - 2]; + const end = prev?.value?.end; + if (Array.isArray(end)) { + Array.prototype.push.apply(end, it.start); + end.push(this.sourceToken); + seq.items.pop(); + return; + } + } + it.start.push(this.sourceToken); + } + return; + case "anchor": + case "tag": + if (it.value || this.indent <= seq.indent) + break; + it.start.push(this.sourceToken); + return; + case "seq-item-ind": + if (this.indent !== seq.indent) + break; + if (it.value || includesToken(it.start, "seq-item-ind")) + seq.items.push({ start: [this.sourceToken] }); + else + it.start.push(this.sourceToken); + return; + } + if (this.indent > seq.indent) { + const bv = this.startBlockValue(seq); + if (bv) { + this.stack.push(bv); + return; + } + } + yield* this.pop(); + yield* this.step(); + } + *flowCollection(fc) { + const it = fc.items[fc.items.length - 1]; + if (this.type === "flow-error-end") { + let top; + do { + yield* this.pop(); + top = this.peek(1); + } while (top?.type === "flow-collection"); + } else if (fc.end.length === 0) { + switch (this.type) { + case "comma": + case "explicit-key-ind": + if (!it || it.sep) + fc.items.push({ start: [this.sourceToken] }); + else + it.start.push(this.sourceToken); + return; + case "map-value-ind": + if (!it || it.value) + fc.items.push({ start: [], key: null, sep: [this.sourceToken] }); + else if (it.sep) + it.sep.push(this.sourceToken); + else + Object.assign(it, { key: null, sep: [this.sourceToken] }); + return; + case "space": + case "comment": + case "newline": + case "anchor": + case "tag": + if (!it || it.value) + fc.items.push({ start: [this.sourceToken] }); + else if (it.sep) + it.sep.push(this.sourceToken); + else + it.start.push(this.sourceToken); + return; + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": { + const fs = this.flowScalar(this.type); + if (!it || it.value) + fc.items.push({ start: [], key: fs, sep: [] }); + else if (it.sep) + this.stack.push(fs); + else + Object.assign(it, { key: fs, sep: [] }); + return; + } + case "flow-map-end": + case "flow-seq-end": + fc.end.push(this.sourceToken); + return; + } + const bv = this.startBlockValue(fc); + if (bv) + this.stack.push(bv); + else { + yield* this.pop(); + yield* this.step(); + } + } else { + const parent = this.peek(2); + if (parent.type === "block-map" && (this.type === "map-value-ind" && parent.indent === fc.indent || this.type === "newline" && !parent.items[parent.items.length - 1].sep)) { + yield* this.pop(); + yield* this.step(); + } else if (this.type === "map-value-ind" && parent.type !== "flow-collection") { + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + fixFlowSeqItems(fc); + const sep = fc.end.splice(1, fc.end.length); + sep.push(this.sourceToken); + const map = { + type: "block-map", + offset: fc.offset, + indent: fc.indent, + items: [{ start, key: fc, sep }] + }; + this.onKeyLine = true; + this.stack[this.stack.length - 1] = map; + } else { + yield* this.lineEnd(fc); + } + } + } + flowScalar(type) { + if (this.onNewLine) { + let nl = this.source.indexOf("\n") + 1; + while (nl !== 0) { + this.onNewLine(this.offset + nl); + nl = this.source.indexOf("\n", nl) + 1; + } + } + return { + type, + offset: this.offset, + indent: this.indent, + source: this.source + }; + } + startBlockValue(parent) { + switch (this.type) { + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return this.flowScalar(this.type); + case "block-scalar-header": + return { + type: "block-scalar", + offset: this.offset, + indent: this.indent, + props: [this.sourceToken], + source: "" + }; + case "flow-map-start": + case "flow-seq-start": + return { + type: "flow-collection", + offset: this.offset, + indent: this.indent, + start: this.sourceToken, + items: [], + end: [] + }; + case "seq-item-ind": + return { + type: "block-seq", + offset: this.offset, + indent: this.indent, + items: [{ start: [this.sourceToken] }] + }; + case "explicit-key-ind": { + this.onKeyLine = true; + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + start.push(this.sourceToken); + return { + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, explicitKey: true }] + }; + } + case "map-value-ind": { + this.onKeyLine = true; + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + return { + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, key: null, sep: [this.sourceToken] }] + }; + } + } + return null; + } + atIndentedComment(start, indent) { + if (this.type !== "comment") + return false; + if (this.indent <= indent) + return false; + return start.every((st) => st.type === "newline" || st.type === "space"); + } + *documentEnd(docEnd) { + if (this.type !== "doc-mode") { + if (docEnd.end) + docEnd.end.push(this.sourceToken); + else + docEnd.end = [this.sourceToken]; + if (this.type === "newline") + yield* this.pop(); + } + } + *lineEnd(token) { + switch (this.type) { + case "comma": + case "doc-start": + case "doc-end": + case "flow-seq-end": + case "flow-map-end": + case "map-value-ind": + yield* this.pop(); + yield* this.step(); + break; + case "newline": + this.onKeyLine = false; + // fallthrough + case "space": + case "comment": + default: + if (token.end) + token.end.push(this.sourceToken); + else + token.end = [this.sourceToken]; + if (this.type === "newline") + yield* this.pop(); + } + } + }; + exports.Parser = Parser; + } +}); + +// node_modules/yaml/dist/public-api.js +var require_public_api = __commonJS({ + "node_modules/yaml/dist/public-api.js"(exports) { + "use strict"; + var composer = require_composer(); + var Document = require_Document(); + var errors = require_errors(); + var log = require_log(); + var identity = require_identity(); + var lineCounter = require_line_counter(); + var parser = require_parser(); + function parseOptions(options) { + const prettyErrors = options.prettyErrors !== false; + const lineCounter$1 = options.lineCounter || prettyErrors && new lineCounter.LineCounter() || null; + return { lineCounter: lineCounter$1, prettyErrors }; + } + function parseAllDocuments(source, options = {}) { + const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options); + const parser$1 = new parser.Parser(lineCounter2?.addNewLine); + const composer$1 = new composer.Composer(options); + const docs = Array.from(composer$1.compose(parser$1.parse(source))); + if (prettyErrors && lineCounter2) + for (const doc of docs) { + doc.errors.forEach(errors.prettifyError(source, lineCounter2)); + doc.warnings.forEach(errors.prettifyError(source, lineCounter2)); + } + if (docs.length > 0) + return docs; + return Object.assign([], { empty: true }, composer$1.streamInfo()); + } + function parseDocument(source, options = {}) { + const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options); + const parser$1 = new parser.Parser(lineCounter2?.addNewLine); + const composer$1 = new composer.Composer(options); + let doc = null; + for (const _doc of composer$1.compose(parser$1.parse(source), true, source.length)) { + if (!doc) + doc = _doc; + else if (doc.options.logLevel !== "silent") { + doc.errors.push(new errors.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()")); + break; + } + } + if (prettyErrors && lineCounter2) { + doc.errors.forEach(errors.prettifyError(source, lineCounter2)); + doc.warnings.forEach(errors.prettifyError(source, lineCounter2)); + } + return doc; + } + function parse2(src, reviver, options) { + let _reviver = void 0; + if (typeof reviver === "function") { + _reviver = reviver; + } else if (options === void 0 && reviver && typeof reviver === "object") { + options = reviver; + } + const doc = parseDocument(src, options); + if (!doc) + return null; + doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning)); + if (doc.errors.length > 0) { + if (doc.options.logLevel !== "silent") + throw doc.errors[0]; + else + doc.errors = []; + } + return doc.toJS(Object.assign({ reviver: _reviver }, options)); + } + function stringify2(value, replacer, options) { + let _replacer = null; + if (typeof replacer === "function" || Array.isArray(replacer)) { + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + } + if (typeof options === "string") + options = options.length; + if (typeof options === "number") { + const indent = Math.round(options); + options = indent < 1 ? void 0 : indent > 8 ? { indent: 8 } : { indent }; + } + if (value === void 0) { + const { keepUndefined } = options ?? replacer ?? {}; + if (!keepUndefined) + return void 0; + } + if (identity.isDocument(value) && !_replacer) + return value.toString(options); + return new Document.Document(value, _replacer, options).toString(options); + } + exports.parse = parse2; + exports.parseAllDocuments = parseAllDocuments; + exports.parseDocument = parseDocument; + exports.stringify = stringify2; + } +}); + +// node_modules/yaml/dist/index.js +var require_dist = __commonJS({ + "node_modules/yaml/dist/index.js"(exports) { + "use strict"; + var composer = require_composer(); + var Document = require_Document(); + var Schema = require_Schema(); + var errors = require_errors(); + var Alias = require_Alias(); + var identity = require_identity(); + var Pair = require_Pair(); + var Scalar = require_Scalar(); + var YAMLMap = require_YAMLMap(); + var YAMLSeq = require_YAMLSeq(); + var cst = require_cst(); + var lexer = require_lexer(); + var lineCounter = require_line_counter(); + var parser = require_parser(); + var publicApi = require_public_api(); + var visit = require_visit(); + exports.Composer = composer.Composer; + exports.Document = Document.Document; + exports.Schema = Schema.Schema; + exports.YAMLError = errors.YAMLError; + exports.YAMLParseError = errors.YAMLParseError; + exports.YAMLWarning = errors.YAMLWarning; + exports.Alias = Alias.Alias; + exports.isAlias = identity.isAlias; + exports.isCollection = identity.isCollection; + exports.isDocument = identity.isDocument; + exports.isMap = identity.isMap; + exports.isNode = identity.isNode; + exports.isPair = identity.isPair; + exports.isScalar = identity.isScalar; + exports.isSeq = identity.isSeq; + exports.Pair = Pair.Pair; + exports.Scalar = Scalar.Scalar; + exports.YAMLMap = YAMLMap.YAMLMap; + exports.YAMLSeq = YAMLSeq.YAMLSeq; + exports.CST = cst; + exports.Lexer = lexer.Lexer; + exports.LineCounter = lineCounter.LineCounter; + exports.Parser = parser.Parser; + exports.parse = publicApi.parse; + exports.parseAllDocuments = publicApi.parseAllDocuments; + exports.parseDocument = publicApi.parseDocument; + exports.stringify = publicApi.stringify; + exports.visit = visit.visit; + exports.visitAsync = visit.visitAsync; + } +}); + +// src/core-skills/bmad-module/scripts/lib/vendor/vendor-entry.mjs +var import_yaml = __toESM(require_dist(), 1); +var export_parse = import_yaml.parse; +var export_stringify = import_yaml.stringify; +export { + export_parse as parse, + export_stringify as stringify +}; diff --git a/src/core-skills/bmad-module/scripts/list.mjs b/src/core-skills/bmad-module/scripts/list.mjs new file mode 100644 index 000000000..13fe89196 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/list.mjs @@ -0,0 +1,43 @@ +import { EXIT, BmadModuleError } from './lib/exit.mjs'; +import { findBmadDir } from './lib/bmad-dir.mjs'; +import { listModuleEntries } from './lib/manifest-ops.mjs'; + +// List community-source modules from manifest.yaml. Output is a fixed-width +// table; `--json` swaps in JSON for programmatic callers. +export async function runList(opts) { + const projectDir = opts.projectDir || process.cwd(); + const bmadDir = await findBmadDir(projectDir); + if (!bmadDir) { + throw new BmadModuleError(EXIT.NO_BMAD_DIR, `no _bmad/ found in ${projectDir}`); + } + + const modules = await listModuleEntries(bmadDir); + + if (opts.json) { + process.stdout.write(JSON.stringify({ modules }, null, 2) + '\n'); + return; + } + + if (modules.length === 0) { + process.stdout.write(`[bmad-module] no modules installed.\n`); + return; + } + + const rows = modules.map((p) => ({ + code: p.name, + name: p.moduleName || '-', + version: p.version || '-', + sha: p.sha ? p.sha.slice(0, 7) : '-', + source: p.repoUrl || p.rawSource || '-', + installed: p.installDate ? p.installDate.slice(0, 10) : '-', + })); + const cols = ['code', 'name', 'version', 'sha', 'source', 'installed']; + const widths = cols.reduce((acc, c) => { + acc[c] = Math.max(c.length, ...rows.map((r) => String(r[c]).length)); + return acc; + }, {}); + const fmt = (r) => cols.map((c) => String(r[c]).padEnd(widths[c])).join(' '); + process.stdout.write(fmt(Object.fromEntries(cols.map((c) => [c, c.toUpperCase()]))) + '\n'); + process.stdout.write(cols.map((c) => '-'.repeat(widths[c])).join(' ') + '\n'); + for (const r of rows) process.stdout.write(fmt(r) + '\n'); +} diff --git a/src/core-skills/bmad-module/scripts/remove.mjs b/src/core-skills/bmad-module/scripts/remove.mjs new file mode 100644 index 000000000..db18e6d8e --- /dev/null +++ b/src/core-skills/bmad-module/scripts/remove.mjs @@ -0,0 +1,125 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { EXIT, BmadModuleError } from './lib/exit.mjs'; +import { findBmadDir } from './lib/bmad-dir.mjs'; +import { pruneEmptyDirs, safePathInsideRoot } from './lib/fs-safe.mjs'; +import { + readManifestYaml, + removeModuleFromManifest, + removeSkillManifestRows, + removeFilesManifestRows, + readFileEntriesForModule, + readSkillCanonicalIdsForModule, +} from './lib/manifest-ops.mjs'; +import { distributeToIdes } from './lib/ide-sync.mjs'; +import { removeModuleFromConfig } from './lib/config-gen.mjs'; +import { regenerateHelpCatalog } from './lib/help-catalog.mjs'; + +// Remove a module's installed files and manifest entries. With `--purge` also +// deletes `_bmad/custom//` (user customization dir). Without it, customs +// are preserved so a re-install picks them back up. +export async function runRemove(opts) { + const projectDir = opts.projectDir || process.cwd(); + const code = opts.code; + if (!code) throw new BmadModuleError(EXIT.USAGE, `bmad-module remove is required`); + + const bmadDir = await findBmadDir(projectDir); + if (!bmadDir) { + throw new BmadModuleError(EXIT.NO_BMAD_DIR, `no _bmad/ found in ${projectDir}`); + } + + const manifest = await readManifestYaml(bmadDir); + const entry = manifest?.modules?.find((m) => m && m.name === code); + if (!entry) { + throw new BmadModuleError(EXIT.NOT_INSTALLED, `no module "${code}" in manifest.yaml`); + } + if (entry.source !== 'community') { + throw new BmadModuleError( + EXIT.PREFIX_COLLISION, + `module "${code}" was installed as source="${entry.source}", not "community". ` + + `Use the appropriate uninstaller (e.g. \`bmad-method uninstall\`).`, + ); + } + + // Capture the module's distributed skill ids before dropping its manifest + // rows, so we can prune them from the IDE directories afterward. + const removedSkillIds = await readSkillCanonicalIdsForModule(bmadDir, code); + + // Strip the module's central-config blocks ([modules.] + its [agents.*]) + // while its module.yaml still exists on disk to identify the agent codes. + try { + await removeModuleFromConfig(bmadDir, code); + } catch (e) { + process.stderr.write(`[bmad-module] warning: failed to update config for removal of ${code}: ${e.message}\n`); + } + + // Resolve the module root with a containment check, so a traversal-tainted + // code (e.g. from a hand-edited manifest) can never delete outside _bmad/. + const moduleRoot = safePathInsideRoot(bmadDir, code); + if (!moduleRoot) { + throw new BmadModuleError(EXIT.PATH_TRAVERSAL, `module code "${code}" escapes _bmad/`); + } + + // Delete each file tracked in files-manifest.csv; prune empty dirs after. + const fileEntries = await readFileEntriesForModule(bmadDir, code); + for (const fe of fileEntries) { + const abs = safePathInsideRoot(bmadDir, fe.path); + if (!abs) { + process.stderr.write(`[bmad-module] warn: skipping files-manifest path that escapes _bmad/: ${fe.path}\n`); + continue; + } + try { + await fs.rm(abs, { force: true }); + await pruneEmptyDirs(path.dirname(abs), moduleRoot); + } catch (e) { + process.stderr.write(`[bmad-module] warn: failed to remove ${fe.path}: ${e.message}\n`); + } + } + + // Remove the module root if it still exists (in case files-manifest was + // incomplete or empty). Safe — at this point we've confirmed source=community. + await fs.rm(moduleRoot, { recursive: true, force: true }); + + // Optionally purge custom overrides. + if (opts.purge) { + const customDir = safePathInsideRoot(path.join(bmadDir, 'custom'), code); + if (customDir) await fs.rm(customDir, { recursive: true, force: true }); + } + + // Drop manifest rows. + await removeFilesManifestRows(bmadDir, code); + await removeSkillManifestRows(bmadDir, code); + await removeModuleFromManifest(bmadDir, code); + + // Rebuild the merged help catalog now that the module's module-help.csv is + // gone, so its skills disappear from `bmad-help`. + try { + await regenerateHelpCatalog(bmadDir); + } catch (e) { + process.stderr.write(`[bmad-module] warning: help catalog rebuild failed: ${e.message}\n`); + } + + // Prune the module's skills from every configured coding assistant. The + // manifest no longer lists the module, so ide-sync removes its skill dirs + + // command pointers and re-syncs the rest. + const ideResult = await distributeToIdes({ projectDir, bmadDir, prune: removedSkillIds }); + if (!ideResult.skipped && !ideResult.ok) { + process.stderr.write(`[bmad-module] warning: ${ideResult.hint}\n`); + } + + process.stdout.write(`[bmad-module] removed ${code} (${fileEntries.length} file(s))\n`); + if (opts.purge) { + process.stdout.write(`[bmad-module] purged _bmad/custom/${code}/\n`); + } else if (await dirExists(path.join(bmadDir, 'custom', code))) { + process.stdout.write(`[bmad-module] preserved _bmad/custom/${code}/ (use --purge to remove)\n`); + } +} + +async function dirExists(p) { + try { + const s = await fs.stat(p); + return s.isDirectory(); + } catch { + return false; + } +} diff --git a/src/core-skills/bmad-module/scripts/update.mjs b/src/core-skills/bmad-module/scripts/update.mjs new file mode 100644 index 000000000..ebf42f251 --- /dev/null +++ b/src/core-skills/bmad-module/scripts/update.mjs @@ -0,0 +1,166 @@ +import path from 'node:path'; +import { EXIT, BmadModuleError } from './lib/exit.mjs'; +import { findBmadDir } from './lib/bmad-dir.mjs'; +import { parseSource, materializeSource } from './lib/source.mjs'; +import { readAndValidateManifest } from './lib/plugin-json.mjs'; +import { readUserIgnores, buildIgnoreMatcher, buildCopyPlan, rewriteManifestPaths, validateDeclaredPaths } from './lib/install-plan.mjs'; +import { stageCopyPlan, atomicSwapDir, sha256File, pruneEmptyDirs, safePathInsideRoot } from './lib/fs-safe.mjs'; +import { + readManifestYaml, + addModuleToManifest, + appendSkillManifestRows, + appendFilesManifestRows, + removeSkillManifestRows, + removeFilesManifestRows, + readFileEntriesForModule, + readSkillCanonicalIdsForModule, +} from './lib/manifest-ops.mjs'; +import { distributeToIdes } from './lib/ide-sync.mjs'; +import { finishModuleInstall, resolveCloneTarget } from './install.mjs'; + +// Update one installed module (or all when opts.all is true). v1 semantics: +// - Re-resolves the original source (or new --ref) and re-clones. +// - Same sha → no-op. +// - Different sha → diff files-manifest rows; abort if any tracked file has +// been modified locally; otherwise install-over-top and prune removed. +export async function runUpdate(opts) { + const projectDir = opts.projectDir || process.cwd(); + const bmadDir = await findBmadDir(projectDir); + if (!bmadDir) { + throw new BmadModuleError(EXIT.NO_BMAD_DIR, `no _bmad/ found in ${projectDir}`); + } + + const manifest = await readManifestYaml(bmadDir); + const allModules = (manifest?.modules || []).filter((m) => m && m.source === 'community'); + + let targets; + if (opts.all) { + targets = allModules; + } else { + if (!opts.code) throw new BmadModuleError(EXIT.USAGE, `bmad-module update is required`); + const t = allModules.find((m) => m.name === opts.code); + if (!t) throw new BmadModuleError(EXIT.NOT_INSTALLED, `no module "${opts.code}" in manifest.yaml`); + targets = [t]; + } + + for (const entry of targets) { + await updateOne(bmadDir, projectDir, entry, opts); + } +} + +async function updateOne(bmadDir, projectDir, entry, opts) { + const code = entry.name; + if (!entry.rawSource) { + throw new BmadModuleError(EXIT.BAD_MANIFEST, `module ${code} has no rawSource in manifest.yaml — cannot re-resolve`); + } + const descriptor = parseSource(entry.rawSource); + // Re-resolve against the channel/ref the module was installed with, unless the + // CLI overrides either. A `stable` entry re-resolves to the latest tag; a + // pinned entry stays put unless `--ref` moves it. + const target = await resolveCloneTarget(descriptor, { + ref: opts.ref ?? entry.ref ?? null, + channel: opts.channel ?? entry.channel ?? null, + }); + const materialized = await materializeSource(descriptor, { ref: target.ref }); + + try { + // No-op fast path. + if (materialized.sha && materialized.sha === entry.sha) { + process.stdout.write(`[bmad-module] ${code} already at ${materialized.sha.slice(0, 7)} — no-op.\n`); + return; + } + + const manifest = await readAndValidateManifest(materialized.dir); + if (manifest.bmad.code !== code) { + throw new BmadModuleError( + EXIT.PREFIX_COLLISION, + `source manifest declares bmad.code "${manifest.bmad.code}" but installed code is "${code}"`, + ); + } + + // Capture the currently-distributed skill ids before we rewrite the + // manifest, so any skill dropped between versions is pruned from the IDE + // directories (and re-distributed ones are refreshed). + const oldSkillIds = await readSkillCanonicalIdsForModule(bmadDir, code); + + // Modified-file check: any tracked file whose on-disk hash diverges from + // the recorded one is treated as user-modified. Abort rather than clobber. + const oldEntries = await readFileEntriesForModule(bmadDir, code); + const modified = []; + for (const fe of oldEntries) { + const abs = safePathInsideRoot(bmadDir, fe.path); + if (!abs) continue; // an entry that escapes _bmad/ is not a tracked file + const current = await sha256File(abs); + if (current === null) continue; + if (fe.hash && current !== fe.hash) modified.push(fe.path); + } + if (modified.length) { + throw new BmadModuleError( + EXIT.MODIFIED_FILES, + `update would overwrite ${modified.length} locally-modified file(s):\n ` + + modified.join('\n ') + + `\nMove your changes into _bmad/custom/${code}/ and re-run.`, + ); + } + + // Build new copy plan, stage, swap. + validateDeclaredPaths(materialized.dir, manifest); + const userIgnores = await readUserIgnores(materialized.dir, manifest); + const matchIgnore = buildIgnoreMatcher(userIgnores); + const { plan, skillDestDirs } = await buildCopyPlan(materialized.dir, manifest, matchIgnore); + const rewrittenManifestJson = rewriteManifestPaths(manifest); + + const stagedDir = path.join(path.dirname(materialized.dir), 'staged-out'); + await stageCopyPlan(materialized.dir, stagedDir, plan, { + '.claude-plugin/plugin.json': rewrittenManifestJson, + }); + const targetDir = safePathInsideRoot(bmadDir, code); + if (!targetDir) { + throw new BmadModuleError(EXIT.PATH_TRAVERSAL, `module code "${code}" escapes _bmad/`); + } + try { + await atomicSwapDir(stagedDir, targetDir); + } catch (e) { + throw new BmadModuleError(EXIT.COMMIT_FAILURE, `failed to swap into ${targetDir}: ${e.message}`); + } + + // Manifest rewrites: remove old rows for this code, then re-append. + await removeSkillManifestRows(bmadDir, code); + await removeFilesManifestRows(bmadDir, code); + await addModuleToManifest(bmadDir, code, { + version: descriptor.kind === 'git' ? target.version : manifest.bmad.moduleVersion || manifest.version, + repoUrl: descriptor.kind === 'git' ? descriptor.url : null, + sha: materialized.sha, + ref: materialized.ref, + channel: target.channel, + rawSource: descriptor.rawInput, + moduleName: manifest.name, + }); + const destPaths = ['.claude-plugin/plugin.json', ...plan.map((p) => p.destRel)]; + await appendSkillManifestRows(bmadDir, code, skillDestDirs); + await appendFilesManifestRows(bmadDir, code, destPaths); + + // Prune empty dirs left behind from removed files. (The atomic swap of + // the module root already replaced everything; this is a no-op guard for + // the edge case where rm-then-mkdir leaves stale parents.) + await pruneEmptyDirs(targetDir, bmadDir); + + process.stdout.write( + `[bmad-module] updated ${code} (${manifest.name} ${manifest.version})${materialized.sha ? ` @ ${materialized.sha.slice(0, 7)}` : ''}\n`, + ); + process.stdout.write(`[bmad-module] previous ${oldEntries.length} file(s) → new ${destPaths.length} file(s)\n`); + + // Re-run the same post-copy completion as install: deps, config + agent + // roster, working directories (moves if a path changed), and help catalog. + await finishModuleInstall({ bmadDir, code, targetDir, manifest, setOverrides: opts.setOverrides }); + + // Re-distribute to the configured coding assistants: prune skills that no + // longer exist in this version, refresh the rest. + const ideResult = await distributeToIdes({ projectDir, bmadDir, prune: oldSkillIds }); + if (!ideResult.skipped && !ideResult.ok) { + process.stderr.write(`[bmad-module] warning: ${ideResult.hint}\n`); + } + } finally { + await materialized.cleanup(); + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/.claude-plugin/plugin.json b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/.claude-plugin/plugin.json new file mode 100644 index 000000000..3d88dd425 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/.claude-plugin/plugin.json @@ -0,0 +1,48 @@ +{ + "name": "acme-devlog", + "version": "0.4.0", + "displayName": "Devlog", + "description": "Daily engineering devlog: write entries, summarize history, and consult Clio the Historian.", + "author": { + "name": "Acme Corp", + "email": "team@acme.example", + "url": "https://acme.example" + }, + "repository": "https://github.com/acme/acme-devlog", + "license": "MIT", + "homepage": "https://acme.example/devlog", + "keywords": ["devlog", "history", "summarization", "engineering-log"], + + "skills": ["./skills/bmad-devlog-setup", "./skills/bmad-devlog-write", "./skills/bmad-devlog-summarize", "./skills/bmad-agent-historian"], + "agents": ["./agents/changelog-archivist.md"], + "hooks": "./hooks/hooks.json", + "mcpServers": "./.mcp.json", + + "bmad": { + "specVersion": "1.0.0", + "code": "devlog", + "category": "knowledge-management", + "subcategory": "history", + "compatibility": { + "bmadMethod": ">=6.6.0 <7.0.0" + }, + "setupSkill": "bmad-devlog-setup", + "moduleDefinition": "skills/bmad-devlog-setup/assets/module.yaml", + "moduleHelpCsv": "skills/bmad-devlog-setup/assets/module-help.csv", + "customize": { + "schemas": ["./skills/bmad-agent-historian/customize.toml"] + }, + "dependencies": { + "modules": [{ "code": "bmm", "version": ">=6.6.0" }] + }, + "install": { + "ignore": ["docs/**", "tests/**", "*.test.*", "README.md", "CHANGELOG.md"], + "postInstallSkill": "bmad-devlog-setup" + }, + "docs": { + "readme": "./README.md", + "changelog": "./CHANGELOG.md", + "homepage": "./docs/index.md" + } + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/.mcp.json b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/.mcp.json new file mode 100644 index 000000000..a73e9e07a --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "devlog-history": { + "command": "node", + "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.js"], + "env": { + "DEVLOG_PATH": "${CLAUDE_PLUGIN_DATA}/devlog" + } + } + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/CHANGELOG.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/CHANGELOG.md new file mode 100644 index 000000000..f821f7a0e --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this module will be documented here. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.4.0] — 2026-05-21 + +### Added + +- Clio the Historian persona-agent (`bmad-agent-historian`) with three menu actions: summarize range, recall topic context, identify patterns. +- `changelog-archivist` Claude subagent for fan-out summarization across long date ranges. +- MCP server stub (`devlog-history`) for programmatic queries against past entries. +- `SessionStart` hook surfaces today's entry (or yesterday's if today is empty). + +### Changed + +- Devlog entry template now includes "Open questions" and "Blockers" sections. +- Setup skill records `devlog_path` separately from `output_folder` so entries are addressable independently of other BMAD output. + +## [0.3.0] — 2026-04-30 + +### Added + +- Initial implementation: `bmad-devlog-write` and `bmad-devlog-summarize` skills. +- Setup skill scaffolding. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/LICENSE b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/LICENSE new file mode 100644 index 000000000..5c1942827 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Acme Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/README.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/README.md new file mode 100644 index 000000000..fe1930a81 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/README.md @@ -0,0 +1,43 @@ +# acme-devlog + +A daily engineering devlog for BMAD-driven projects. + +This module demonstrates **every supported surface** of the BMAD Module Manifest Specification — skills, a persona-agent with a `customize.toml`, a Claude subagent, a SessionStart hook, an MCP server stub, install-time configuration via `module.yaml`, and a setup skill. Use it as a reference when authoring a real module. + +## What it does + +- **Write** a structured daily entry (template-driven) with `/bmad-devlog-write`. +- **Summarize** a date range with `/bmad-devlog-summarize`. +- **Consult Clio**, a persona-agent historian, with `/bmad-agent-historian`. Clio narrates patterns across entries, surfaces forgotten context, and routes you to the right write/summarize skill. +- **Show today's entry** at session start (via the `SessionStart` hook). +- **Query history programmatically** via the bundled MCP server stub (`devlog-history`). + +## Install + +``` +bmad-module install acme/acme-devlog +``` + +Installs to `_bmad/devlog/`. The setup skill (`bmad-devlog-setup`) runs automatically and prompts for the devlog output path (default: `_bmad-output/devlog`). + +## Configuration + +After install, `_bmad/devlog/config.yaml` records your devlog path. Override defaults per skill via `_bmad/custom/`: + +- Team: `_bmad/custom/bmad-agent-historian.toml` +- Personal: `_bmad/custom/bmad-agent-historian.user.toml` + +See `docs/index.md` (in this repo) for the customization recipe. + +## Uninstall + +``` +bmad-module remove devlog # leaves _bmad/custom/bmad-agent-historian*.toml intact +bmad-module remove devlog --purge # removes user customizations too +``` + +Devlog entries written to `_bmad-output/devlog/` are **never** deleted by uninstall. + +## License + +MIT. See `LICENSE`. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/agents/changelog-archivist.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/agents/changelog-archivist.md new file mode 100644 index 000000000..e923360de --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/agents/changelog-archivist.md @@ -0,0 +1,47 @@ +--- +name: changelog-archivist +description: Summarizes a single week of devlog entries into a tight changelog-style brief. Used as a fan-out subagent by /bmad-devlog-summarize for long date ranges. +model: sonnet +--- + +# Changelog Archivist + +You are a focused subagent invoked by the `bmad-devlog-summarize` skill to compress a single week's devlog entries into a brief paragraph. + +## Inputs + +You receive: + +- A list of devlog entry file paths covering one ISO week. +- The week label (e.g. `2026-W21`). + +## Task + +For each entry, extract: + +- What shipped (verbatim where possible). +- Recurring blockers. +- Decisions visible in the entry. + +Produce a single section in this exact shape: + +``` +### + +**Shipped.** + +**Blockers.** + +**Decisions.** +``` + +## Rules + +- Do not invent content. If the week has no entries, output the week label and `_no entries this week_` and stop. +- Do not editorialize beyond what's in the entries. +- Cite dates inline (e.g. `On 2026-05-14, …`) only when a single day's content dominates. +- Keep the section under 120 words. + +## Why a subagent + +For ranges longer than two weeks, the parent skill fans out one subagent per week so each archivist operates with a small, focused context. The parent then concatenates the sections and adds a top-level intro. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/docs/index.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/docs/index.md new file mode 100644 index 000000000..f61946f8d --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/docs/index.md @@ -0,0 +1,56 @@ +# acme-devlog — Authoring & Customization + +This doc lives in the module source. It is **excluded** from install via `bmad.install.ignore` (see `.claude-plugin/plugin.json`). Read it on GitHub or in a clone of the module repo, not under `_bmad/devlog/`. + +## Customizing Clio + +Clio's defaults live in `_bmad/devlog/skills/bmad-agent-historian/customize.toml`. You should not edit that file directly — the installer overwrites it on `bmad-module update`. + +Instead, drop overrides into one of: + +| File | Scope | Git status | +| --------------------------------------------- | ---------------- | ----------- | +| `_bmad/custom/bmad-agent-historian.toml` | Team (committed) | tracked | +| `_bmad/custom/bmad-agent-historian.user.toml` | Personal | git-ignored | + +Both files use the same TOML shape as the base `customize.toml`. The `bmad-customize` core skill merges them in this order: base → team → personal. + +**Merge rules:** + +- Scalars: override wins. +- Tables: deep merge. +- Arrays of tables keyed by `code` (e.g. `[[agent.menu]]`): matching codes replace, new codes append. +- Other arrays (`persistent_facts`, `principles`, `activation_steps_*`): append. + +### Example — add a menu item + +`_bmad/custom/bmad-agent-historian.user.toml`: + +```toml +[[agent.menu]] +code = "TODAY" +description = "Read today's entry aloud" +prompt = """ +Read the file at `{devlog_path}/$(date +%F).md` if it exists. If not, +say "No entry yet for today — try the WRT menu item." +""" +``` + +### Example — change communication style + +`_bmad/custom/bmad-agent-historian.toml`: + +```toml +[agent] +communication_style = "Crisp, dry, footnoted. Cites entries like a historian writing for The Economist." +``` + +## Why a SessionStart hook? + +The hook is convenience, not requirement. Disable it by removing the `hooks` field from `_bmad/devlog/.claude-plugin/plugin.json` (or by uninstalling). It surfaces the current entry so you have context the moment a session starts; for some teams that's noise — opt out freely. + +## Why `module.yaml` lives under `skills/bmad-devlog-setup/assets/`? + +This module uses PluginResolver Strategy 2 (the `-setup` skill carries the module definition in `assets/`). The advantage is that `bmad-devlog-setup` can be re-run any time to reconfigure without touching the rest of the install. The `bmad.moduleDefinition` field in `plugin.json` points the installer at the right file. + +A simpler module can put `module.yaml` at `skills/module.yaml` (the default) and skip `bmad.setupSkill`. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/hooks/hooks.json b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/hooks/hooks.json new file mode 100644 index 000000000..f09995637 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/hooks/hooks.json @@ -0,0 +1,15 @@ +{ + "description": "Surface today's (or most recent) devlog entry at session start.", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/fetch-git-history.sh\"" + } + ] + } + ] + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/scripts/fetch-git-history.sh b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/scripts/fetch-git-history.sh new file mode 100755 index 000000000..d050347b4 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/scripts/fetch-git-history.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# SessionStart hook: print today's devlog entry if it exists, else the most +# recent entry. Reads devlog_path from _bmad/devlog/config.yaml. +# +# Bound by Claude Code's SessionStart event via hooks/hooks.json. Exits 0 +# silently when there's nothing useful to surface. + +set -eu + +config="${PWD}/_bmad/devlog/config.yaml" +[ -f "$config" ] || exit 0 + +devlog_path=$(awk -F': *' '/^devlog_path:/ {print $2; exit}' "$config" | tr -d '"') +[ -n "$devlog_path" ] && [ -d "$devlog_path" ] || exit 0 + +today="$(date +%F)" +today_file="${devlog_path}/${today}.md" + +if [ -f "$today_file" ]; then + echo "=== Devlog — ${today} ===" + cat "$today_file" + exit 0 +fi + +# Fall back to the most recent .md by mtime. Glob + `-nt` instead of parsing +# `ls` so filenames with spaces/newlines are handled safely (and ShellCheck +# stays happy). +latest="" +for f in "${devlog_path}"/*.md; do + [ -e "$f" ] || continue + if [ -z "$latest" ] || [ "$f" -nt "$latest" ]; then + latest="$f" + fi +done +if [ -n "$latest" ]; then + echo "=== Most recent devlog ($(basename "$latest" .md)) ===" + cat "$latest" +fi + +exit 0 diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-agent-historian/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-agent-historian/SKILL.md new file mode 100644 index 000000000..d3f1a74e5 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-agent-historian/SKILL.md @@ -0,0 +1,60 @@ +--- +name: bmad-agent-historian +description: Clio, the Devlog Historian. A persona-agent for narrative recall, pattern detection across entries, and routing to the right devlog action. Use when the user asks to talk to Clio or requests the historian. +--- + +# Clio — Devlog Historian + +## Overview + +You are Clio, named for the muse of history. You read the project's devlog with an archivist's care and a journalist's nose for the story underneath the entries. You don't invent context — every observation you make is grounded in a specific entry, cited by date. + +## Conventions + +- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives). +- `{project-root}` resolves to the project working directory. +- `{skill-name}` resolves to this skill's directory basename. + +## On Activation + +### Step 1: Resolve the Agent Block + +Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent` + +If the script fails, resolve the `agent` block yourself by reading these three files in base → team → user order and applying BMad's structural merge rules: + +1. `{skill-root}/customize.toml` — defaults +2. `{project-root}/_bmad/custom/{skill-name}.toml` — team overrides +3. `{project-root}/_bmad/custom/{skill-name}.user.toml` — personal overrides + +Scalars override, tables deep-merge, arrays of tables keyed by `code` or `id` replace matching entries and append new entries, and all other arrays append. + +### Step 2: Adopt Persona + +Adopt the Clio identity from the Overview. Layer on `{agent.role}`, `{agent.identity}`, `{agent.communication_style}`, and `{agent.principles}` from the resolved block. + +Do not break character until the user dismisses the persona. + +### Step 3: Load Persistent Facts + +Treat every entry in `{agent.persistent_facts}` as foundational context. Entries prefixed `file:` are paths or globs under `{project-root}` — load the referenced contents as facts. + +### Step 4: Load Devlog Config + +Load `{project-root}/_bmad/devlog/config.yaml`. Note `devlog_path` and `entry_format`. If the config is missing, tell the user kindly: + +> "I can't find the devlog config yet — run `/bmad-devlog-setup` first and call me back." + +…then stand down. + +### Step 5: Greet and Present Menu + +Greet using `{user_name}` from `{project-root}/_bmad/bmm/config.yaml` (fallback: "friend"). Then present the menu (`{agent.menu}`) as a numbered list. Each item has a `code`, `description`, and either a `skill` or a `prompt`. + +### Step 6: Handle Selection + +When the user picks a menu code, invoke its `skill` or execute its `prompt`. The persona carries through; stay in character. + +## Notes for Authors + +This is a reference persona-agent. The `customize.toml` next door defines Clio's defaults — copy and adapt the structure for your own persona-agent skills. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-agent-historian/customize.toml b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-agent-historian/customize.toml new file mode 100644 index 000000000..caf579c55 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-agent-historian/customize.toml @@ -0,0 +1,80 @@ +# DO NOT EDIT -- overwritten on every update. +# +# Clio, the Devlog Historian, is the hardcoded identity of this agent. +# Customize the persona and menu below to shape behavior without changing +# who the agent is. Override in _bmad/custom/bmad-agent-historian.toml +# (team) or _bmad/custom/bmad-agent-historian.user.toml (personal). + +[agent] +# Non-configurable skill frontmatter. Create a custom persona-agent skill +# if you need a new name/title. +name = "Clio" +title = "Devlog Historian" + +# --- Configurable below. Overrides merge per BMad structural rules: --- +# scalars: override wins • arrays (persistent_facts, principles, activation_steps_*): append +# arrays-of-tables with `code`/`id`: replace matching items, append new ones. + +icon = "🕰️" + +activation_steps_prepend = [] +activation_steps_append = [] + +# Persistent facts the agent keeps in mind for the whole session. +# `file:` entries are loaded as content; literal entries are facts verbatim. +# Note: the devlog config is intentionally NOT loaded here as an always-on +# `file:` fact — it may not exist on first run, which would fail activation +# before the `/bmad-devlog-setup` fallback can create it. Skills read the +# config after checking it exists. +persistent_facts = [ + "The devlog is a primary source. Never invent context that isn't written down.", +] + +role = "Help the user recall, analyze, and narrate the project's history through the devlog." +identity = "Channels the patient eye of an archivist and the narrative nose of a journalist. Treats every entry as primary source material." +communication_style = "Calm and measured, with date markers (`On 2026-05-14, …`). Tells the story underneath the entries; never editorializes beyond what's written." + +principles = [ + "Every observation cites at least one entry by date.", + "Patterns over events: surface what recurs, not what's loudest.", + "When the record is silent, say so — do not extrapolate.", + "Route the user to write/summarize skills when they need to act, not reminisce.", +] + +# Capabilities menu. Overrides merge by `code`: matching codes replace the +# item in place; new codes append. Each item has exactly one of `skill` +# (invokes a registered skill) or `prompt` (executes the prompt text directly). + +[[agent.menu]] +code = "SUM" +description = "Summarize a date range" +skill = "bmad-devlog-summarize" + +[[agent.menu]] +code = "REC" +description = "Recall context for a topic" +prompt = """ +Ask the user for a topic or keyword. Search every devlog entry under +`{devlog_path}` for matches. For each match, cite the entry date and quote +the relevant lines. End with a one-paragraph narrative tying the matches +together. If no matches, say so plainly. +""" + +[[agent.menu]] +code = "PAT" +description = "Identify recurring patterns" +prompt = """ +Read every entry in the last 30 days. Identify themes that appear in ≥3 +entries (recurring blockers, repeated decisions, drifting open questions). +Present each pattern as: name, evidence (3+ cited entries), implication. +""" + +[[agent.menu]] +code = "WRT" +description = "Write today's entry" +skill = "bmad-devlog-write" + +[[agent.menu]] +code = "EXIT" +description = "Dismiss Clio" +prompt = "Acknowledge the dismissal in character ('Until the next entry, then.'), break persona, and return control." diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/SKILL.md new file mode 100644 index 000000000..824c00fae --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/SKILL.md @@ -0,0 +1,45 @@ +--- +name: bmad-devlog-setup +description: One-time setup for the acme-devlog module. Use after `bmad-module install acme/acme-devlog`, or when the user says "configure devlog" or "re-run devlog setup". +--- + +# Devlog Setup + +Installs and configures the devlog module by reading `assets/module.yaml`, collecting answers to its prompts, and writing `_bmad/devlog/config.yaml`. + +This skill is invoked automatically by `bmad-module install` (via `bmad.install.postInstallSkill`). It is also safe to re-run any time — it merges over the existing config without losing prior answers. + +## EXECUTION + +### Step 1: Load the module definition + +Read `./assets/module.yaml` from the skill root. Parse its prompt entries (e.g. `devlog_path`, `entry_format`). + +### Step 2: Collect answers + +For each prompt, ask the user. Show the default in parens. Accept the default when the user replies with empty input or "use default". + +If `_bmad/devlog/config.yaml` already exists, load existing answers and pre-fill them as the prompt default. + +### Step 3: Apply variable substitution + +Resolve `{value}`, `{project-root}`, and `{output_folder}` placeholders using each prompt's `result:` template. + +### Step 4: Write config + +Write the resolved key/value map to `_bmad/devlog/config.yaml` (YAML, 2-space indent, keys sorted). + +### Step 5: Create directories + +Ensure `{devlog_path}` exists on disk. Create it if absent. + +### Step 6: Confirm + +Print: + +``` +Devlog configured. + devlog_path: + entry_format: +Try `/bmad-devlog-write` to create today's entry. +``` diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/assets/module-help.csv b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/assets/module-help.csv new file mode 100644 index 000000000..6bf829b6c --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/assets/module-help.csv @@ -0,0 +1,4 @@ +module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs +devlog,bmad-devlog-write,Write Devlog Entry,dw,Write today's devlog entry from a template.,bmad-devlog-write,,daily,,,,{devlog_path},entry.md +devlog,bmad-devlog-summarize,Summarize Devlog,ds,Summarize devlog entries across a date range.,bmad-devlog-summarize,,history,,,,,summary.md +devlog,bmad-agent-historian,Clio the Historian,ch,Persona-agent for narrative recall and pattern detection.,bmad-agent-historian,,,,,,, diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/assets/module.yaml b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/assets/module.yaml new file mode 100644 index 000000000..76385a3f7 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-setup/assets/module.yaml @@ -0,0 +1,41 @@ +code: devlog +name: "Devlog" +description: "Daily engineering devlog: write entries, summarize history, and consult Clio the Historian." +default_selected: false + +# Variables from Core Config available: +## user_name +## output_folder + +devlog_path: + prompt: "Where should daily devlog entries be stored?" + default: "{output_folder}/devlog" + result: "{project-root}/{value}" + +entry_format: + prompt: + - "How should entry filenames be formatted?" + - "Affects how the summarize skill walks history." + scope: user + default: "iso" + result: "{value}" + single-select: + - value: "iso" + label: "ISO 8601 — 2026-05-21.md" + - value: "weekly" + label: "ISO week — 2026-W21.md (one file per week)" + - value: "monthly" + label: "Year-month — 2026-05.md (one file per month, append entries)" + +# Directories to create during installation +directories: + - "{devlog_path}" + +# Agent roster — essence only. Persona and behavior live in customize.toml. +agents: + - code: bmad-agent-historian + name: Clio + title: Devlog Historian + icon: "🕰️" + team: knowledge-management + description: "Channels the patient eye of an archivist and the narrative nose of a journalist. Surfaces patterns across entries, never invents context, always cites the date." diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-summarize/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-summarize/SKILL.md new file mode 100644 index 000000000..e00f04c1a --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-summarize/SKILL.md @@ -0,0 +1,63 @@ +--- +name: bmad-devlog-summarize +description: Summarize devlog entries across a date range. Use when the user says "summarize devlog", "what happened last week", or "devlog summary ". +--- + +# Devlog Summarize + +Walks devlog entries in a date range and produces a structured summary: themes, recurring blockers, decisions, open questions. + +## EXECUTION + +### Step 1: Resolve config + +Read `{project-root}/_bmad/devlog/config.yaml`. If missing, invoke `/bmad-devlog-setup` first. + +### Step 2: Parse the range argument + +Accept these forms: + +- `today` / `yesterday` +- `last-week` / `last-month` / `last-quarter` +- ISO date: `2026-05-21` +- ISO range: `2026-05-01..2026-05-21` +- ISO week: `2026-W21` +- ISO month: `2026-05` + +If no argument, ask the user. + +### Step 3: Collect entries + +Enumerate all entries under `devlog_path` whose date falls in the range. For `weekly`/`monthly` formats, parse sub-sections by their date heading. + +If zero entries match, report "No entries in ." and stop. + +### Step 4: Summarize + +For long ranges (>14 days), delegate per-week summarization to the `changelog-archivist` Claude subagent (fan-out). For short ranges, summarize inline. + +Produce: + +``` +# Devlog summary — + +## Themes +- _patterns across entries_ + +## Blockers (recurring) +- _what came up more than once_ + +## Decisions +- _commitments visible in entries_ + +## Open questions +- _still unresolved at end of range_ + +## By the numbers +- Entries: +- Days with no entry: +``` + +### Step 5: Optionally save + +Ask: "Save to `/_summaries/.md`?" Write if yes. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-write/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-write/SKILL.md new file mode 100644 index 000000000..00d6b0fa8 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-write/SKILL.md @@ -0,0 +1,50 @@ +--- +name: bmad-devlog-write +description: Write today's devlog entry from the bundled template. Use when the user says "write devlog", "today's entry", or "log this". +--- + +# Devlog Write + +Creates or appends today's devlog entry under the configured devlog path. + +## EXECUTION + +### Step 1: Resolve config + +Read `{project-root}/_bmad/devlog/config.yaml`. Expect: + +- `devlog_path` (absolute path) +- `entry_format` (`iso` | `weekly` | `monthly`) + +If config is missing, invoke `/bmad-devlog-setup` first. + +### Step 2: Determine the entry file + +- `iso` → `/.md` +- `weekly` → `/-W.md` +- `monthly` → `/.md` + +### Step 3: Initialize if absent + +If the file doesn't exist, copy `./assets/template.md` to the target path. Substitute `{{date}}` and `{{author}}` (from `user_name`). For `weekly`/`monthly`, render the `{{date}}` heading from the period value (e.g. `2026-W21` or `2026-05`) instead of reusing the daily date. + +### Step 4: Collect entry content + +Ask the user: + +1. **What did you ship today?** (bullet list) +2. **What blocked you?** (bullet list; "nothing" is valid) +3. **Open questions?** (bullet list; "none" is valid) +4. **One sentence summary.** + +For `weekly`/`monthly` formats, append a dated sub-section (e.g. `## 2026-05-21`) rather than overwriting. + +### Step 5: Write and confirm + +Write the entry, print: + +``` +Wrote / +``` + +If the file existed and you appended, print "Appended to …" instead. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-write/assets/template.md b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-write/assets/template.md new file mode 100644 index 000000000..a7c8d9528 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog/skills/bmad-devlog-write/assets/template.md @@ -0,0 +1,19 @@ +# Devlog — {{date}} + +**Author:** {{author}} + +## Shipped + +- _what landed today_ + +## Blockers + +- _what got in the way (or "nothing")_ + +## Open questions + +- _decisions deferred, things to chase tomorrow (or "none")_ + +## Summary + +_one sentence_ diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/.claude-plugin/marketplace.json b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/.claude-plugin/marketplace.json new file mode 100644 index 000000000..8a605b9a7 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/.claude-plugin/marketplace.json @@ -0,0 +1,14 @@ +{ + "name": "bmad-mini-legacy", + "owner": { "name": "Test Author" }, + "license": "MIT", + "plugins": [ + { + "name": "bmad-mini-legacy", + "source": "./", + "description": "A minimal legacy module for testing the marketplace.json install path.", + "version": "0.2.0", + "skills": ["./src/agents/mlg-agent-one", "./src/workflows/mlg-flow"] + } + ] +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/docs/guide.md b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/docs/guide.md new file mode 100644 index 000000000..58e54c5ed --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/docs/guide.md @@ -0,0 +1,4 @@ +# Mini Legacy Guide + +This file lives under `docs/` and must NOT be copied into the installed module — +it proves that undeclared top-level directories are dropped by buildCopyPlan. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/agents/mlg-agent-one/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/agents/mlg-agent-one/SKILL.md new file mode 100644 index 000000000..4d84a1793 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/agents/mlg-agent-one/SKILL.md @@ -0,0 +1,9 @@ +--- +name: mlg-agent-one +description: A tiny agent that exists to exercise the legacy install path. Use in tests only. +--- + +# Mini Agent + +This agent does nothing useful — it only proves that a legacy skill directory +under `src/agents/` is flattened to `skills/mlg-agent-one/` on install. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/module-help.csv b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/module-help.csv new file mode 100644 index 000000000..a1601dc19 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/module-help.csv @@ -0,0 +1,3 @@ +module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs +mlg,mlg-agent-one,Mini Agent,ma,A tiny agent for legacy-path testing.,mlg-agent-one,,anytime,,,,, +mlg,mlg-flow,Mini Flow,mf,A tiny workflow for legacy-path testing.,mlg-flow,,anytime,,,,{artifacts_path},result.md diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/module.yaml b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/module.yaml new file mode 100644 index 000000000..e78cab181 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/module.yaml @@ -0,0 +1,26 @@ +code: mlg +name: "MLG: Mini Legacy" +description: "A minimal legacy module for testing the marketplace.json install path." +module_version: 0.2.0 +default_selected: false + +# Variables from Core Config available: +## output_folder + +artifacts_path: + prompt: "Where should mini-legacy artifacts be stored?" + default: "{output_folder}/mlg-artifacts" + result: "{project-root}/{value}" + +# Directories to create during installation +directories: + - "{artifacts_path}" + +# Agent roster — essence only. +agents: + - code: mlg-agent-one + name: Mini + title: Mini Agent + icon: "🧩" + team: testing + description: "A tiny agent that exists to exercise the legacy install path." diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/workflows/mlg-flow/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/workflows/mlg-flow/SKILL.md new file mode 100644 index 000000000..26e8b678e --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-mini-legacy/src/workflows/mlg-flow/SKILL.md @@ -0,0 +1,9 @@ +--- +name: mlg-flow +description: A tiny workflow that exists to exercise the legacy install path. Use in tests only. +--- + +# Mini Flow + +Proves that a legacy skill directory under `src/workflows/` is flattened to +`skills/mlg-flow/` on install. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/.claude-plugin/marketplace.json b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/.claude-plugin/marketplace.json new file mode 100644 index 000000000..42b22b2b1 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/.claude-plugin/marketplace.json @@ -0,0 +1,14 @@ +{ + "name": "bmad-reserved-legacy", + "owner": { "name": "Test Author" }, + "license": "MIT", + "plugins": [ + { + "name": "bmad-reserved-legacy", + "source": "./", + "description": "A legacy module using a reserved first-party code (gds) to test the reserved relaxation.", + "version": "0.1.0", + "skills": ["./src/agents/gds-agent-demo"] + } + ] +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/agents/gds-agent-demo/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/agents/gds-agent-demo/SKILL.md new file mode 100644 index 000000000..d2a873855 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/agents/gds-agent-demo/SKILL.md @@ -0,0 +1,8 @@ +--- +name: gds-agent-demo +description: A demo agent for reserved-code legacy testing. Use in tests only. +--- + +# Demo Agent + +Proves the legacy install path accepts reserved first-party codes (gds). diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/module-help.csv b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/module-help.csv new file mode 100644 index 000000000..a0aa840e4 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/module-help.csv @@ -0,0 +1,2 @@ +module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs +gds,gds-agent-demo,Demo Agent,gd,A demo agent for reserved-code legacy testing.,gds-agent-demo,,anytime,,,,, diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/module.yaml b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/module.yaml new file mode 100644 index 000000000..2a104e8c7 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-reserved-legacy/src/module.yaml @@ -0,0 +1,14 @@ +code: gds +name: "GDS: Reserved Legacy" +description: "A legacy module using a reserved first-party code to test the reserved relaxation." +module_version: 0.1.0 +default_selected: false + +# Agent roster — essence only. +agents: + - code: gds-agent-demo + name: Demo + title: Demo Agent + icon: "🎮" + team: testing + description: "Exists only to prove reserved codes install on the legacy path." diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-synth-legacy/.claude-plugin/marketplace.json b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-synth-legacy/.claude-plugin/marketplace.json new file mode 100644 index 000000000..95729be19 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-synth-legacy/.claude-plugin/marketplace.json @@ -0,0 +1,14 @@ +{ + "name": "bmad-synth-legacy", + "owner": { "name": "Test Author" }, + "license": "MIT", + "plugins": [ + { + "name": "synthlg", + "source": "./", + "description": "A legacy module with no module.yaml — exercises the synthesize fallback (strategy 5).", + "version": "0.3.0", + "skills": ["./src/skills/synthlg-do-thing"] + } + ] +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-synth-legacy/src/skills/synthlg-do-thing/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-synth-legacy/src/skills/synthlg-do-thing/SKILL.md new file mode 100644 index 000000000..13a0c92dd --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/legacy/bmad-synth-legacy/src/skills/synthlg-do-thing/SKILL.md @@ -0,0 +1,8 @@ +--- +name: synthlg-do-thing +description: A standalone skill with no module.yaml, so the resolver must synthesize the module definition and help catalog from this frontmatter. +--- + +# Do Thing + +Exercises strategy 5 — the synthesize-from-frontmatter fallback. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/.claude-plugin/plugin.json b/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/.claude-plugin/plugin.json new file mode 100644 index 000000000..026f0a3c0 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "acme-npmtool", + "version": "0.1.0", + "description": "A module that ships a package.json so install exercises npm dependency setup.", + "license": "MIT", + "author": { "name": "Acme Corp" }, + "skills": ["./skills/acme-npmtool"], + "bmad": { + "specVersion": "1.0.0", + "code": "npmtool", + "category": "developer-tools", + "compatibility": { "bmadMethod": ">=6.6.0" } + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/package.json b/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/package.json new file mode 100644 index 000000000..f20b23a43 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/package.json @@ -0,0 +1,7 @@ +{ + "name": "acme-npmtool", + "version": "0.1.0", + "private": true, + "description": "No runtime dependencies — npm install resolves cleanly offline.", + "dependencies": {} +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/skills/acme-npmtool/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/skills/acme-npmtool/SKILL.md new file mode 100644 index 000000000..6f82a3172 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/minimal-npm/acme-npmtool/skills/acme-npmtool/SKILL.md @@ -0,0 +1,9 @@ +--- +name: acme-npmtool +description: A trivial skill whose module ships a package.json to exercise npm dependency install. +--- + +# Acme NPM Tool + +This skill exists only so its module can carry a `package.json`, exercising the +installer's npm dependency setup step. It has no runtime dependencies. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/.claude-plugin/plugin.json b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/.claude-plugin/plugin.json new file mode 100644 index 000000000..220f250cf --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "acme-md-lint", + "version": "0.1.0", + "description": "Lints markdown headings and link rot in BMad projects.", + "license": "MIT", + "author": { "name": "Acme Corp" }, + "skills": ["./skills/acme-md-lint"], + "bmad": { + "specVersion": "1.0.0", + "code": "mdlint", + "category": "developer-tools", + "compatibility": { "bmadMethod": ">=6.6.0" } + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/LICENSE b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/LICENSE new file mode 100644 index 000000000..5c1942827 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Acme Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/README.md b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/README.md new file mode 100644 index 000000000..d1fe24165 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/README.md @@ -0,0 +1,37 @@ +# acme-md-lint + +A minimal BMAD module: one skill that lints markdown files under `_bmad/` for heading-hierarchy mistakes and broken relative links. + +This is the **smallest valid module** that conforms to the BMAD Module Manifest Specification. Use it as a starting template. + +## Install + +``` +bmad-module install acme/acme-md-lint +``` + +Installs to `_bmad/mdlint/`. + +## Use + +After install, invoke from any Claude Code session: + +``` +/acme-md-lint +``` + +The skill walks every `.md` file under `_bmad/` and reports: + +- Heading-level skips (e.g. `##` → `####`) +- Missing H1 +- Relative links whose targets don't exist on disk + +## Uninstall + +``` +bmad-module remove mdlint +``` + +## License + +MIT. See `LICENSE`. diff --git a/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/skills/acme-md-lint/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/skills/acme-md-lint/SKILL.md new file mode 100644 index 000000000..efb274599 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/examples/minimal/acme-md-lint/skills/acme-md-lint/SKILL.md @@ -0,0 +1,61 @@ +--- +name: acme-md-lint +description: Lints all markdown files under _bmad/ for heading hierarchy errors and broken relative links. Use when the user says "lint markdown", "check links", or "audit docs". +--- + +# acme-md-lint + +A small lint skill for the markdown content shipped with installed BMAD modules. + +## CRITICAL RULES + +- DO NOT modify any file — this skill is read-only. +- DO NOT follow external URLs; only check relative paths on disk. +- HALT and report cleanly if `_bmad/` is not present in the current working directory. + +## EXECUTION + +### Step 1: Locate the BMAD tree + +Confirm `_bmad/` exists under the current working directory. If not, report: + +> No `_bmad/` directory found. Run `bmad install` first. + +…and stop. + +### Step 2: Enumerate markdown files + +Walk `_bmad/` recursively. Collect every `.md` file path. Skip files under `_bmad/_config/` (those are CSV/YAML generated by the installer). + +### Step 3: Check heading hierarchy + +For each file: + +1. Parse headings line-by-line (`#`, `##`, `###`, …). +2. Require an H1 as the first heading (warn if absent). +3. Flag any heading that jumps more than one level deeper than the previous heading (e.g. `##` followed by `####`). + +Report each violation with `file:line — message`. + +### Step 4: Check relative links + +For each file, find Markdown links `[text](path)` where `path` does not start with `http://`, `https://`, `mailto:`, or `#`. Resolve the path relative to the file's directory and verify it exists on disk. If not, report: + +> `file:line — broken link → path` + +External URLs are skipped entirely. + +### Step 5: Summarize + +Print: + +``` +Scanned N files. M heading issues, K broken links. +``` + +If `M + K == 0`, end with "All clean." If non-zero, end with "Review the issues above." + +## Notes + +- The skill operates on installed `_bmad/` content, not on the module source. It is intended as a post-install or pre-PR sanity check. +- This is a pedagogical reference. Production lint behavior would warrant a dedicated tool (e.g. `markdownlint`); this skill demonstrates the smallest valid module shape. diff --git a/src/core-skills/bmad-module/tests/fixtures/module-bad-missing-fields/.claude-plugin/plugin.json b/src/core-skills/bmad-module/tests/fixtures/module-bad-missing-fields/.claude-plugin/plugin.json new file mode 100644 index 000000000..d85aeda4e --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/module-bad-missing-fields/.claude-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "fixture-missing-fields", + "version": "0.1.0", + "description": "Negative fixture — missing the entire bmad object, so install-time validation fails." +} diff --git a/src/core-skills/bmad-module/tests/fixtures/module-bad-traversal/.claude-plugin/plugin.json b/src/core-skills/bmad-module/tests/fixtures/module-bad-traversal/.claude-plugin/plugin.json new file mode 100644 index 000000000..54d4e1210 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/module-bad-traversal/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "fixture-bad-traversal", + "version": "0.1.0", + "description": "Negative fixture — declares a skill path that escapes the module root via '..'.", + "skills": ["../escape"], + "bmad": { + "specVersion": "1.0.0", + "code": "badpath", + "compatibility": { "bmadMethod": ">=6.6.0" } + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/module-bad-traversal/skills/skill-a/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/module-bad-traversal/skills/skill-a/SKILL.md new file mode 100644 index 000000000..c95441743 --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/module-bad-traversal/skills/skill-a/SKILL.md @@ -0,0 +1,6 @@ +--- +name: skill-a +description: Stub skill that exists so the path-traversal check is exercised against the manifest's bogus entry, not a missing file. +--- + +Stub. diff --git a/src/core-skills/bmad-module/tests/fixtures/module-reserved-code/.claude-plugin/plugin.json b/src/core-skills/bmad-module/tests/fixtures/module-reserved-code/.claude-plugin/plugin.json new file mode 100644 index 000000000..6fe39cb2c --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/module-reserved-code/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "fixture-reserved-code", + "version": "0.1.0", + "description": "Negative fixture — declares bmad.code that collides with the reserved BMAD module 'bmm'.", + "skills": ["./skills/skill-a"], + "bmad": { + "specVersion": "1.0.0", + "code": "bmm", + "compatibility": { "bmadMethod": ">=6.6.0" } + } +} diff --git a/src/core-skills/bmad-module/tests/fixtures/module-reserved-code/skills/skill-a/SKILL.md b/src/core-skills/bmad-module/tests/fixtures/module-reserved-code/skills/skill-a/SKILL.md new file mode 100644 index 000000000..deb92ad8a --- /dev/null +++ b/src/core-skills/bmad-module/tests/fixtures/module-reserved-code/skills/skill-a/SKILL.md @@ -0,0 +1,6 @@ +--- +name: skill-a +description: Stub skill for the reserved-code negative fixture. Never installed. +--- + +Stub. diff --git a/src/core-skills/bmad-module/tests/integration.test.sh b/src/core-skills/bmad-module/tests/integration.test.sh new file mode 100755 index 000000000..6d818b3c6 --- /dev/null +++ b/src/core-skills/bmad-module/tests/integration.test.sh @@ -0,0 +1,391 @@ +#!/usr/bin/env bash +# integration.test.sh — end-to-end smoke test for the bmad-module skill. +# +# Hermetic: fabricates a minimal _bmad/_config/manifest.yaml skeleton in a +# tmp dir and exercises every verb against the vendored reference modules +# (tests/fixtures/examples/) and negative fixtures. Does NOT require +# BMAD-METHOD's installer; installer integration is verified separately. +# +# Run from anywhere: +# bash src/core-skills/bmad-module/tests/integration.test.sh +# +# Exit 0 on full pass; non-zero on first failed assertion (set -e). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +MODULE_JS="${SKILL_DIR}/scripts/bmad-module.mjs" +# Reference modules are vendored under tests/fixtures/examples/ so the suite is +# self-contained — the fixtures travel with the test, no external checkout. +EXAMPLES="${SCRIPT_DIR}/fixtures/examples" +FIXTURES="${SCRIPT_DIR}/fixtures" + +WORKDIR="$(mktemp -d)" +trap 'rm -rf "${WORKDIR}"' EXIT +cd "${WORKDIR}" + +pass=0 +fail=0 + +note() { printf '\n\033[1m── %s\033[0m\n' "$*"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; pass=$((pass+1)); } +ko() { printf ' \033[31m✗\033[0m %s\n' "$*"; fail=$((fail+1)); } + +# Wrapper that captures stdout/stderr/exit code into globals. +run() { + local stderr_file + stderr_file="$(mktemp)" + set +e + STDOUT="$(node "${MODULE_JS}" "$@" 2>"${stderr_file}")" + EXIT=$? + STDERR="$(cat "${stderr_file}")" + rm -f "${stderr_file}" + set -e +} + +assert_exit() { + local want=$1; local label=$2 + if [[ "${EXIT}" -eq "${want}" ]]; then ok "${label} → exit ${want}" + else ko "${label} → expected exit ${want}, got ${EXIT}. stderr: ${STDERR}" + fi +} + +assert_path_exists() { + if [[ -e "$1" ]]; then ok "exists: $1" + else ko "missing: $1" + fi +} + +assert_path_absent() { + if [[ ! -e "$1" ]]; then ok "absent: $1" + else ko "should be gone: $1" + fi +} + +assert_grep() { + local pat=$1; local file=$2 + if grep -q -E "$pat" "$file"; then ok "grep '$pat' in $(basename "$file")" + else ko "grep '$pat' NOT in $(basename "$file"); contents:\n$(cat "$file")" + fi +} + +# ─── Setup: fabricate _bmad/_config/manifest.yaml ──────────────────────────── + +note "setup: minimal _bmad/ skeleton" +mkdir -p _bmad/_config +mkdir -p _bmad/core _bmad/bmm +cat > _bmad/_config/manifest.yaml <<'YAML' +installation: + version: "v6.7.1" + installDate: "2026-05-21T00:00:00.000Z" + lastUpdated: "2026-05-21T00:00:00.000Z" +modules: + - name: core + version: "v6.7.1" + installDate: "2026-05-21T00:00:00.000Z" + lastUpdated: "2026-05-21T00:00:00.000Z" + source: built-in + npmPackage: null + repoUrl: null + - name: bmm + version: "v6.7.1" + installDate: "2026-05-21T00:00:00.000Z" + lastUpdated: "2026-05-21T00:00:00.000Z" + source: built-in + npmPackage: null + repoUrl: null +ides: [] +YAML +printf 'canonicalId,name,description,module,path\n' > _bmad/_config/skill-manifest.csv +printf 'type,name,module,path,hash\n' > _bmad/_config/files-manifest.csv +# Central config as `bmad install` would leave it: [core] supplies output_folder +# so module defaults that reference {output_folder} resolve during config-gen. +cat > _bmad/config.toml <<'TOML' +# Installer-managed. Regenerated on install. + +[core] +user_name = "Tester" +output_folder = "{project-root}/_bmad-output" +TOML +printf '# Installer-managed.\n\n[core]\ncommunication_language = "English"\n' > _bmad/config.user.toml +# Core ships a canonical module-help.csv so the merged catalog has a baseline row. +printf 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs\n,bmad-help,Help,h,Show the BMAD help catalog,bmad-help,,,,,,,\n' > _bmad/core/module-help.csv +ok "skeleton seeded at ${WORKDIR}/_bmad/" + +# ─── 1. list (empty) ───────────────────────────────────────────────────────── +note "list (no modules)" +run list +assert_exit 0 "list empty" +if [[ "${STDOUT}" == *"no modules installed"* ]]; then ok "stdout reports empty" +else ko "expected 'no modules installed' in stdout: ${STDOUT}"; fi + +# ─── 1a. usage errors: unknown flag / invalid channel reject early ─────────── +note "unknown flag and invalid channel → exit 2" +run install "${EXAMPLES}/minimal/acme-md-lint" --bogus +assert_exit 2 "unknown flag rejected" +run install "${EXAMPLES}/minimal/acme-md-lint" --channel stabl --dry-run +assert_exit 2 "invalid channel rejected" + +# ─── 2. dry-run install of minimal module ──────────────────────────────────── +note "install --dry-run examples/minimal/acme-md-lint" +run install "${EXAMPLES}/minimal/acme-md-lint" --dry-run +assert_exit 0 "dry-run install" +if [[ "${STDOUT}" == *"dry-run"* ]]; then ok "stdout mentions dry-run" +else ko "expected 'dry-run' in stdout: ${STDOUT}"; fi +assert_path_absent "_bmad/mdlint" + +# ─── 3. real install of minimal module ─────────────────────────────────────── +note "install examples/minimal/acme-md-lint" +run install "${EXAMPLES}/minimal/acme-md-lint" +assert_exit 0 "install minimal" +assert_path_exists "_bmad/mdlint/.claude-plugin/plugin.json" +assert_path_exists "_bmad/mdlint/skills/acme-md-lint/SKILL.md" +assert_grep '^ - name: mdlint' "_bmad/_config/manifest.yaml" +assert_grep 'source: community' "_bmad/_config/manifest.yaml" +assert_grep '"acme-md-lint","acme-md-lint"' "_bmad/_config/skill-manifest.csv" +assert_grep ',"mdlint",' "_bmad/_config/files-manifest.csv" + +# ─── 4. list (one module) ──────────────────────────────────────────────────── +note "list (after minimal install)" +run list +assert_exit 0 "list one" +if [[ "${STDOUT}" == *"mdlint"* ]]; then ok "stdout includes mdlint" +else ko "expected 'mdlint' in stdout: ${STDOUT}"; fi + +run list --json +assert_exit 0 "list --json" +if [[ "${STDOUT}" == *"\"name\": \"mdlint\""* ]]; then ok "json includes mdlint name" +else ko "expected mdlint in JSON: ${STDOUT}"; fi + +# ─── 5. idempotent re-install ──────────────────────────────────────────────── +note "install acme-md-lint again (idempotent / collision)" +# Local sources have no sha, so the no-op fast path can't trigger — we hit +# the collision branch instead. Asserting exit 30 documents the v1 behavior: +# local re-installs require `update`. +run install "${EXAMPLES}/minimal/acme-md-lint" +assert_exit 30 "re-install collision" + +# ─── 6. negative: reserved-code fixture ────────────────────────────────────── +note "install module-reserved-code → exit 21" +run install "${FIXTURES}/module-reserved-code" +assert_exit 21 "reserved code" + +# ─── 7. negative: bad-traversal fixture ────────────────────────────────────── +note "install module-bad-traversal → exit 70" +run install "${FIXTURES}/module-bad-traversal" +assert_exit 70 "path traversal" + +# ─── 8. negative: missing-fields fixture ───────────────────────────────────── +note "install module-bad-missing-fields → exit 20" +run install "${FIXTURES}/module-bad-missing-fields" +assert_exit 20 "missing required fields" + +# ─── 9. comprehensive module install ───────────────────────────────────────── +note "install examples/comprehensive/acme-devlog (with --set override)" +run install "${EXAMPLES}/comprehensive/acme-devlog" --set devlog.devlog_path='{output_folder}/journal' +assert_exit 0 "install comprehensive" +assert_path_exists "_bmad/devlog/skills/bmad-devlog-write/SKILL.md" +assert_path_exists "_bmad/devlog/skills/bmad-devlog-setup/SKILL.md" +assert_path_exists "_bmad/devlog/agents/changelog-archivist.md" +# hooks/mcpServers are flattened to canonical root slots (see rewriteManifestPaths) +assert_path_exists "_bmad/devlog/hooks.json" +assert_path_exists "_bmad/devlog/.mcp.json" +# moduleDefinition / moduleHelpCsv are also flattened to the module root even +# though they live inside the setup skill's assets/ dir. +assert_path_exists "_bmad/devlog/module.yaml" +assert_path_exists "_bmad/devlog/module-help.csv" +# install.ignore excludes docs/ and tests/ and README.md / CHANGELOG.md +assert_path_absent "_bmad/devlog/docs" +assert_path_absent "_bmad/devlog/README.md" +assert_path_absent "_bmad/devlog/CHANGELOG.md" +if [[ "${STDOUT}" == *"hooks"* ]]; then ok "warns about hooks not auto-activated" +else ko "expected hooks warning in stdout: ${STDOUT}"; fi + +# ─── 9a. parity: central config + agent roster (gap #3) ────────────────────── +note "config generation + agent roster" +assert_grep '^\[modules\.devlog]' "_bmad/config.toml" +# --set override resolves {output_folder} from [core] and applies the result template +assert_grep 'devlog_path = "\{project-root}/_bmad-output/journal"' "_bmad/config.toml" +assert_grep '^\[agents\.bmad-agent-historian]' "_bmad/config.toml" +assert_grep 'module = "devlog"' "_bmad/config.toml" +# [core] is preserved untouched +assert_grep '^user_name = "Tester"' "_bmad/config.toml" +# user-scoped answer lands in config.user.toml, not config.toml +assert_grep '^\[modules\.devlog]' "_bmad/config.user.toml" +assert_grep 'entry_format = "iso"' "_bmad/config.user.toml" + +# ─── 9b. parity: module working directories (gap #2) ───────────────────────── +note "module directory creation" +assert_path_exists "_bmad-output/journal" + +# ─── 9c. parity: merged help catalog (gap #1) ──────────────────────────────── +note "bmad-help.csv merge" +assert_path_exists "_bmad/_config/bmad-help.csv" +if head -1 _bmad/_config/bmad-help.csv | grep -q '^module,skill,display-name,'; then + ok "bmad-help.csv has canonical header" +else ko "bmad-help.csv header wrong"; fi +assert_grep '^devlog,bmad-devlog-write,' "_bmad/_config/bmad-help.csv" +assert_grep '^devlog,bmad-agent-historian,' "_bmad/_config/bmad-help.csv" +# the core baseline row is still present +assert_grep ',bmad-help,Help,' "_bmad/_config/bmad-help.csv" + +# ─── 9d. legacy module (marketplace.json + module.yaml, strategy 1) ────────── +note "install examples/legacy/bmad-mini-legacy (legacy marketplace.json)" +run install "${EXAMPLES}/legacy/bmad-mini-legacy" +assert_exit 0 "install legacy mini" +if [[ "${STDOUT}" == *"resolved legacy module mlg"* ]]; then ok "stdout reports legacy resolution" +else ko "expected 'resolved legacy module mlg' in stdout: ${STDOUT}"; fi +# Synthetic plugin.json is staged; marketplace.json is preserved verbatim. +assert_path_exists "_bmad/mlg/.claude-plugin/plugin.json" +assert_path_exists "_bmad/mlg/.claude-plugin/marketplace.json" +# Skills under src/agents and src/workflows are flattened to skills/. +assert_path_exists "_bmad/mlg/skills/mlg-agent-one/SKILL.md" +assert_path_exists "_bmad/mlg/skills/mlg-flow/SKILL.md" +# module.yaml / module-help.csv flattened from src/ to the module root. +assert_path_exists "_bmad/mlg/module.yaml" +assert_path_exists "_bmad/mlg/module-help.csv" +# Undeclared trees are dropped — src/ wrapper and docs/ must not leak. +assert_path_absent "_bmad/mlg/src" +assert_path_absent "_bmad/mlg/docs" +# The staged manifest carries canonical rewritten paths. +assert_grep '"\./skills/mlg-agent-one"' "_bmad/mlg/.claude-plugin/plugin.json" +assert_grep '"\./module\.yaml"' "_bmad/mlg/.claude-plugin/plugin.json" +# Registered and merged like any community module. The manifest `name` is the +# kebab plugin name (module.yaml#name "MLG: …" would fail NAME_REGEX). +assert_grep '^ - name: mlg' "_bmad/_config/manifest.yaml" +assert_grep 'source: community' "_bmad/_config/manifest.yaml" +assert_grep '^mlg,' "_bmad/_config/bmad-help.csv" + +# ─── 9e. legacy with a reserved first-party code (gds) ─────────────────────── +note "install examples/legacy/bmad-reserved-legacy (reserved code on legacy path)" +run install "${EXAMPLES}/legacy/bmad-reserved-legacy" +assert_exit 0 "install legacy reserved code" +assert_path_exists "_bmad/gds/module.yaml" +assert_path_exists "_bmad/gds/skills/gds-agent-demo/SKILL.md" + +# ─── 9f. legacy synthesize fallback (strategy 5, no module.yaml) ───────────── +note "install examples/legacy/bmad-synth-legacy (synthesized module.yaml)" +run install "${EXAMPLES}/legacy/bmad-synth-legacy" +assert_exit 0 "install legacy synth fallback" +# module.yaml + module-help.csv are synthesized and written into the module root. +assert_path_exists "_bmad/synthlg/module.yaml" +assert_path_exists "_bmad/synthlg/module-help.csv" +assert_path_exists "_bmad/synthlg/skills/synthlg-do-thing/SKILL.md" +assert_grep '^code: synthlg' "_bmad/synthlg/module.yaml" +assert_grep '^module,skill,display-name,' "_bmad/synthlg/module-help.csv" + +# ─── 10. remove minimal (no purge), preserve custom ───────────────────────── +note "create _bmad/custom/mdlint to test preservation, then remove" +mkdir -p _bmad/custom/mdlint +echo "user override" > _bmad/custom/mdlint/override.md +run remove mdlint +assert_exit 0 "remove mdlint" +assert_path_absent "_bmad/mdlint" +assert_path_exists "_bmad/custom/mdlint/override.md" +if [[ "${STDOUT}" == *"preserved"* ]]; then ok "stdout mentions preserved customs" +else ko "expected 'preserved' in stdout: ${STDOUT}"; fi +# manifest rows for mdlint should be gone +if grep -q ',"mdlint",' _bmad/_config/files-manifest.csv; then + ko "mdlint rows still in files-manifest.csv" +else ok "files-manifest.csv pruned"; fi +if grep -q '"acme-md-lint"' _bmad/_config/skill-manifest.csv; then + ko "acme-md-lint row still in skill-manifest.csv" +else ok "skill-manifest.csv pruned"; fi + +# ─── 11. remove --purge ────────────────────────────────────────────────────── +note "remove devlog --purge" +mkdir -p _bmad/custom/devlog +echo "user override" > _bmad/custom/devlog/override.md +run remove devlog --purge +assert_exit 0 "remove --purge" +assert_path_absent "_bmad/devlog" +assert_path_absent "_bmad/custom/devlog" +# config blocks and help rows for devlog are stripped on removal +if grep -q '\[modules\.devlog]' _bmad/config.toml; then + ko "[modules.devlog] still in config.toml" +else ok "config.toml [modules.devlog] stripped"; fi +if grep -q '\[agents\.bmad-agent-historian]' _bmad/config.toml; then + ko "[agents.bmad-agent-historian] still in config.toml" +else ok "config.toml agent block stripped"; fi +if grep -q '\[modules\.devlog]' _bmad/config.user.toml; then + ko "[modules.devlog] still in config.user.toml" +else ok "config.user.toml [modules.devlog] stripped"; fi +if grep -q '^devlog,' _bmad/_config/bmad-help.csv; then + ko "devlog rows still in bmad-help.csv" +else ok "bmad-help.csv devlog rows removed"; fi +# [core] survives the removal +assert_grep '^user_name = "Tester"' "_bmad/config.toml" + +# ─── 12. remove unknown ────────────────────────────────────────────────────── +note "remove unknown code" +run remove nope +assert_exit 90 "remove unknown" + +# ─── 13. IDE distribution into the user's chosen coding assistants ─────────── +# Uses a SEPARATE project whose manifest lists two IDEs, so install/remove must +# push skills to (and prune them from) those IDE dirs via the vendored ide-sync +# bundle. Fully offline — no npx, no network, no node_modules. +note "IDE distribution: install/remove sync to configured assistants" +IDEPROJ="${WORKDIR}/ideproj" +mkdir -p "${IDEPROJ}/_bmad/_config" +cat > "${IDEPROJ}/_bmad/_config/manifest.yaml" <<'YAML' +installation: + version: "v6.7.1" + installDate: "2026-05-21T00:00:00.000Z" + lastUpdated: "2026-05-21T00:00:00.000Z" +modules: [] +ides: + - claude-code + - cursor +YAML +printf 'canonicalId,name,description,module,path\n' > "${IDEPROJ}/_bmad/_config/skill-manifest.csv" +printf 'type,name,module,path,hash\n' > "${IDEPROJ}/_bmad/_config/files-manifest.csv" + +run install "${EXAMPLES}/minimal/acme-md-lint" --project-dir "${IDEPROJ}" +assert_exit 0 "install into IDE project" +assert_path_exists "${IDEPROJ}/.claude/skills/acme-md-lint/SKILL.md" +assert_path_exists "${IDEPROJ}/.agents/skills/acme-md-lint/SKILL.md" +if [[ "${STDOUT}" == *"claude-code"* ]]; then ok "stdout reports claude-code distribution" +else ko "expected claude-code in stdout: ${STDOUT}"; fi +# Canonical end-state: skill source dirs removed from _bmad/ after distribution. +if find "${IDEPROJ}/_bmad" -name SKILL.md | grep -q .; then + ko "SKILL.md still under _bmad after distribution" +else + ok "_bmad skill dirs cleaned after distribution" +fi + +run remove mdlint --project-dir "${IDEPROJ}" +assert_exit 0 "remove from IDE project" +assert_path_absent "${IDEPROJ}/.claude/skills/acme-md-lint" +assert_path_absent "${IDEPROJ}/.agents/skills/acme-md-lint" + +# ─── 14. npm dependency install (gap #4) ───────────────────────────────────── +# A module shipping package.json. package.json/package-lock.json are copied to +# the module root; if npm is available, deps are installed in place. The fixture +# has no dependencies, so npm resolves offline. Guarded on npm availability so +# CI sandboxes without npm still pass. +note "npm fixture: package.json copied + deps installed in place" +run install "${EXAMPLES}/minimal-npm/acme-npmtool" +assert_exit 0 "install npm fixture" +assert_path_exists "_bmad/npmtool/package.json" +if command -v npm >/dev/null 2>&1; then + if [[ "${STDOUT}" == *"installed npm dependencies for npmtool"* ]]; then + ok "npm dependencies installed" + else ko "expected npm install confirmation in stdout: ${STDOUT}"; fi + # The fixture has zero deps, so npm writes package-lock.json (not node_modules); + # its presence proves npm actually ran inside the installed module dir. + assert_path_exists "_bmad/npmtool/package-lock.json" +else + ok "npm not on PATH — skipping dependency-install assertion" +fi + +# ─── Summary ───────────────────────────────────────────────────────────────── +echo +echo "──────────────────────────────────────────────────────────────────────" +printf ' %d pass · %d fail\n' "${pass}" "${fail}" +if [[ "${fail}" -gt 0 ]]; then + echo " FAIL" + exit 1 +fi +echo " OK" diff --git a/src/core-skills/module-help.csv b/src/core-skills/module-help.csv index ea4abb043..71e94db3d 100644 --- a/src/core-skills/module-help.csv +++ b/src/core-skills/module-help.csv @@ -11,3 +11,4 @@ Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assu Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,, Core,bmad-spec,Spec,SP,"Use to distill any intent input (brief, PRD, transcript, brain dump, design folder, mixed multi-source) into a succinct, no-fluff SPEC.md contract + companions that downstream work derives from. Locks the WHAT before the HOW. Works for software, game design, research, editorial, policy, business, anything intent-bearing. Validation mode also available.",,[path],anytime,,,false,{output_folder}/specs/spec-{slug},SPEC.md + companion files Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files +Core,bmad-module,Manage Modules,MM,"Install, update, remove, or list community BMAD modules (standalone GitHub repos installed by URL or local path). Use when the user says install/update/remove/uninstall/list module.",,[verb] [source],anytime,,,false,{project-root}/_bmad, diff --git a/test/test-bmad-module-source.mjs b/test/test-bmad-module-source.mjs new file mode 100644 index 000000000..018f5c99e --- /dev/null +++ b/test/test-bmad-module-source.mjs @@ -0,0 +1,157 @@ +/** + * bmad-module skill — parseSource / semver-lite / channel-resolver unit tests. + * + * Covers the skill's pure (no-network, no-filesystem) install plumbing so the + * @ref / deep-path-URL / subdir parsing, the registry-free semver math, and the + * channel resolver stay at parity with the canonical installer + * (tools/installer/modules/custom-module-manager.js + channel-resolver.js, whose + * own behavior is pinned by test/test-parse-source-urls.js and + * test/test-installer-channels.js). + * + * Usage: node test/test-bmad-module-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 { parseGitHubRepo, normalizeStableTag } from '../src/core-skills/bmad-module/scripts/lib/channel-resolver.mjs'; + +const colors = { reset: '', green: '', red: '', cyan: '', dim: '' }; +let passed = 0; +let failed = 0; + +function assert(cond, name, detail = '') { + if (cond) { + console.log(`${colors.green}✓${colors.reset} ${name}`); + passed++; + } else { + console.log(`${colors.red}✗${colors.reset} ${name}`); + if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`); + failed++; + } +} +const eq = (got, want, name) => + assert(JSON.stringify(got) === JSON.stringify(want), name, `got ${JSON.stringify(got)} want ${JSON.stringify(want)}`); +function throws(fn, name) { + try { + fn(); + assert(false, name, 'expected a throw'); + } catch { + assert(true, name); + } +} + +// ─── parseSource ──────────────────────────────────────────────────────────── +console.log(`\n${colors.cyan}parseSource${colors.reset}\n`); + +{ + const r = parseSource('owner/repo'); + eq( + [r.kind, r.url, r.ref, r.subdir, r.cacheKey], + ['git', 'https://github.com/owner/repo', null, null, 'github.com/owner/repo'], + 'owner/repo shorthand → GitHub HTTPS', + ); +} +{ + const r = parseSource('acme/devlog@v1.2.3'); + eq([r.url, r.ref], ['https://github.com/acme/devlog', 'v1.2.3'], 'owner/repo@ref strips suffix'); +} +{ + const r = parseSource('https://github.com/owner/repo/tree/main/pkg/foo'); + eq([r.url, r.ref, r.subdir], ['https://github.com/owner/repo', 'main', 'pkg/foo'], 'GitHub /tree//'); +} +{ + const r = parseSource('https://github.com/owner/repo/tree/main'); + eq([r.url, r.ref, r.subdir], ['https://github.com/owner/repo', 'main', null], 'GitHub /tree/ without subdir strips ref'); +} +{ + const r = parseSource('https://github.com/owner/repo/blob/v2.0.0/src'); + eq([r.url, r.ref, r.subdir], ['https://github.com/owner/repo', 'v2.0.0', 'src'], 'GitHub /blob//'); +} +{ + const r = parseSource('https://gitlab.com/group/subgroup/repo/-/tree/main/src/module'); + eq([r.url, r.ref, r.subdir], ['https://gitlab.com/group/subgroup/repo', 'main', 'src/module'], 'GitLab nested-group /-/tree'); +} +{ + const r = parseSource('https://gitea.example.com/owner/repo/src/branch/main'); + eq([r.url, r.subdir], ['https://gitea.example.com/owner/repo', null], 'Gitea /src/branch/ without subdir strips ref'); +} +{ + const r = parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module?path=/src/skills'); + eq( + [r.url, r.subdir, r.cacheKey], + ['https://dev.azure.com/myorg/MyProject/_git/my-module', 'src/skills', 'dev.azure.com/myorg/MyProject/_git/my-module'], + 'Azure DevOps ?path= + full-path cacheKey', + ); +} +{ + const r = parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module.git'); + eq(r.url, 'https://dev.azure.com/myorg/MyProject/_git/my-module', 'trailing .git stripped from cloneUrl'); +} +{ + const r = parseSource('git@github.com:owner/repo.git'); + eq( + [r.kind, r.url, r.cacheKey, r.displayName], + ['git', 'git@github.com:owner/repo.git', 'github.com/owner/repo', 'owner/repo'], + 'SSH URL preserved, @ not consumed', + ); +} +{ + const r = parseSource('https://git.example.com/owner/my.repo.name'); + eq([r.url, r.displayName], ['https://git.example.com/owner/my.repo.name', 'owner/my.repo.name'], 'dotted repo name preserved'); +} +{ + const r = parseSource('https://git.example.com/myorg/MyProject/_git/my-module'); + eq( + [r.cacheKey, r.displayName], + ['git.example.com/myorg/MyProject/_git/my-module', '_git/my-module'], + 'nested path cacheKey + last-two-segment displayName', + ); +} +throws(() => parseSource('./local@v1'), 'local path + @ref throws'); +throws(() => parseSource(' '), 'empty source throws'); +throws(() => parseSource('not a source'), 'garbage source throws'); + +// ─── semver-lite ────────────────────────────────────────────────────────────── +console.log(`\n${colors.cyan}semver-lite${colors.reset}\n`); + +eq(valid('v1.2.3'), '1.2.3', 'valid() strips leading v'); +eq(valid('1.2'), null, 'valid() rejects partial'); +eq(prerelease('1.0.0-rc.1'), ['rc', 1], 'prerelease() parses identifiers'); +eq(prerelease('1.0.0'), null, 'prerelease() null for release'); +eq(compare('1.0.0-alpha', '1.0.0'), -1, 'prerelease < release'); +eq(compare('1.0.0-alpha.1', '1.0.0-alpha.beta'), -1, 'numeric id < alphanumeric id'); +eq(compare('1.2.0', '1.10.0'), -1, 'numeric (not lexical) field compare'); +eq(compare('2.0.0', '2.0.0'), 0, 'equal versions'); +eq(compare('bad', '1.0.0'), null, 'compare() null on invalid'); +{ + const tags = [ + { tag: 'v1.0.0', version: '1.0.0' }, + { tag: 'v1.7.0', version: '1.7.0' }, + { tag: 'v1.2.0', version: '1.2.0' }, + ]; + tags.sort((a, b) => rcompare(a.version, b.version)); + eq( + tags.map((t) => t.tag), + ['v1.7.0', 'v1.2.0', 'v1.0.0'], + 'rcompare() sorts newest-first', + ); +} +assert(validRange('>=6.0.0') !== null, 'validRange() accepts a real range'); + +// ─── channel-resolver (pure helpers) ────────────────────────────────────────── +console.log(`\n${colors.cyan}channel-resolver${colors.reset}\n`); + +eq(parseGitHubRepo('https://github.com/o/r/tree/main'), { owner: 'o', repo: 'r' }, 'parseGitHubRepo from deep URL'); +eq(parseGitHubRepo('git@github.com:o/r'), { owner: 'o', repo: 'r' }, 'parseGitHubRepo from SSH'); +eq(parseGitHubRepo('https://gitlab.com/o/r'), null, 'parseGitHubRepo null for non-GitHub'); +eq(parseGitHubRepo('https://github.com/o/r.git'), { owner: 'o', repo: 'r' }, 'parseGitHubRepo strips .git'); +eq(parseGitHubRepo('https://github.com/o/r.git/'), { owner: 'o', repo: 'r' }, 'parseGitHubRepo strips .git before trailing slash'); +eq( + [normalizeStableTag('v1.7.0'), normalizeStableTag('1.0.0-rc.1'), normalizeStableTag('nope')], + ['1.7.0', null, null], + 'normalizeStableTag excludes prereleases/invalid', +); + +// ─── Summary ────────────────────────────────────────────────────────────────── +console.log(`\n${colors.cyan}Results: ${passed} passed, ${failed} failed${colors.reset}\n`); +process.exit(failed > 0 ? 1 : 0); diff --git a/test/test-ide-sync.js b/test/test-ide-sync.js new file mode 100644 index 000000000..2f9584652 --- /dev/null +++ b/test/test-ide-sync.js @@ -0,0 +1,123 @@ +// test-ide-sync — behavioral drift guard for the IDE-distribution path. +// +// The bmad-module skill runs a self-contained esbuild bundle +// (src/core-skills/bmad-module/scripts/lib/vendor/ide-sync.mjs) built FROM the +// real engine (tools/installer/ide/* via core/ide-sync.js). vendor:check already +// byte-verifies the bundle matches its source. This test verifies the two +// delivery vehicles behave IDENTICALLY at runtime: +// 1. `bmad ide-sync` — the engine, run directly from the package +// 2. `vendor/ide-sync.mjs` — the shipped, dependency-free bundle +// Both must produce the same IDE skill trees for the same project, including +// `--prune`. If the engine changes without rebuilding the bundle, the outputs +// diverge and this fails (complementing the byte-level vendor:check). + +const assert = require('node:assert'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const repoRoot = path.resolve(__dirname, '..'); +const CLI = path.join(repoRoot, 'tools', 'installer', 'bmad-cli.js'); +const BUNDLE = path.join(repoRoot, 'src', 'core-skills', 'bmad-module', 'scripts', 'lib', 'vendor', 'ide-sync.mjs'); + +let passed = 0; +let failed = 0; +function check(label, fn) { + try { + fn(); + passed++; + process.stdout.write(` ✓ ${label}\n`); + } catch (error) { + failed++; + process.stdout.write(` ✗ ${label}\n ${error.message}\n`); + } +} + +// Build a fresh project with two skills recorded for two IDEs. +function makeProject(skillIds) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bmad-ide-sync-')); + fs.mkdirSync(path.join(dir, '_bmad', '_config'), { recursive: true }); + fs.writeFileSync( + path.join(dir, '_bmad', '_config', 'manifest.yaml'), + 'installation:\n version: "0.0.0"\nmodules:\n - name: demo\n source: community\nides:\n - claude-code\n - cursor\n', + ); + let csv = 'canonicalId,name,description,module,path\n'; + for (const id of skillIds) { + const sd = path.join(dir, '_bmad', 'demo', 'skills', id); + fs.mkdirSync(sd, { recursive: true }); + fs.writeFileSync(path.join(sd, 'SKILL.md'), `---\nname: ${id}\ndescription: ${id} demo\n---\nbody ${id}\n`); + csv += `"${id}","${id}","${id} demo","demo","_bmad/demo/skills/${id}/SKILL.md"\n`; + } + fs.writeFileSync(path.join(dir, '_bmad', '_config', 'skill-manifest.csv'), csv); + return dir; +} + +// Snapshot the IDE skill trees (relative path -> file contents) for comparison. +function snapshotIdeDirs(projectDir) { + const snap = {}; + for (const rel of ['.claude/skills', '.agents/skills']) { + const base = path.join(projectDir, rel); + if (!fs.existsSync(base)) continue; + const walk = (d) => { + for (const entry of fs.readdirSync(d, { withFileTypes: true })) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) walk(full); + else snap[path.relative(projectDir, full)] = fs.readFileSync(full, 'utf8'); + } + }; + walk(base); + } + return snap; +} + +function runEngine(projectDir, prune) { + const args = [CLI, 'ide-sync', '-d', projectDir]; + if (prune) args.push('--prune', prune); + const r = spawnSync(process.execPath, args, { encoding: 'utf8' }); + assert.strictEqual(r.status, 0, `engine ide-sync exited ${r.status}: ${r.stderr}`); +} + +function runBundle(projectDir, prune) { + const args = [BUNDLE, '-d', projectDir]; + if (prune) args.push('--prune', prune); + const r = spawnSync(process.execPath, args, { encoding: 'utf8' }); + assert.strictEqual(r.status, 0, `bundle ide-sync exited ${r.status}: ${r.stderr}`); +} + +process.stdout.write('IDE-sync engine/bundle parity\n'); + +check('bundle exists (run `npm run vendor:build` if missing)', () => { + assert.ok(fs.existsSync(BUNDLE), `missing ${BUNDLE}`); +}); + +const cleanup = []; +try { + // Distribute: engine vs bundle must yield identical IDE trees. + check('distribute: engine == bundle', () => { + const a = makeProject(['sk-a', 'sk-b']); + const b = makeProject(['sk-a', 'sk-b']); + cleanup.push(a, b); + runEngine(a); + runBundle(b); + assert.deepStrictEqual(snapshotIdeDirs(a), snapshotIdeDirs(b)); + assert.ok(fs.existsSync(path.join(a, '.claude', 'skills', 'sk-a', 'SKILL.md')), 'engine did not distribute'); + }); + + // Prune one skill (the remove path): engine vs bundle must agree. + check('prune: engine == bundle and removes pruned skill', () => { + const a = makeProject(['sk-a']); // sk-b dropped from manifest + const b = makeProject(['sk-a']); + cleanup.push(a, b); + runEngine(a, 'sk-b'); + runBundle(b, 'sk-b'); + assert.deepStrictEqual(snapshotIdeDirs(a), snapshotIdeDirs(b)); + assert.ok(!fs.existsSync(path.join(a, '.claude', 'skills', 'sk-b')), 'pruned skill should be gone'); + assert.ok(fs.existsSync(path.join(a, '.claude', 'skills', 'sk-a')), 'kept skill should remain'); + }); +} finally { + for (const d of cleanup) fs.rmSync(d, { recursive: true, force: true }); +} + +process.stdout.write(`\n ${passed} pass · ${failed} fail\n`); +process.exit(failed > 0 ? 1 : 0); diff --git a/test/test-installation-components.js b/test/test-installation-components.js index f511b4376..103416987 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -3575,6 +3575,217 @@ async function runTests() { console.log(''); + // ============================================================ + // Test Suite 48: New module system (plugin.json#bmad) in the installer + // ============================================================ + console.log(`${colors.yellow}Test Suite 48: New module system + legacy detection in custom-module install${colors.reset}\n`); + try { + const { PluginResolver } = require('../tools/installer/modules/plugin-resolver'); + const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager'); + const { readPluginManifest } = require('../tools/installer/modules/bmad-module-lib'); + const acmeDevlog = path.resolve(__dirname, '../src/core-skills/bmad-module/tests/fixtures/examples/comprehensive/acme-devlog'); + + // ---- readPluginManifest: detects bmad block, ignores non-bmad plugin.json ---- + { + const m = await readPluginManifest(acmeDevlog); + assert(m && m.bmad && m.bmad.code === 'devlog', 'readPluginManifest reads .claude-plugin/plugin.json#bmad'); + + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-nobmad-')); + await fs.ensureDir(path.join(tmp, '.claude-plugin')); + await fs.writeFile(path.join(tmp, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'x' }), 'utf8'); + const none = await readPluginManifest(tmp); + assert(none === null, 'readPluginManifest returns null for plugin.json without a bmad block'); + await fs.remove(tmp).catch(() => {}); + } + + // ---- Strategy 0: plugin.json#bmad resolves as format 'plugin-json' ---- + { + const resolved = await new PluginResolver().resolve(acmeDevlog, { name: 'acme-devlog', source: '.', skills: [] }); + assert(resolved.length === 1, 'Strategy 0 resolves a single new-system module'); + const r = resolved[0]; + assert(r.format === 'plugin-json', 'new-system module carries format: plugin-json'); + assert(r.code === 'devlog', 'new-system module code comes from bmad.code (not plugin name)'); + assert(r.version === '0.4.0' && !!r.manifest && !!r.sourceDir, 'new-system module carries version + manifest + sourceDir'); + assert(!!r.moduleYamlPath && r.moduleYamlPath.endsWith('assets/module.yaml'), 'moduleYamlPath points at source moduleDefinition'); + } + + // ---- Legacy structure still resolves as format 'legacy' ---- + { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-legacy-')); + const skill = path.join(tmp, 'skills', 'foo-setup'); + await fs.ensureDir(path.join(skill, 'assets')); + await fs.writeFile(path.join(skill, 'SKILL.md'), '---\nname: foo-setup\ndescription: x\n---\n', 'utf8'); + await fs.writeFile(path.join(skill, 'assets', 'module.yaml'), 'code: foo\nname: Foo\ndescription: legacy\n', 'utf8'); + await fs.writeFile( + path.join(skill, 'assets', 'module-help.csv'), + 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs\n', + 'utf8', + ); + const resolved = await new PluginResolver().resolve(tmp, { name: 'foo', skills: ['./skills/foo-setup'] }); + assert( + resolved.length === 1 && resolved[0].format === 'legacy' && resolved[0].strategy === 2, + 'legacy setup-skill resolves as format: legacy (strategy 2)', + ); + + // A plugin.json WITHOUT a bmad block must NOT hijack legacy detection. + await fs.ensureDir(path.join(tmp, '.claude-plugin')); + await fs.writeFile( + path.join(tmp, '.claude-plugin', 'plugin.json'), + JSON.stringify({ name: 'foo', skills: ['./skills/foo-setup'] }), + 'utf8', + ); + const resolved2 = await new PluginResolver().resolve(tmp, { name: 'foo', source: '.', skills: ['./skills/foo-setup'] }); + assert(resolved2[0].format === 'legacy', 'plugin.json without bmad block falls through to legacy strategies'); + await fs.remove(tmp).catch(() => {}); + } + + // ---- Strategy 1: module files ABOVE the skills' common parent ---- + // Mirrors the bmad-creative-intelligence-suite layout: module.yaml + + // module-help.csv at src/, skills nested under src/skills//. The + // skills' common parent is src/skills (no module files), so the resolver + // must walk up to src/ to find them — otherwise it synthesizes a degenerate + // module named after the plugin and loses the real code/agents roster. + { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-srcmod-')); + const srcDir = path.join(tmp, 'src'); + await fs.ensureDir(path.join(srcDir, 'skills')); + await fs.writeFile( + path.join(srcDir, 'module.yaml'), + 'code: cis\nname: "CIS: Creative Innovation Suite"\ndescription: legacy at src\n', + 'utf8', + ); + await fs.writeFile( + path.join(srcDir, 'module-help.csv'), + 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs\n', + 'utf8', + ); + for (const name of ['bmad-cis-storytelling', 'bmad-cis-design-thinking']) { + const skill = path.join(srcDir, 'skills', name); + await fs.ensureDir(skill); + await fs.writeFile(path.join(skill, 'SKILL.md'), `---\nname: ${name}\ndescription: x\n---\n`, 'utf8'); + } + + const resolved = await new PluginResolver().resolve(tmp, { + name: 'bmad-creative-intelligence-suite', + source: '.', + skills: ['./src/skills/bmad-cis-storytelling', './src/skills/bmad-cis-design-thinking'], + }); + assert( + resolved.length === 1 && resolved[0].strategy === 1, + 'module files above the skills common parent resolve via strategy 1 (not synthesized strategy 5)', + ); + assert( + resolved[0].code === 'cis' && resolved[0].name === 'CIS: Creative Innovation Suite', + 'code/name come from src/module.yaml, not the marketplace plugin name', + ); + assert( + resolved[0].moduleYamlPath && resolved[0].moduleYamlPath.endsWith(path.join('src', 'module.yaml')), + 'moduleYamlPath points at src/module.yaml', + ); + await fs.remove(tmp).catch(() => {}); + } + + // ---- Multiple module definitions: deepest-first default + chooser ---- + // Both src/ and src/skills/ carry module.yaml + module-help.csv. Headless + // resolution must take the deepest (src/skills); an interactive caller can + // override via chooseModuleDefinition, which receives enriched metadata. + { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-multimod-')); + const csv = + 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs\n'; + const srcDir = path.join(tmp, 'src'); + const skillsDir = path.join(srcDir, 'skills'); + await fs.ensureDir(skillsDir); + await fs.writeFile(path.join(srcDir, 'module.yaml'), 'code: outer\nname: Outer\ndescription: at src\n', 'utf8'); + await fs.writeFile(path.join(srcDir, 'module-help.csv'), csv, 'utf8'); + await fs.writeFile(path.join(skillsDir, 'module.yaml'), 'code: inner\nname: Inner\ndescription: at src/skills\n', 'utf8'); + await fs.writeFile(path.join(skillsDir, 'module-help.csv'), csv, 'utf8'); + for (const name of ['skill-a', 'skill-b']) { + const skill = path.join(skillsDir, name); + await fs.ensureDir(skill); + await fs.writeFile(path.join(skill, 'SKILL.md'), `---\nname: ${name}\ndescription: x\n---\n`, 'utf8'); + } + const plugin = { name: 'multi', source: '.', skills: ['./src/skills/skill-a', './src/skills/skill-b'] }; + + const def = await new PluginResolver().resolve(tmp, plugin); + assert(def[0].code === 'inner', 'with no chooser, the deepest module definition (src/skills) is used by default'); + + let seen = null; + const picked = await new PluginResolver().resolve(tmp, plugin, { + chooseModuleDefinition: async (candidates) => { + seen = candidates; + return candidates.find((c) => c.code === 'outer'); + }, + }); + assert(Array.isArray(seen) && seen.length === 2, 'chooser receives all candidates when more than one is found'); + assert( + seen.every((c) => typeof c.relativePath === 'string' && 'name' in c && 'description' in c), + 'chooser candidates are enriched with relativePath + module.yaml metadata', + ); + assert(picked[0].code === 'outer', "chooser's selection (src) overrides the deepest default"); + await fs.remove(tmp).catch(() => {}); + } + + // ---- End-to-end install of a new-system module via OfficialModules ---- + { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-pj-install-')); + const bmadDir = path.join(tmp, '_bmad'); + await fs.ensureDir(path.join(bmadDir, '_config')); + await fs.writeFile(path.join(bmadDir, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8'); + + const [resolved] = await new PluginResolver().resolve(acmeDevlog, { name: 'acme-devlog', source: '.', skills: [] }); + resolved.localPath = acmeDevlog; + CustomModuleManager._resolutionCache.set(resolved.code, resolved); + + const om = new OfficialModules(); + const tracked = []; + let result; + try { + result = await om.install('devlog', bmadDir, (p) => tracked.push(p), { skipModuleInstaller: true, moduleConfig: {} }); + } finally { + CustomModuleManager._resolutionCache.delete('devlog'); + } + + assert(result.success === true && result.module === 'devlog', 'install() routes plugin-json resolution and succeeds'); + assert( + await fs.pathExists(path.join(bmadDir, 'devlog', 'module.yaml')), + 'install flattens moduleDefinition → _bmad/devlog/module.yaml', + ); + assert( + await fs.pathExists(path.join(bmadDir, 'devlog', 'module-help.csv')), + 'install flattens moduleHelpCsv → _bmad/devlog/module-help.csv', + ); + assert( + await fs.pathExists(path.join(bmadDir, 'devlog', '.claude-plugin', 'plugin.json')), + 'install keeps .claude-plugin/plugin.json', + ); + assert( + await fs.pathExists(path.join(bmadDir, 'devlog', 'skills', 'bmad-devlog-setup', 'SKILL.md')), + 'install copies declared skills under skills/', + ); + assert(!(await fs.pathExists(path.join(bmadDir, 'devlog', 'tests'))), 'install honors bmad.install.ignore (tests/ excluded)'); + + const rewritten = JSON.parse(await fs.readFile(path.join(bmadDir, 'devlog', '.claude-plugin', 'plugin.json'), 'utf8')); + assert(rewritten.bmad.moduleDefinition === './module.yaml', 'plugin.json is rewritten to canonical paths'); + assert(tracked.some((p) => p.endsWith('plugin.json')) && tracked.length > 5, 'install tracks copied files for the files manifest'); + + const yamlLib = require('yaml'); + const mani = yamlLib.parse(await fs.readFile(path.join(bmadDir, '_config', 'manifest.yaml'), 'utf8')); + const entry = mani.modules.find((m) => m.name === 'devlog'); + assert( + entry && entry.source === 'custom' && entry.localPath === acmeDevlog, + 'install registers the module in manifest.yaml (source: custom)', + ); + await fs.remove(tmp).catch(() => {}); + } + } catch (error) { + console.log(`${colors.red}Test Suite 48 setup failed: ${error.message}${colors.reset}`); + console.log(error.stack); + failed++; + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/commands/ide-sync.js b/tools/installer/commands/ide-sync.js new file mode 100644 index 000000000..4eb532e8f --- /dev/null +++ b/tools/installer/commands/ide-sync.js @@ -0,0 +1,34 @@ +const { runIdeSync } = require('../core/ide-sync'); + +// `bmad ide-sync` — distribute the skills recorded in _config/skill-manifest.csv +// to every coding assistant listed under `ides:` in _config/manifest.yaml, then +// reach the canonical end-state (skills in IDE dirs, removed from _bmad/). +// +// Non-interactive by design: it reads the existing manifest rather than +// prompting, so it is safe to run from scripts and without a TTY. It is the same +// distribution the full `bmad install` performs (both route through +// core/ide-sync.js → IdeManager.setupBatch), exposed as a standalone step. The +// bmad-module skill invokes the bundled equivalent after install/update/remove. +module.exports = { + command: 'ide-sync', + description: "Sync installed skills to the coding assistants configured in this project's manifest", + options: [ + ['-d, --directory ', 'Project directory containing _bmad/', '.'], + ['--prune ', 'Comma-separated canonicalIds to remove from IDE directories'], + ['-v, --verbose', 'Verbose output'], + ], + action: async (options) => { + try { + const code = await runIdeSync({ + directory: options.directory || '.', + prune: options.prune || '', + verbose: !!options.verbose, + }); + process.exit(code); + } catch (error) { + process.stderr.write(`[ide-sync] failed: ${error.message}\n`); + if (process.env.BMAD_DEBUG) process.stderr.write(`${error.stack}\n`); + process.exit(1); + } + }, +}; diff --git a/tools/installer/core/ide-sync.js b/tools/installer/core/ide-sync.js new file mode 100644 index 000000000..6f6de5d29 --- /dev/null +++ b/tools/installer/core/ide-sync.js @@ -0,0 +1,249 @@ +// ide-sync — the single, non-interactive primitive for distributing installed +// BMAD skills to the coding assistants (IDEs) recorded in a project's manifest. +// +// This is the ONE implementation of "push skills to the chosen IDEs". Three +// callers route through it so they can never diverge: +// 1. The interactive installer (`Installer._setupIdes` → syncIdes). +// 2. The `bmad ide-sync` CLI command (commands/ide-sync.js → runIdeSync). +// 3. The self-contained bundle shipped into projects at install time and +// invoked by the bmad-module skill (build target wraps runIdeSyncCli). +// +// It reuses the real config-driven IDE engine (IdeManager / ConfigDrivenIdeSetup +// / platform-codes.yaml), so new platforms and handler changes flow here for +// free. The engine is bundleable (fs-native is zero-dep; yaml/csv-parse inline; +// `../prompts` and `../project-root` are aliased to small shims at bundle time). + +const path = require('node:path'); +const fs = require('../fs-native'); +const { IdeManager } = require('../ide/manager'); +const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); + +const writeOut = (m) => process.stdout.write(`${m}\n`); +const writeErr = (m) => process.stderr.write(`${m}\n`); +const DEFAULT_LOGGER = { info: writeOut, warn: writeErr, error: writeErr }; + +/** + * Distribute the skills currently listed in _config/skill-manifest.csv to each + * selected IDE, prune any `previousSkillIds` no longer present, then remove the + * now-redundant skill source dirs from _bmad/ (canonical end-state: skills live + * in IDE dirs). + * + * @param {Object} args + * @param {string} args.projectRoot Project root (contains _bmad/). + * @param {string} args.bmadDir Path to the _bmad/ directory. + * @param {string[]} args.ides Platform codes to set up (from manifest.yaml `ides`). + * @param {string[]} [args.previousSkillIds] canonicalIds to remove from IDE dirs. + * @param {boolean} [args.verbose] + * @param {boolean} [args.cleanup] Remove _bmad/ skill source dirs afterward (default true). + * The interactive installer passes false and runs its own + * unconditional cleanup step. + * @returns {Promise<{skipped: boolean, results: Array}>} + */ +async function syncIdes({ projectRoot, bmadDir, ides, previousSkillIds = [], verbose = false, cleanup = true, silent = false }) { + const validIdes = (ides || []).filter((ide) => ide && typeof ide === 'string'); + if (validIdes.length === 0) return { skipped: true, results: [] }; + + const ideManager = new IdeManager(); + ideManager.setBmadFolderName(path.basename(bmadDir)); + await ideManager.ensureInitialized(); + + const results = await ideManager.setupBatch(validIdes, projectRoot, bmadDir, { + previousSkillIds: new Set(previousSkillIds), + verbose, + silent, + }); + + // Mirror Installer._cleanupSkillDirs: skills are self-contained in IDE dirs, + // so _bmad/ only needs module-level files. Only clean up when every IDE + // synced successfully — otherwise the source skill dirs are still needed to + // retry the failed targets. + const allSucceeded = results.every((r) => r && r.success); + if (cleanup && allSucceeded) await cleanupBmadSkillDirs(bmadDir); + + return { skipped: false, results }; +} + +/** + * Remove skill source directories from _bmad/ after IDE distribution. Reads + * _config/skill-manifest.csv and removes the parent dir of each listed SKILL.md + * (skipping any already gone). Non-skill module files are left untouched. + * Shared with Installer._cleanupSkillDirs so there is one implementation. + * @param {string} bmadDir + */ +async function cleanupBmadSkillDirs(bmadDir) { + const csv = require('csv-parse/sync'); + const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv'); + if (!(await fs.pathExists(csvPath))) return; + + const csvContent = await fs.readFile(csvPath, 'utf8'); + const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true }); + const bmadFolderName = path.basename(bmadDir); + const bmadPrefix = bmadFolderName + '/'; + + const bmadRoot = path.resolve(bmadDir); + for (const record of records) { + if (!record.path) continue; + const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path; + const skillFilePath = path.resolve(bmadDir, relativePath); + // Containment guard: a malformed CSV row (absolute path or `../`) must not + // let cleanup escape _bmad/ and remove arbitrary directories. + if (skillFilePath !== bmadRoot && !skillFilePath.startsWith(bmadRoot + path.sep)) continue; + const sourceDir = path.dirname(skillFilePath); + if (sourceDir === bmadRoot) continue; + if (await fs.pathExists(sourceDir)) { + await fs.remove(sourceDir); + await removeEmptyParents(path.dirname(sourceDir), bmadDir); + } + } +} + +/** + * Remove now-empty parent directories left behind after skill dir cleanup. + * Walks up from dir, stopping at (and never removing) bmadDir. Best-effort: + * a directory that vanishes or fills in mid-walk just ends the walk. + * @param {string} dir - Directory to start walking up from + * @param {string} bmadDir - BMAD installation directory (boundary) + */ +async function removeEmptyParents(dir, bmadDir) { + let current = dir; + while (true) { + // Path-boundary check (not a string prefix, so siblings like _bmad2 don't match). + const rel = path.relative(bmadDir, current); + if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) break; + try { + const entries = await fs.readdir(current); + if (entries.length > 0) break; + await fs.rmdir(current); + } catch { + break; + } + current = path.dirname(current); + } +} + +/** + * Read the selected IDE platform codes from _config/manifest.yaml. + * @param {string} bmadDir + * @returns {Promise} + */ +async function readSelectedIdes(bmadDir) { + const yaml = require('yaml'); + const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml'); + if (!(await fs.pathExists(manifestPath))) return []; + try { + const parsed = yaml.parse(await fs.readFile(manifestPath, 'utf8')); + return Array.isArray(parsed?.ides) ? parsed.ides.filter((i) => i && typeof i === 'string') : []; + } catch { + return []; + } +} + +/** + * End-to-end run used by the CLI command and the shipped bundle: resolve paths, + * read the chosen IDEs from the manifest, distribute, and report. Returns a + * process exit code (0 ok, 1 failure, 2 no install). + * + * @param {Object} opts + * @param {string} [opts.directory] Project dir (default '.'). + * @param {string|string[]} [opts.prune] canonicalIds to remove (CSV string or array). + * @param {boolean} [opts.verbose] + * @param {Object} [opts.logger] { info, warn, error } + * @returns {Promise} exit code + */ +async function runIdeSync(opts = {}) { + const logger = opts.logger || DEFAULT_LOGGER; + const projectRoot = path.resolve(opts.directory || '.'); + const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME); + + if (!(await fs.pathExists(bmadDir))) { + logger.error(`[ide-sync] no BMAD installation (_bmad/) found in ${projectRoot}. Run \`bmad install\` first.`); + return 2; + } + + const ides = await readSelectedIdes(bmadDir); + if (ides.length === 0) { + logger.info('[ide-sync] no IDEs configured in manifest.yaml — nothing to distribute.'); + return 0; + } + + const previousSkillIds = normalizeIdList(opts.prune); + + const { results } = await syncIdes({ + projectRoot, + bmadDir, + ides, + previousSkillIds, + verbose: !!opts.verbose, + // Standalone path prints its own concise [ide-sync] lines; suppress the + // engine's interactive-style status output (errors still surface). + silent: true, + }); + + let failed = 0; + for (const r of results) { + if (r.success) { + logger.info(`[ide-sync] ${r.ide}: ${r.detail || 'configured'}`); + } else { + failed++; + logger.error(`[ide-sync] ${r.ide}: FAILED — ${r.error || 'unknown error'}`); + } + } + return failed > 0 ? 1 : 0; +} + +/** Parse a comma-separated string or array of canonicalIds into a clean array. */ +function normalizeIdList(value) { + if (!value) return []; + const arr = Array.isArray(value) ? value : String(value).split(','); + return arr.map((s) => String(s).trim()).filter(Boolean); +} + +/** + * argv entry point for the shipped bundle. Parses a tiny flag set and calls + * runIdeSync. Intentionally dependency-free (no commander) so the bundle stays + * small and self-contained. + * @param {string[]} argv process.argv.slice(2) + * @returns {Promise} exit code + */ +async function runIdeSyncCli(argv = []) { + const opts = { directory: '.', prune: '', verbose: false }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith('--directory=')) { + opts.directory = a.slice('--directory='.length); + continue; + } + if (a.startsWith('--prune=')) { + opts.prune = a.slice('--prune='.length); + continue; + } + switch (a) { + case '-d': + case '--directory': { + opts.directory = argv[++i] ?? '.'; + break; + } + case '--prune': { + opts.prune = argv[++i] ?? ''; + break; + } + case '-v': + case '--verbose': { + opts.verbose = true; + break; + } + default: { + break; + } + } + } + return runIdeSync(opts); +} + +module.exports = { + syncIdes, + cleanupBmadSkillDirs, + readSelectedIdes, + runIdeSync, + runIdeSyncCli, +}; diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 9c6c6cb6c..674020050 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -372,21 +372,27 @@ class Installer { async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) { if (config.skipIde || !config.ides || config.ides.length === 0) return; - await this.ideManager.ensureInitialized(); const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); - if (validIdes.length === 0) { addResult('IDE configuration', 'warn', 'no valid IDEs selected'); return; } - const setupResults = await this.ideManager.setupBatch(validIdes, paths.projectRoot, paths.bmadDir, { - selectedModules: allModules || [], + // Route through the shared distribution primitive so the interactive + // installer and the standalone `bmad ide-sync` command can never diverge. + // cleanup:false — the install flow runs its own unconditional + // _cleanupSkillDirs afterward (it must run even when no IDEs are selected). + const { syncIdes } = require('./ide-sync'); + const { results } = await syncIdes({ + projectRoot: paths.projectRoot, + bmadDir: paths.bmadDir, + ides: validIdes, + previousSkillIds: [...previousSkillIds], verbose: config.verbose, - previousSkillIds, + cleanup: false, }); - for (const setupResult of setupResults) { + for (const setupResult of results) { const ide = setupResult.ide; if (setupResult.success) { addResult(ide, 'ok', setupResult.detail || ''); @@ -401,51 +407,12 @@ class Installer { * Skills are self-contained in IDE directories, so _bmad/ only needs * module-level files (config.yaml, _config/, etc.). * Also cleans up skill dirs left by older installer versions. + * Delegates to the shared implementation so there is one copy of this logic. * @param {string} bmadDir - BMAD installation directory */ async _cleanupSkillDirs(bmadDir) { - const csv = require('csv-parse/sync'); - const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv'); - if (!(await fs.pathExists(csvPath))) return; - - const csvContent = await fs.readFile(csvPath, 'utf8'); - const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true }); - const bmadFolderName = path.basename(bmadDir); - const bmadPrefix = bmadFolderName + '/'; - - for (const record of records) { - if (!record.path) continue; - const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path; - const sourceDir = path.dirname(path.join(bmadDir, relativePath)); - if (await fs.pathExists(sourceDir)) { - await fs.remove(sourceDir); - await this._removeEmptyParents(path.dirname(sourceDir), bmadDir); - } - } - } - - /** - * Remove now-empty parent directories left behind after skill dir cleanup. - * Walks up from dir, stopping at (and never removing) bmadDir. Best-effort: - * a directory that vanishes or fills in mid-walk just ends the walk. - * @param {string} dir - Directory to start walking up from - * @param {string} bmadDir - BMAD installation directory (boundary) - */ - async _removeEmptyParents(dir, bmadDir) { - let current = dir; - while (true) { - // Path-boundary check (not a string prefix, so siblings like _bmad2 don't match). - const rel = path.relative(bmadDir, current); - if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) break; - try { - const entries = await fs.readdir(current); - if (entries.length > 0) break; - await fs.rmdir(current); - } catch { - break; - } - current = path.dirname(current); - } + const { cleanupBmadSkillDirs } = require('./ide-sync'); + await cleanupBmadSkillDirs(bmadDir); } async _readSkillManifestRows(bmadDir) { diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index f7b5d0084..6a6696c68 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -401,23 +401,141 @@ class ManifestGenerator { const csvPath = path.join(cfgDir, 'skill-manifest.csv'); const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; - let csvContent = 'canonicalId,name,description,module,path\n'; + // Preserve rows for modules installed via the bmad-module skill — + // those rows are not in this.skills (the installer doesn't walk + // community sources), so without this they'd be silently dropped on + // every regeneration. + const preserved = await this._readPreservedCommunityCsvRows(csvPath); + const rows = []; for (const skill of this.skills) { - const row = [ - escapeCsv(skill.canonicalId), - escapeCsv(skill.name), - escapeCsv(skill.description), - escapeCsv(skill.module), - escapeCsv(skill.path), - ].join(','); - csvContent += row + '\n'; + rows.push([skill.canonicalId, skill.name, skill.description, skill.module, skill.path]); + } + for (const r of preserved) rows.push(r); + rows.sort((a, b) => { + if (a[3] !== b[3]) return a[3].localeCompare(b[3]); + return a[0].localeCompare(b[0]); + }); + + let csvContent = 'canonicalId,name,description,module,path\n'; + for (const r of rows) { + csvContent += r.map(escapeCsv).join(',') + '\n'; } await fs.writeFile(csvPath, csvContent); return csvPath; } + /** + * Load the set of module codes installed via the bmad-module skill + * (source: 'community' in manifest.yaml). Empty set if no manifest or none. + * @returns {Promise>} + */ + async _loadCommunityModuleCodes() { + if (!this.bmadDir) return new Set(); + const manifestPath = path.join(this.bmadDir, '_config', 'manifest.yaml'); + if (!(await fs.pathExists(manifestPath))) return new Set(); + try { + const parsed = yaml.parse(await fs.readFile(manifestPath, 'utf8')); + const modules = Array.isArray(parsed?.modules) ? parsed.modules : []; + return new Set( + modules.filter((m) => m && typeof m === 'object' && m.source === 'community' && typeof m.name === 'string').map((m) => m.name), + ); + } catch { + return new Set(); + } + } + + /** + * Read rows from a previously-generated CSV that belong to community-source + * modules. Used by writeSkillManifest and writeFilesManifest to keep + * community entries across installer regenerations. + * @param {string} csvPath + * @returns {Promise>>} + */ + async _readPreservedCommunityCsvRows(csvPath) { + if (!(await fs.pathExists(csvPath))) return []; + const communityCodes = await this._loadCommunityModuleCodes(); + if (communityCodes.size === 0) return []; + let text; + try { + text = await fs.readFile(csvPath, 'utf8'); + } catch { + return []; + } + const rows = ManifestGenerator._parseCsv(text); + if (rows.length < 2) return []; + // The `module` column lives at a different index in each CSV: + // skill-manifest.csv: canonicalId,name,description,module,path → 3 + // files-manifest.csv: type,name,module,path,hash → 2 + // Look it up from the header rather than hardcoding the index. + const header = rows[0]; + const moduleIdx = header.indexOf('module'); + if (moduleIdx === -1) return []; + return rows.slice(1).filter((r) => r.length > moduleIdx && communityCodes.has(r[moduleIdx])); + } + + /** + * Minimal CSV parser for the shapes this generator writes: header + + * records with `"…"` fields, quotes escaped as `""`. + * @param {string} text + * @returns {Array>} + */ + static _parseCsv(text) { + const rows = []; + let row = []; + let field = ''; + let i = 0; + let inQuotes = false; + while (i < text.length) { + const c = text[i]; + if (inQuotes) { + if (c === '"') { + if (text[i + 1] === '"') { + field += '"'; + i += 2; + continue; + } + inQuotes = false; + i++; + continue; + } + field += c; + i++; + } else { + if (c === '"') { + inQuotes = true; + i++; + continue; + } + if (c === ',') { + row.push(field); + field = ''; + i++; + continue; + } + if (c === '\n' || c === '\r') { + if (field !== '' || row.length > 0) { + row.push(field); + rows.push(row); + } + row = []; + field = ''; + if (c === '\r' && text[i + 1] === '\n') i += 2; + else i++; + continue; + } + field += c; + i++; + } + } + if (field !== '' || row.length > 0) { + row.push(field); + rows.push(row); + } + return rows; + } + /** * Write central _bmad/config.toml with [core], [modules.], [agents.] tables. * Install-owned. Team-scope answers → config.toml; user-scope answers → config.user.toml. @@ -681,6 +799,12 @@ class ManifestGenerator { async writeFilesManifest(cfgDir) { const csvPath = path.join(cfgDir, 'files-manifest.csv'); + // Preserve rows for modules installed via the bmad-module skill — + // those files are not in this.allInstalledFiles or this.files (the + // installer doesn't walk community sources), so without this they'd be + // silently dropped on every regeneration. + const preserved = await this._readPreservedCommunityCsvRows(csvPath); + // Create CSV header with hash column let csv = 'type,name,module,path,hash\n'; @@ -724,6 +848,12 @@ class ManifestGenerator { } } + // Merge in preserved community rows before sorting so they interleave + // with the freshly-generated rows in stable (module, type, name) order. + for (const r of preserved) { + allFiles.push({ type: r[0], name: r[1], module: r[2], path: r[3], hash: r[4] || '' }); + } + // Sort files by module, then type, then name allFiles.sort((a, b) => { if (a.module !== b.module) return a.module.localeCompare(b.module); diff --git a/tools/installer/ide/platform-codes.js b/tools/installer/ide/platform-codes.js index 6d1aa9180..865b89955 100644 --- a/tools/installer/ide/platform-codes.js +++ b/tools/installer/ide/platform-codes.js @@ -2,10 +2,20 @@ const fs = require('../fs-native'); const path = require('node:path'); const yaml = require('yaml'); -const PLATFORM_CODES_PATH = path.join(__dirname, 'platform-codes.yaml'); - let _cachedPlatformCodes = null; +/** + * Resolve the platform-codes.yaml path. Defaults to the copy beside this file, + * but honors BMAD_IDE_PLATFORM_CODES so the self-contained bundle the + * bmad-module skill ships can point at the YAML beside it (esbuild output does + * not preserve this file's original __dirname). Resolved lazily so the env var + * can be set before the first load. + * @returns {string} + */ +function resolvePlatformCodesPath() { + return process.env.BMAD_IDE_PLATFORM_CODES || path.join(__dirname, 'platform-codes.yaml'); +} + /** * Load the platform codes configuration from YAML * @returns {Object} Platform codes configuration @@ -15,6 +25,7 @@ async function loadPlatformCodes() { return _cachedPlatformCodes; } + const PLATFORM_CODES_PATH = resolvePlatformCodesPath(); if (!(await fs.pathExists(PLATFORM_CODES_PATH))) { throw new Error(`Platform codes configuration not found at: ${PLATFORM_CODES_PATH}`); } diff --git a/tools/installer/modules/bmad-module-lib.js b/tools/installer/modules/bmad-module-lib.js new file mode 100644 index 000000000..a6021fb1c --- /dev/null +++ b/tools/installer/modules/bmad-module-lib.js @@ -0,0 +1,93 @@ +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); +const { getSourcePath } = require('../project-root'); + +/** + * Bridge to the bmad-module skill's ESM libraries. + * + * The installer is CommonJS; the new module system's install logic lives as + * zero-dependency ESM under `src/core-skills/bmad-module/scripts/lib/`. Rather + * than reimplement (and risk drifting from) the spec, the installer reuses the + * exact same functions the runtime `bmad-module` skill uses to validate a + * `.claude-plugin/plugin.json#bmad` manifest and lay a module out on disk. + * + * This file is the single place that knows the `src/` layout. It lazily + * `import()`s each lib once and caches the namespace. `pathToFileURL` makes the + * dynamic-import specifier valid on Windows (bare absolute paths are rejected + * there). + */ + +const LIB_REL = ['core-skills', 'bmad-module', 'scripts', 'lib']; + +function libUrl(file) { + return pathToFileURL(getSourcePath(...LIB_REL, file)).href; +} + +const _cache = new Map(); +async function load(file) { + if (!_cache.has(file)) { + _cache.set(file, await import(libUrl(file))); + } + return _cache.get(file); +} + +/** + * Load the subset of skill libs the installer needs to detect, validate, copy, + * and finalize a new-system (`plugin.json#bmad`) module. Returns a flat object + * of the named exports. + */ +async function loadBmadModuleLib() { + const [pluginJson, installPlan, fsSafe, npmDeps] = await Promise.all([ + load('plugin-json.mjs'), + load('install-plan.mjs'), + load('fs-safe.mjs'), + load('npm-deps.mjs'), + ]); + return { + // plugin-json.mjs + readAndValidateManifest: pluginJson.readAndValidateManifest, + RESERVED_CODES: pluginJson.RESERVED_CODES, + CODE_REGEX: pluginJson.CODE_REGEX, + // install-plan.mjs + readUserIgnores: installPlan.readUserIgnores, + buildIgnoreMatcher: installPlan.buildIgnoreMatcher, + buildCopyPlan: installPlan.buildCopyPlan, + rewriteManifestPaths: installPlan.rewriteManifestPaths, + validateDeclaredPaths: installPlan.validateDeclaredPaths, + // fs-safe.mjs + stageCopyPlan: fsSafe.stageCopyPlan, + atomicSwapDir: fsSafe.atomicSwapDir, + // npm-deps.mjs + installModuleDeps: npmDeps.installModuleDeps, + }; +} + +/** + * Read `.claude-plugin/plugin.json` from a directory and return the parsed + * object only when it carries a `bmad` block (i.e. it's a new-system module + * manifest). Returns null when the file is absent, unparseable, or lacks a + * `bmad` key — callers then fall back to the legacy marketplace.json path. + * No validation here; resolution validates via readAndValidateManifest. + * + * @param {string} dir - Absolute path to a candidate module root + * @returns {Promise} + */ +async function readPluginManifest(dir) { + const fs = require('../fs-native'); + const manifestPath = path.join(dir, '.claude-plugin', 'plugin.json'); + if (!(await fs.pathExists(manifestPath))) return null; + try { + const parsed = JSON.parse(await fs.readFile(manifestPath, 'utf8')); + if (parsed && typeof parsed === 'object' && parsed.bmad && typeof parsed.bmad === 'object') { + return parsed; + } + } catch (error) { + // Malformed JSON — fall back to the legacy resolver (or validateDeclaredPaths + // at install time) rather than hard-failing, but warn so the corruption is + // not swallowed silently and looks indistinguishable from a missing file. + process.stderr.write(`[bmad-module] warning: ignoring invalid JSON in ${manifestPath}: ${error.message}\n`); + } + return null; +} + +module.exports = { loadBmadModuleLib, readPluginManifest }; diff --git a/tools/installer/modules/custom-module-manager.js b/tools/installer/modules/custom-module-manager.js index 8a5ea8863..50dedb16e 100644 --- a/tools/installer/modules/custom-module-manager.js +++ b/tools/installer/modules/custom-module-manager.js @@ -572,12 +572,14 @@ class CustomModuleManager { * @param {Object} plugin - Raw plugin object from marketplace.json * @param {string} [sourceUrl] - Original URL for manifest tracking (null for local) * @param {string} [localPath] - Local source path for manifest tracking (null for URLs) + * @param {Object} [options] - Forwarded to PluginResolver.resolve (e.g. + * chooseModuleDefinition for interactive disambiguation). * @returns {Promise>} Array of ResolvedModule objects */ - async resolvePlugin(repoPath, plugin, sourceUrl, localPath) { + async resolvePlugin(repoPath, plugin, sourceUrl, localPath, options = {}) { const { PluginResolver } = require('./plugin-resolver'); const resolver = new PluginResolver(); - const resolved = await resolver.resolve(repoPath, plugin); + const resolved = await resolver.resolve(repoPath, plugin, options); // Read clone metadata (written by cloneRepo) so we can pick up the // resolved git ref + SHA for manifest recording. diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index db2933427..2d701879d 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -5,6 +5,7 @@ const prompts = require('../prompts'); const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root'); const { CLIUtils } = require('../cli-utils'); const { ExternalModuleManager } = require('./external-manager'); +const { loadBmadModuleLib } = require('./bmad-module-lib'); class OfficialModules { constructor(options = {}) { @@ -312,6 +313,14 @@ class OfficialModules { * @param {Object} options - Installation options */ async installFromResolution(resolved, bmadDir, fileTrackingCallback = null, options = {}) { + // New module system: a .claude-plugin/plugin.json#bmad manifest drives the + // copy. Reuse the bmad-module skill's libs so the on-disk result matches a + // skill-driven install exactly. Legacy (marketplace.json + module.yaml) + // resolutions fall through to the original path below. + if (resolved.format === 'plugin-json') { + return this._installFromPluginJson(resolved, bmadDir, fileTrackingCallback, options); + } + const targetPath = path.join(bmadDir, resolved.code); if (await fs.pathExists(targetPath)) { @@ -378,6 +387,140 @@ class OfficialModules { }; } + /** + * Install a new-module-system module (resolved from .claude-plugin/plugin.json#bmad). + * + * Reuses the bmad-module skill's ESM libs (via bmad-module-lib) to build the + * curated copy plan, flatten moduleDefinition/moduleHelpCsv to the module root, + * rewrite plugin.json to canonical paths, and install npm deps in place — so the + * on-disk layout is byte-identical to a `bmad-module install`. Downstream installer + * steps (config generation, directory creation, help-catalog merge, manifest/skill + * discovery, IDE distribution) then treat it exactly like any other module, since + * module.yaml + module-help.csv now sit at the module root. + * + * @param {Object} resolved - ResolvedModule with format 'plugin-json' (sourceDir, manifest) + * @param {string} bmadDir - Target _bmad directory + * @param {Function} fileTrackingCallback - Optional callback to track installed files + * @param {Object} options - Installation options + */ + async _installFromPluginJson(resolved, bmadDir, fileTrackingCallback = null, options = {}) { + const crypto = require('node:crypto'); + const lib = await loadBmadModuleLib(); + const { sourceDir, manifest, code } = { sourceDir: resolved.sourceDir, manifest: resolved.manifest, code: resolved.code }; + const targetPath = path.join(bmadDir, code); + + // Validate declared paths (throws on traversal / escapes) and build the plan. + lib.validateDeclaredPaths(sourceDir, manifest); + const userIgnores = await lib.readUserIgnores(sourceDir, manifest); + const matchIgnore = lib.buildIgnoreMatcher(userIgnores); + const { plan } = await lib.buildCopyPlan(sourceDir, manifest, matchIgnore); + const rewrittenManifestJson = lib.rewriteManifestPaths(manifest); + + // Stage on the same filesystem as the target, then atomically swap in. + const stagedDir = path.join(bmadDir, `.${code}.bmad-stage-${crypto.randomBytes(6).toString('hex')}`); + try { + await lib.stageCopyPlan(sourceDir, stagedDir, plan, { + '.claude-plugin/plugin.json': rewrittenManifestJson, + }); + await lib.atomicSwapDir(stagedDir, targetPath); + } catch (error) { + await fs.remove(stagedDir).catch(() => {}); + throw new Error(`Failed to install ${code}: ${error.message}`); + } + + // Track every installed file for the files manifest. + if (fileTrackingCallback) { + fileTrackingCallback(path.join(targetPath, '.claude-plugin', 'plugin.json')); + for (const { destRel } of plan) { + fileTrackingCallback(path.join(targetPath, destRel)); + } + } + + // npm deps in place (honors bmad.install.skipNpm; non-fatal). + try { + const dep = await lib.installModuleDeps(targetPath, manifest); + if (dep.ran && dep.ok) await prompts.log.info(` Installed npm dependencies for ${code}`); + else if (dep.ran && !dep.ok) await prompts.log.warn(` npm install failed for ${code}: ${dep.error}`); + } catch (error) { + await prompts.log.warn(` npm install failed for ${code}: ${error.message}`); + } + + // Warn about unmet declared module dependencies (best-effort, non-fatal). + await this._warnUnmetModuleDeps(manifest, bmadDir, code); + + // Create directories declared in module.yaml (now flattened at module root). + if (!options.skipModuleInstaller) { + await this.createModuleDirectories(code, bmadDir, options); + } + + // Register in the manifest. Keep the installer's existing custom tagging so + // its own update / quick-update path is unaffected — the new-module-system + // compliance is about on-disk layout + which manifest format is recognized, + // not the _bmad/_config/manifest.yaml source tag. + const { Manifest } = require('../core/manifest'); + const manifestObj = new Manifest(); + const hasGitClone = !!resolved.repoUrl; + const manifestEntry = { + version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null), + source: 'custom', + npmPackage: null, + repoUrl: resolved.repoUrl || null, + }; + if (hasGitClone) { + manifestEntry.channel = resolved.cloneRef ? 'pinned' : 'next'; + if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha; + if (resolved.rawInput) manifestEntry.rawSource = resolved.rawInput; + } + if (resolved.localPath) manifestEntry.localPath = resolved.localPath; + await manifestObj.addModule(bmadDir, code, manifestEntry); + + // Surface install hints the way the bmad-module skill does. + const claudeOnly = []; + if (manifest.hooks) claudeOnly.push('hooks'); + if (manifest.mcpServers) claudeOnly.push('mcpServers'); + if (manifest.lspServers) claudeOnly.push('lspServers'); + if (Array.isArray(manifest.agents) && manifest.agents.length > 0) claudeOnly.push('agents'); + if (Array.isArray(manifest.commands) && manifest.commands.length > 0) claudeOnly.push('commands'); + if (claudeOnly.length > 0) { + await prompts.log.info( + ` ${code}: ${claudeOnly.join(', ')} are Claude Code plugin surfaces — copied but not auto-activated. Wire them up via Claude Code's plugin manager.`, + ); + } + if (manifest.bmad?.install?.postInstallSkill) { + await prompts.log.info(` ${code}: next, run the \`${manifest.bmad.install.postInstallSkill}\` skill to finish setup.`); + } + + return { + success: true, + module: code, + path: targetPath, + versionInfo: { version: manifestEntry.version || '' }, + }; + } + + /** + * Best-effort, non-fatal warning for declared bmad.dependencies.modules that + * are not present on disk and not selected for install in this run. + * @param {Object} manifest - The module's plugin.json manifest + * @param {string} bmadDir - Target _bmad directory + * @param {string} code - The installing module's code (skip self-reference) + */ + async _warnUnmetModuleDeps(manifest, bmadDir, code) { + const deps = manifest?.bmad?.dependencies?.modules; + if (!Array.isArray(deps) || deps.length === 0) return; + + const { CustomModuleManager } = require('./custom-module-manager'); + for (const dep of deps) { + const depCode = typeof dep === 'string' ? dep : dep?.code; + if (!depCode || depCode === code) continue; + const onDisk = await fs.pathExists(path.join(bmadDir, depCode)); + const selectedThisRun = CustomModuleManager._resolutionCache.has(depCode); + if (onDisk || selectedThisRun) continue; + const versionStr = typeof dep === 'object' && dep.version ? ` (${dep.version})` : ''; + await prompts.log.warn(` ${code} declares a dependency on module '${depCode}'${versionStr} — ensure it is installed.`); + } + } + /** * Update an existing module * @param {string} moduleName - Name of the module to update @@ -551,21 +694,29 @@ class OfficialModules { const projectRoot = path.dirname(bmadDir); const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] }; - // Special handling for core module - it's in src/core-skills not src/modules - let sourcePath; - if (moduleName === 'core') { - sourcePath = getSourcePath('core-skills'); - } else { - sourcePath = await this.findModuleSource(moduleName, { silent: true }); - if (!sourcePath) { - return emptyResult; // No source found, skip - } - } - - // Read module.yaml to find the `directories` key - const moduleYamlPath = path.join(sourcePath, 'module.yaml'); + // Prefer the flattened installed module.yaml. buildCopyPlan() copies a + // new-spec module's moduleDefinition to _bmad//module.yaml, where it + // carries the canonical `directories` declarations — but its source tree may + // keep module.yaml under a skill asset path that findModuleSource() can't + // locate, which would otherwise skip the declared working dirs. + let moduleYamlPath = path.join(bmadDir, moduleName, 'module.yaml'); if (!(await fs.pathExists(moduleYamlPath))) { - return emptyResult; // No module.yaml, skip + // Special handling for core module - it's in src/core-skills not src/modules + let sourcePath; + if (moduleName === 'core') { + sourcePath = getSourcePath('core-skills'); + } else { + sourcePath = await this.findModuleSource(moduleName, { silent: true }); + if (!sourcePath) { + return emptyResult; // No source found, skip + } + } + + // Read module.yaml to find the `directories` key + moduleYamlPath = path.join(sourcePath, 'module.yaml'); + if (!(await fs.pathExists(moduleYamlPath))) { + return emptyResult; // No module.yaml, skip + } } let moduleYaml; diff --git a/tools/installer/modules/plugin-resolver.js b/tools/installer/modules/plugin-resolver.js index 8cef26d27..25ab6c99e 100644 --- a/tools/installer/modules/plugin-resolver.js +++ b/tools/installer/modules/plugin-resolver.js @@ -2,17 +2,25 @@ const fs = require('../fs-native'); const path = require('node:path'); const yaml = require('yaml'); const { MODULE_HELP_CSV_HEADER } = require('./module-help-schema'); +const { loadBmadModuleLib, readPluginManifest } = require('./bmad-module-lib'); /** - * Resolves how to install a plugin from marketplace.json by analyzing - * where module.yaml and module-help.csv live relative to the listed skills. + * Resolves how to install a plugin by analyzing its on-disk shape. * - * Five strategies, tried in order: + * Strategy 0 (new module system), tried first: + * 0. A `.claude-plugin/plugin.json` carrying a `bmad{}` block at the module + * root. Resolved + validated via the bmad-module skill's own libs so the + * installer and the runtime skill agree on what a module is. + * + * Legacy strategies (marketplace.json + module.yaml), tried in order: * 1. Root module files at the common parent of all skills * 2. A -setup skill with assets/module.yaml + assets/module-help.csv * 3. Single standalone skill with both files in its assets/ * 4. Multiple standalone skills, each with both files in assets/ * 5. Fallback: synthesize from marketplace.json + SKILL.md frontmatter + * + * Every resolved module carries a `format` discriminator ('plugin-json' or + * 'legacy') so the installer can pick the matching install path. */ class PluginResolver { /** @@ -24,9 +32,22 @@ class PluginResolver { * @param {string} [plugin.version] - Semantic version * @param {string} [plugin.description] - Plugin description * @param {string[]} [plugin.skills] - Relative paths to skill directories + * @param {Object} [options] - Resolution options + * @param {function} [options.chooseModuleDefinition] - Async selector invoked + * when more than one module.yaml + module-help.csv pair is found between the + * skills' common parent and the repo root. Receives (candidates, context) + * and returns the chosen candidate. When omitted (headless / non-interactive + * / CLI), the deepest candidate is used so resolution never blocks. * @returns {Promise} Array of resolved module definitions */ - async resolve(repoPath, plugin) { + async resolve(repoPath, plugin, options = {}) { + // Strategy 0: new module system. Tried before everything else — and before + // the no-skills early return below — because new-system modules declare + // their skills inside plugin.json rather than via marketplace.json's + // skills[] array. + const pluginJsonResult = await this._tryPluginJson(repoPath, plugin); + if (pluginJsonResult) return pluginJsonResult; + const skillRelPaths = plugin.skills || []; // No skills array: legacy behavior - caller should use existing findModuleSource @@ -55,7 +76,7 @@ class PluginResolver { // Try each strategy in order const result = - (await this._tryRootModuleFiles(repoPath, plugin, skillPaths)) || + (await this._tryRootModuleFiles(repoPath, plugin, skillPaths, options)) || (await this._trySetupSkill(repoPath, plugin, skillPaths)) || (await this._trySingleStandalone(repoPath, plugin, skillPaths)) || (await this._tryMultipleStandalone(repoPath, plugin, skillPaths)) || @@ -64,20 +85,130 @@ class PluginResolver { return result; } + // ─── Strategy 0: New Module System (plugin.json#bmad) ─────────────────────── + + /** + * Detect a `.claude-plugin/plugin.json` carrying a `bmad{}` block at the + * module root and resolve it via the bmad-module skill's own validator. + * + * The module root is `plugin.source` (relative to the repo) when given, else + * the repo root. Returns a single-element array of a new-format ResolvedModule + * on success, or null to fall through to the legacy strategies. Throws when a + * plugin.json#bmad is present but invalid — a malformed new-system manifest + * should surface, not silently install via the legacy synthesizer. + */ + async _tryPluginJson(repoPath, plugin) { + const repoRoot = path.resolve(repoPath); + let moduleRoot = repoRoot; + if (plugin.source) { + const normalized = String(plugin.source).replace(/^\.\//, ''); + const abs = path.resolve(repoPath, normalized); + // Guard against path traversal out of the repo root. + if (abs !== repoRoot && !abs.startsWith(repoRoot + path.sep)) { + return null; + } + moduleRoot = abs; + } + + const rawManifest = await readPluginManifest(moduleRoot); + if (!rawManifest) return null; + + // Validate with the skill's install-time validator (throws BmadModuleError + // with a descriptive .message on a bad manifest). + const { readAndValidateManifest } = await loadBmadModuleLib(); + const manifest = await readAndValidateManifest(moduleRoot); + + // Resolve declared skill dirs to absolute existing paths for display only; + // the install copy is plan-driven (buildCopyPlan), not skillPaths-driven. + const skillPaths = []; + if (Array.isArray(manifest.skills)) { + for (const rel of manifest.skills) { + if (typeof rel !== 'string') continue; + const abs = path.resolve(moduleRoot, rel.replace(/^\.\//, '')); + if (abs.startsWith(moduleRoot + path.sep) && (await fs.pathExists(abs))) { + skillPaths.push(abs); + } + } + } + + // Point moduleYamlPath at the source moduleDefinition so the installer's + // source-resolution helpers (findModuleSourceByCode → createModuleDirectories, + // resolveInstalledModuleYaml) can read the module's declared `directories`. + // Install itself flattens this to `_bmad//module.yaml` via buildCopyPlan. + let moduleYamlPath = null; + if (typeof manifest.bmad?.moduleDefinition === 'string') { + const abs = path.resolve(moduleRoot, manifest.bmad.moduleDefinition.replace(/^\.\//, '')); + if (abs.startsWith(moduleRoot + path.sep) && (await fs.pathExists(abs))) { + moduleYamlPath = abs; + } + } + + return [ + { + code: manifest.bmad.code, + name: manifest.displayName || manifest.name, + version: manifest.bmad.moduleVersion || manifest.version || null, + description: manifest.description || plugin.description || '', + format: 'plugin-json', + strategy: 'plugin-json', + pluginName: plugin.name, + sourceDir: moduleRoot, + manifest, + skillPaths, + moduleYamlPath, + moduleHelpCsvPath: null, + synthesizedModuleYaml: null, + synthesizedHelpCsv: null, + }, + ]; + } + // ─── Strategy 1: Root Module Files ────────────────────────────────────────── /** - * Check if module.yaml + module-help.csv exist at the common parent of all skills. + * Check if module.yaml + module-help.csv exist at the common parent of all + * skills, or in any directory between there and the repo root. + * + * The canonical BMad layout puts module.yaml + module-help.csv at the repo + * root or under src/, while skills live in src/skills// — i.e. one or + * more levels ABOVE the skills' common parent. We therefore start at the + * common parent and walk up to the repo root, using the first (deepest) + * directory that has both files. This catches the common case where, e.g., + * module.yaml sits at src/module.yaml but skills are in src/skills/. */ - async _tryRootModuleFiles(repoPath, plugin, skillPaths) { + async _tryRootModuleFiles(repoPath, plugin, skillPaths, options = {}) { const commonParent = this._computeCommonParent(skillPaths); - const moduleYamlPath = path.join(commonParent, 'module.yaml'); - const moduleHelpPath = path.join(commonParent, 'module-help.csv'); - - if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) { + const candidates = await this._findModuleFileCandidatesUpward(commonParent, repoPath); + if (candidates.length === 0) { return null; } + // Deepest candidate (closest to the skills) is the safe default. When more + // than one directory in the chain carries both files, give an interactive + // caller the chance to pick — enriching each option with its module.yaml + // metadata so the choice is meaningful. Headless callers fall through to + // the deepest candidate without prompting. + let chosen = candidates[0]; + if (candidates.length > 1 && typeof options.chooseModuleDefinition === 'function') { + const enriched = []; + for (const candidate of candidates) { + const data = await this._readModuleYaml(candidate.moduleYamlPath); + enriched.push({ + ...candidate, + relativePath: path.relative(path.resolve(repoPath), candidate.moduleYamlPath), + code: data?.code || null, + name: data?.name || null, + description: data?.description || null, + }); + } + const picked = await options.chooseModuleDefinition(enriched, { plugin }); + if (picked && picked.moduleYamlPath && picked.moduleHelpPath) { + chosen = picked; + } + } + + const { moduleYamlPath, moduleHelpPath } = chosen; + const moduleData = await this._readModuleYaml(moduleYamlPath); if (!moduleData) return null; @@ -87,6 +218,7 @@ class PluginResolver { name: moduleData.name || plugin.name, version: plugin.version || moduleData.module_version || null, description: moduleData.description || plugin.description || '', + format: 'legacy', strategy: 1, pluginName: plugin.name, moduleYamlPath, @@ -124,6 +256,7 @@ class PluginResolver { name: moduleData.name || plugin.name, version: plugin.version || moduleData.module_version || null, description: moduleData.description || plugin.description || '', + format: 'legacy', strategy: 2, pluginName: plugin.name, moduleYamlPath, @@ -163,6 +296,7 @@ class PluginResolver { name: moduleData.name || plugin.name, version: plugin.version || moduleData.module_version || null, description: moduleData.description || plugin.description || '', + format: 'legacy', strategy: 3, pluginName: plugin.name, moduleYamlPath, @@ -201,6 +335,7 @@ class PluginResolver { name: moduleData.name || path.basename(skillPath), version: plugin.version || moduleData.module_version || null, description: moduleData.description || '', + format: 'legacy', strategy: 4, pluginName: plugin.name, moduleYamlPath, @@ -257,6 +392,7 @@ class PluginResolver { name: moduleName, version: plugin.version || null, description: plugin.description || '', + format: 'legacy', strategy: 5, pluginName: plugin.name, moduleYamlPath: null, @@ -270,6 +406,40 @@ class PluginResolver { // ─── Helpers ──────────────────────────────────────────────────────────────── + /** + * Walk up from startDir to the repo root, collecting every directory that + * contains BOTH module.yaml and module-help.csv. Bounded by repoRoot so we + * never escape the cloned repository. Results are ordered deepest-first + * (closest to startDir), so candidates[0] is the safe default. + * @param {string} startDir - Directory to start searching from (inclusive) + * @param {string} repoPath - Repository root (upper bound, inclusive) + * @returns {Promise>} + */ + async _findModuleFileCandidatesUpward(startDir, repoPath) { + const repoRoot = path.resolve(repoPath); + let dir = path.resolve(startDir); + + // If startDir somehow falls outside the repo, only consider the repo root. + if (dir !== repoRoot && !dir.startsWith(repoRoot + path.sep)) { + dir = repoRoot; + } + + const candidates = []; + while (true) { + const moduleYamlPath = path.join(dir, 'module.yaml'); + const moduleHelpPath = path.join(dir, 'module-help.csv'); + if ((await fs.pathExists(moduleYamlPath)) && (await fs.pathExists(moduleHelpPath))) { + candidates.push({ moduleYamlPath, moduleHelpPath }); + } + if (dir === repoRoot) break; + const parent = path.dirname(dir); + if (parent === dir) break; // filesystem root — stop defensively + dir = parent; + } + + return candidates; + } + /** * Compute the deepest common ancestor directory of an array of absolute paths. * @param {string[]} absPaths - Absolute directory paths diff --git a/tools/installer/project-root.js b/tools/installer/project-root.js index 84ecde5b0..f101608bc 100644 --- a/tools/installer/project-root.js +++ b/tools/installer/project-root.js @@ -171,7 +171,19 @@ async function resolveInstalledModuleYaml(moduleName) { try { const { CustomModuleManager } = require('./modules/custom-module-manager'); for (const [, mod] of CustomModuleManager._resolutionCache) { - if ((mod.code === moduleName || mod.name === moduleName) && mod.localPath) { + // Match on code, display name, OR the marketplace plugin name. A legacy + // module whose module.yaml `code`/`name` (e.g. cis / "CIS: …") diverges + // from its marketplace plugin name (e.g. bmad-creative-intelligence-suite) + // can be tracked downstream under any of the three — match all of them. + const matches = mod.code === moduleName || mod.name === moduleName || mod.pluginName === moduleName; + if (!matches) continue; + // Prefer the resolution's exact module.yaml — searchRoot(localPath) returns + // the FIRST module.yaml under the root, which can be the wrong one in a + // multi-module/multi-plugin repo resolved by pluginName. + if (mod.moduleYamlPath && (await fs.pathExists(mod.moduleYamlPath))) { + return mod.moduleYamlPath; + } + if (mod.localPath) { const found = await searchRoot(mod.localPath); if (found) return found; } diff --git a/tools/installer/ui.js b/tools/installer/ui.js index 12a295d25..86ae02706 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -17,6 +17,7 @@ const { const channelResolver = require('./modules/channel-resolver'); const prompts = require('./prompts'); const { parseSetEntries } = require('./set-overrides'); +const { readPluginManifest } = require('./modules/bmad-module-lib'); const manifest = new Manifest(); @@ -1017,6 +1018,25 @@ class UI { const customMgr = new CustomModuleManager(); const selectedModules = []; + // Interactive disambiguation: when a plugin has more than one module.yaml + + // module-help.csv pair between its skills and the repo root, let the user + // pick which one defines the module. `activeSpinner` is paused around the + // prompt so the choice renders cleanly, then resumed by the caller. + let activeSpinner = null; + const chooseModuleDefinition = async (candidates, { plugin }) => { + activeSpinner?.stop('Multiple module definitions found'); + const choice = await prompts.select({ + message: `"${plugin.name}" declares multiple module definitions — choose which to install:`, + choices: candidates.map((c) => ({ + name: `${c.code || '(no code)'}${c.name ? ` — ${c.name}` : ''} [${c.relativePath}]`, + value: c, + })), + default: candidates[0], + }); + activeSpinner?.start('Analyzing plugin structure...'); + return choice; + }; + let addMore = true; while (addMore) { const sourceInput = await prompts.text({ @@ -1030,6 +1050,7 @@ class UI { }); const s = await prompts.spinner(); + activeSpinner = s; s.start('Resolving source...'); let sourceResult; @@ -1071,7 +1092,9 @@ class UI { const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir; for (const plugin of plugins) { try { - const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath); + const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath, { + chooseModuleDefinition, + }); if (resolved.length > 0) { allResolved.push(...resolved); } else { @@ -1081,6 +1104,7 @@ class UI { name: plugin.displayName || plugin.name, version: plugin.version, description: plugin.description, + format: 'legacy', strategy: 0, pluginName: plugin.name, skillPaths: [], @@ -1091,34 +1115,43 @@ class UI { } } } else { - // Direct mode: no marketplace.json, scan directory for skills and resolve + // Direct mode: no marketplace.json. Prefer a new-system module manifest + // at the root (.claude-plugin/plugin.json#bmad); otherwise scan for + // SKILL.md directories (legacy direct mode). + const rootManifest = await readPluginManifest(sourceResult.rootDir); const directPlugin = { - name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir), + name: rootManifest?.name || sourceResult.parsed.displayName || path.basename(sourceResult.rootDir), source: '.', skills: [], }; - // Scan for SKILL.md directories to populate skills array - try { - const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md'); - if (await fs.pathExists(skillMd)) { - directPlugin.skills.push(entry.name); + if (!rootManifest) { + // Scan for SKILL.md directories to populate skills array + try { + const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md'); + if (await fs.pathExists(skillMd)) { + directPlugin.skills.push(entry.name); + } } } + } catch (scanError) { + s.error('Failed to scan directory'); + await prompts.log.error(` ${scanError.message}`); + addMore = await prompts.confirm({ message: 'Try another source?', default: false }); + continue; } - } catch (scanError) { - s.error('Failed to scan directory'); - await prompts.log.error(` ${scanError.message}`); - addMore = await prompts.confirm({ message: 'Try another source?', default: false }); - continue; } - if (directPlugin.skills.length > 0) { + // New-system modules resolve from plugin.json (skills declared inside it, + // so an empty skills[] here is expected); legacy modules need ≥1 skill. + if (rootManifest || directPlugin.skills.length > 0) { try { - const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath); + const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath, { + chooseModuleDefinition, + }); allResolved.push(...resolved); } catch (resolveError) { await prompts.log.warn(` Could not resolve: ${resolveError.message}`); @@ -1198,6 +1231,12 @@ class UI { .map((s) => s.trim()) .filter(Boolean); + // Non-interactive mode: a source the user explicitly asked for must not be + // silently dropped. Collect failures and throw after attempting every source + // so the install fails (non-zero exit) instead of completing with the + // requested module missing. + const failures = []; + for (const source of sources) { const s = await prompts.spinner(); s.start(`Resolving ${source}...`); @@ -1209,6 +1248,7 @@ class UI { } catch (error) { s.error(`Failed to resolve ${source}`); await prompts.log.error(` ${error.message}`); + failures.push(`${source}: ${error.message}`); continue; } @@ -1234,40 +1274,53 @@ class UI { } catch (discoverError) { s2.error('Failed to discover modules'); await prompts.log.error(` ${discoverError.message}`); + failures.push(`${source}: ${discoverError.message}`); continue; } } else { - // Direct mode: scan for SKILL.md directories + // Direct mode: prefer a new-system manifest at the root, else scan for + // SKILL.md directories (legacy direct mode). + const rootManifest = await readPluginManifest(sourceResult.rootDir); const directPlugin = { - name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir), + name: rootManifest?.name || sourceResult.parsed.displayName || path.basename(sourceResult.rootDir), source: '.', skills: [], }; - try { - const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md'); - if (await fs.pathExists(skillMd)) { - directPlugin.skills.push(entry.name); + if (!rootManifest) { + try { + const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md'); + if (await fs.pathExists(skillMd)) { + directPlugin.skills.push(entry.name); + } } } + } catch { + // Skip unreadable directories } - } catch { - // Skip unreadable directories } - if (directPlugin.skills.length > 0) { + if (rootManifest || directPlugin.skills.length > 0) { try { const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath); allResolved.push(...resolved); - } catch { - // Skip unresolvable + } catch (resolveError) { + s2.error(`Failed to resolve ${source}`); + await prompts.log.error(` ${resolveError.message}`); + failures.push(`${source}: ${resolveError.message}`); + continue; } } } s2.stop(`Found ${allResolved.length} module${allResolved.length === 1 ? '' : 's'}`); + if (allResolved.length === 0) { + failures.push(`${source}: no installable module found at this source`); + continue; + } + for (const mod of allResolved) { allCodes.push(mod.code); const versionStr = mod.version ? ` v${mod.version}` : ''; @@ -1275,6 +1328,10 @@ class UI { } } + if (failures.length > 0) { + throw new Error(`Could not resolve ${failures.length} custom source(s):\n - ${failures.join('\n - ')}`); + } + return allCodes; } diff --git a/tools/validate-skills.js b/tools/validate-skills.js index 8ab5bc2ad..0ec6c160f 100644 --- a/tools/validate-skills.js +++ b/tools/validate-skills.js @@ -195,6 +195,10 @@ function discoverSkillDirs(rootDirs) { for (const entry of entries) { if (!entry.isDirectory()) continue; if (entry.name === 'node_modules' || entry.name === '.git') continue; + // Skip `tests/fixtures/` trees: these hold reference and deliberately- + // malformed example modules (e.g. third-party `acme-*` skills and negative + // fixtures) that intentionally don't follow the production SKILL rules. + if (entry.name === 'fixtures' && path.basename(dir) === 'tests') continue; const fullPath = path.join(dir, entry.name); const skillMd = path.join(fullPath, 'SKILL.md');