- Bun workspace symlinks (v1.3.x):
bun installonly createsnode_modules/@agent-plugin-builder/*symlinks at the repo root when the rootpackage.jsondeclares the workspace packages as dependencies ("workspace:*"). Without root-level references, bun links workspace packages only in the nestednode_modulesof packages that depend on them — so code at the repo root (e.g.tests/) cannot resolve@agent-plugin-builder/*and fails withCannot find module. Fix: list all workspace packages as rootdevDependencieswith"workspace:*".
- CI (GitHub Actions) runs
bun install --frozen-lockfile; any fix to module resolution must work from a completely freshnode_modulesstate, not rely on pre-existing symlinks. - The
main/typesfields already point at./src/index.tsin all four packages; those were not the cause of the CI failure. - Hardcoded
/tmp/in tests fails on the Windows CI runner: the CI matrix (ci.yml) runs[ubuntu-latest, macos-latest, windows-latest]. On Windows,/tmp/xresolves to<drive>:\tmp\x(drive root, e.g.D:\tmp) which does not exist →fs.writeFileSync/non-recursivefs.mkdirSyncthrow ENOENT, and error-message assertions containing the literal/tmp/...string mismatch the resolved path. Tests that usefs.mkdirSync(dir, { recursive: true })orfs.mkdtempSync(...)pass on Windows because they create the parent. Fix pattern: usepath.join(os.tmpdir(), ...)(equals/tmpon Linux/macOS, real%TEMP%on Windows). When fixing CI-only test failures, always check.github/workflows/ci.ymlmatrix OSes first.fs.mkdtempSyncdoes NOT create parents (vsmkdirSync recursive) —tests/cli/commands.test.ts(v0.0.9): the module-levelfs.mkdtempSync(path.join('/tmp', 'agent-plugins-cli-'))crashed the whole CLI test file at load on Windows (ENOENT: mkdtemp '\tmp\agent-plugins-cli-XXXXXX'→ "Unhandled error between tests", 1 error + 1 fail).mkdirSync(recursive:true)creates the<drive>:\tmpparent so other files'/tmp/fixtures passed;mkdtempSyncrequires the template dir to exist. Fix:fs.mkdtempSync(path.join(os.tmpdir(), ...))and replace all 18 hardcoded/tmp/test-cli-*scratch dirs in that file withpath.join(os.tmpdir(), ...).
- npm publishing (packages/npm): The publishable
agent-plugin-builderpackage bundles the whole CLI (114 modules incl.prompts,commander,chalk) into one file viabun build ../cli/src/index.ts --outdir ./dist --target node. No--externalneeded — bun bundles CJS deps likepromptsfine for the node target. Two traps solved:bun build --target nodeemits ESM (export { run }), sopackages/npm/package.jsonMUST have"type": "module"or node dies onUnexpected token 'export'.- The CLI entry only
exportsrun()(never auto-invokes), so the bin shim must call it:import('../dist/index.js').then(({ run }) => run()). A bareimport(...)does nothing.
- Version coupling:
packages/cli/src/index.tshardcodes the CLI version for--version; the npm bundle is built from that source, so the version lives in two places (packages/npm/package.json+ CLI source) and must be bumped in lockstep. Both are 0.0.2. - CI publish auth (current): The release job uses
NPM_TOKENviaactions/setup-node. OIDC trusted publishing was tried and failed withENEEDAUTH, so it was reverted. Current shape:permissions: contents: read(noid-token: write),actions/setup-node@v4withnode-version: '20'+registry-url: 'https://registry.npmjs.org'(this is what writes the.npmrcauth line), thennpm publish --access publicwithenv: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}. No--provenance(that needs OIDC). Do not hand-createpackages/npm/.npmrc—setup-nodehandles it; onlysetup-bunalone would have needed one.- Prerequisite: the
NPM_TOKENrepo secret must exist (Automation-type token from npmjs.com). As of the switch,gh api repos/HiAi-gg/agent-plugin-builder/actions/secretsreturned no secrets — the token still has to be added or the release job will fail again.
- Prerequisite: the
- Declarative config (Phases 2–8, v0.0.2):
packages/core/src/config.tsadds aplugin.ymlschema (pluginConfigSchema) +parseConfigFile/configToPortablePlugin. Config file name can beplugin.ymloragent-plugin.yml— it is passed viacreate --config <file>. - MCP server names:
PortableMcpServercarries an optional_namefield (underscore-prefixed = pipeline-internal, never serialized). The generator strips_nameviaconst { _name, ...serverData } = serverbefore writing mcp.json (the strict mcp schema would reject it). When_nameis absent, the name is derived from the command basename or URL hostname (deriveServerName). - Commander
--versiontrap:program.version('0.0.2')registers a program-level--versionoption that greedily interceptscreate --version <version>BEFORE subcommand dispatch (commander'sparseOptionsconsumes options after operands on the parent). Fix: don't use.version(); handle top-level--version/-Vmanually inrun()(args.length === 1check) so the subcommand flag is untouched. - Commander variadic options emit one value per event: with
--mcp-args <args...>, commander emits each value as a separatestring, so the collector must be(value, previous) => [...previous, value]— spreadingvalue(a string) splits it into characters. - LICENSE generation:
packages/generator/src/licenses.tsships full text for MIT, Apache-2.0, ISC, BSD-2-Clause, BSD-3-Clause; unknown SPDX ids get a short notice referencing the id. README/LICENSE are emitted whenPortablePlugin._generateReadme/_licenseTypeare set (or viaGeneratePluginOptions.generateReadme/licenseType).
- init wizard (v0.0.2):
initbuilds aPortablePluginvia exported helpers (buildPortablePlugin,defaultAnswers,parseCommaSeparated,parseEnvPairs,defaultSkillBody) and callsgeneratePlugin()— the same API ascreate. Modes:--config <file>(parseConfigFile/configToPortablePlugin, no prompts),--yes/--non-interactive/CI(defaultAnswers: 1 example skill + README + LICENSE), else interactivepromptswizard with preview viageneratePlugin({ dryRun: true }).optsWithGlobals()on the action's command param is the way to read program-level flags (--force,--non-interactive) from a subcommand — subcommand-level duplicate options are shadowed by the program's own option during the parse walk, so a sub-defined--non-interactivenever lands inoptions. Plugin name validated withNAME_PATTERNfrom@agent-plugin-builder/core(spec/v1); skill names use the stricter^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$(they become directory names). - init --dry-run (Phase 2.4):
initreads the program-level--dry-runglobal (viaoptsWithGlobals()) and passes it togeneratePluginon both write paths — the--configdeclarative path AND the final wizard/--yespath. Dry-run printsDry run - would create:with relative file paths instead of✓ Created. Regression tests intests/cli/commands.test.tsassert no directory/files are created for both paths. - Interactive PTY testing:
promptsneeds a TTY;printf '...' | script -qec "bun ..." /dev/nullworks, but all input must be sent withprintf '%b'(so\ris interpreted) and small sleeps between lines — an instant pipe burst concatenates everything into the first field (each char is a keypress; Enter-as-submit is lost in a burst). - Claude/Cursor MCP migration (ECO-012):
packages/sources/src/claude/index.tsandpackages/sources/src/cursor/index.tsno longer blind-spread.mcp.jsonserver entries. They use an explicit type-mapping block: no type + command present →stdio;stdio/local→stdio;http/streamable-http/remote→streamable-http;sse→sse; unknown type, stdio-missing-command, or http-missing-url →warning+ skip. Only known fields (command, args, env, cwd, url, headers) are extracted intoPortableMcpServerwith_name. - Prettier is NOT enforced in this repo: no
.prettierrc, prettier not in devDependencies, and the codebase uses single quotes (prettier defaults = double quotes) —prettier --check .fails on ~50 files including untouched ones. The real gates (AGENTS.md) arebun run lint(ESLint),bun run typecheck,bun run test. Match repo style (single quotes, semicolons) rather than runningprettier --write(which whole-file reformats to double quotes). - MCP cwd normalization (ECO-004, Issue 3): the mcp.json schema (
packages/core/src/schemas/mcp.ts) only accepts cwd values starting with./,${PLUGIN_ROOT}, or${PLUGIN_DATA}— it must NOT be relaxed (spec-faithful). Shared helpernormalizeMcpCwd()lives inpackages/core/src/normalize-cwd.ts(moved there frompackages/sources— core cannot import from sources, and both need it): absolute cwd under the base dir →./<relative>; absolute cwd outside → warning + preserve original; already-valid values preserved. Callers: vscode + opencode adapters (base = project root), andconfigToPortablePlugin()inpackages/core/src/config.ts(base = config dir, label "config directory", warnings appended tomigrationWarnings).packages/cli/src/commands/package.tsdowngrades cwd schema issues to warnings so hand-written/pre-existing mcp.json files don't block packaging. Note: on Linux, Windows-style absolute cwds (C:\...) are notpath.isAbsolute→ preserved untouched (can't be meaningfully converted cross-platform). - Skill frontmatter parsing (ECO-001 + HR preservation): all five source adapters (claude/cursor/opencode/vscode/codex-adjacent) now use the shared
parseSkillFrontmatter()(packages/sources/src/parse-frontmatter.ts) instead of the old toggle loop. Only a---block at the very start is frontmatter; the FIRST closing---ends it, and any later---lines (horizontal rules) stay in the body — previously the toggle loop re-entered "in frontmatter" at a body HR and swallowed everything after it. Generated SKILL.md must contain exactly ONE frontmatter block (2---delimiters); a migrated body may legitimately contain more (HRs). - Codex TOML args parsing (Issue 1):
packages/sources/src/codex/index.tsparsesconfig.tomlby splitting on table-header lines ([mcp_servers.<name>]) instead of the old regex that terminated the args match at the first[.parseTomlStringArray()handles quoted strings, escapes, and brackets inside values (e.g.--format=[json]). Unparseable args → warning + omit rather than silent drop. - Commander global-flag shadowing (
--forceon migrate): subcommand-local options that duplicate program-level globals get SHADOWED —options.forcein the action handler isundefinedwhileoptsWithGlobals().forceistrue.migrate.tswas readingoptions.force(broken, silently ignored--force); fixed toopts.force(matchesinit.ts). If a subcommand's--force/--dry-runever appears dead, read it viacommand.optsWithGlobals(). - create --config warning surfacing:
create.tsconfig path now printsresult.warnings(fromgeneratePlugin), so out-of-base cwd warnings fromconfigToPortablePlugin()are visible instead of being silently swallowed.migrationWarningsalso live on thePortablePluginmodel; plugin.json itself is metadata-only and never serializes them.