一键导入
writing-file-emitter
Implement a FileEmitter for boost-core to emit a custom file (e.g. .mcp.json, .editorconfig) into the host project during boost:sync.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Implement a FileEmitter for boost-core to emit a custom file (e.g. .mcp.json, .editorconfig) into the host project during boost:sync.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | writing-file-emitter |
| description | Implement a FileEmitter for boost-core to emit a custom file (e.g. .mcp.json, .editorconfig) into the host project during boost:sync. |
| metadata | {"boost-tags":"boost-extension"} |
A FileEmitter is a single-method interface in SanderMuller\BoostCore\Contracts\FileEmitter:
public function emit(SyncContext $ctx): ?EmittedFile;
Return EmittedFile to write a file. Return null to skip (e.g. an
optional dependency isn't installed). Throwing is recorded as errored
and sync continues with other emitters.
The contract is @experimental — the shape may change before v1.0
stable. Pin to an exact boost-core version if you build against this.
namespace YourVendor\YourPackage\Emitters;
use SanderMuller\BoostCore\Contracts\FileEmitter;
use SanderMuller\BoostCore\Sync\EmittedFile;
use SanderMuller\BoostCore\Sync\SyncContext;
final class YourEmitter implements FileEmitter
{
public function emit(SyncContext $ctx): ?EmittedFile
{
if (! $ctx->packages->has('some/required-dep')) {
return null;
}
return new EmittedFile(
relativePath: '.your-config.json',
content: json_encode(['key' => 'value']) . "\n",
);
}
}
In your package's composer.json:
{
"extra": {
"boost": {
"emitters": [
"YourVendor\\YourPackage\\Emitters\\YourEmitter"
]
}
}
}
Emitters are only loaded from allowlisted vendors (per the host's
boost.php withAllowedVendors([]) declaration). Untrusted vendors'
emitters never instantiate.
null skips, it does not reapemit() returning null skips writing on this sync — it does not
remove a file an earlier sync already wrote. If your emitter wrote
.mcp.json while an optional dependency was installed, then that
dependency is dropped and emit() starts returning null, the earlier
.mcp.json is left behind: stale, possibly pointing at tooling that no
longer exists.
There is no uninstall hook to react to — boost-core retired its
Composer plugin, so no PACKAGE_UNINSTALL event fires. Reaping is the
sync reconcile's job instead. As of boost-core 0.14.0, the reconcile
records each emitter-emitted path in the sync manifest under the file
category (SyncManifest::CATEGORY_FILE) and prunes the orphaned ones —
a boost-managed path that drops out of the intended set is reaped on a
later sync. On boost-core 0.13 and earlier, emitter outputs are not
tracked and nothing reaps them: the stale file is left behind until the
consumer upgrades. Design for the reconcile either way:
EmittedFile
under the project root and let boost-core write it — never write the
file yourself out-of-band. Only paths boost-core writes for you can be
recorded in the manifest and later reaped; a file written outside that
path is invisible to reconcile and will be left stale.emit() counterpart called
on removal, and stashing cleanup state on the instance does not work
(emit() runs once per sync, never on uninstall). Model removal as
"next sync, this path is no longer in the intended set," not as an
event you handle.null — not by throwing, not by disabling.
The reconcile reaps on a clean null (dormant), but preserves
(never reaps) in two cases: an errored result (a thrown emit()) —
a half-broken sync must not delete a still-wanted file — and an emitter
switched off with withDisabledEmitters() — disabling means "stop
regenerating," not "delete." Throwing or disabling to signal "remove my
file" leaves it in place; return null to deregister the path..mcp.json), the reconcile preserves it rather than deleting
their work — emitter outputs are operator-editable, the same as
guidance files.If your emitter overwrites a file boost never owned, boost does not claim it — it warns on that sync, and the path is never reaped. Ownership is recorded only for files boost created fresh or already owned, so a take-over of pre-existing operator content can never be silently deleted later.
null
does not delete the file on this sync (see Lifecycle above) — the
reconcile reaps it on a later sync, once the path has dropped from
the intended set. On boost-core 0.13 and earlier there is no reaping at
all, so the file is orphaned until the consumer upgrades; emit through
the managed write path only so the reconcile can claim it when present.emit()
is ever called. There is no separate guard method; do skip-checks
inside emit() and return null. Keep constructors parameterless
and side-effect-free.../,
absolute paths) is rejected by FileWriter. Always emit a relative
path under the project root.claude.md resolves to CLAUDE.md), .gitignore,
.boost/, any agent skill/command root (active or not), .ai/,
resources/boost/, or a wrapper-claimed path or its descendants.
Don't rely on ./ prefixes or case variants to slip past it. Emit to
a path that is yours alone.emit() runs more than once. It is called exactly once
per sync. Don't stash state on the instance expecting a later call —
there isn't one. Do all detection ($ctx->packages->has(...)) inline.errored result. Either pick distinct paths or use
one emitter that branches internally.SanderMuller\BoostCore\Sync\SyncContext for what's available on $ctxpackage-boost-laravel's McpJsonEmitter for a real working exampleWrite boost-core skill files — frontmatter shape, skill tags, collision guards, source-dir selection, body content.
Keep the published Composer archive lean by excluding dev/test/AI dirs via .gitattributes.