feat(plugin): install the pipelines' skill plugins on SessionStart
The ship-feature pipelines drive `/superpowers:*` commands as their per-node drivers, so a fresh install had a dependency it was only told about — the `/ccam-doctor` WARN row and a line in the README. `server/lib/skill-plugins.js` now carries the whole thing, shared by the doctor (reports) and the bootstrap (installs) so the two cannot disagree about what is missing. The bootstrap installs at the very end of its DETACHED worker — the install clones from the network and nothing in the 10 s SessionStart hook may wait on it — retrying once behind a `marketplace add`, logging a failure with the manual command, and never throwing: the dashboard, hooks, CLI and MCP all work without these plugins. The outcome is recorded as `state.skillPluginsSettled`, which `fastPathOk()` now also requires, so the check runs exactly once per install and `/ccam-update` is the retry. `CCAM_AUTO_INSTALL_PLUGINS=0|false|off` opts out. Reporting is derived, installing is allowlisted. Requirements come from the loaded templates, which is right for a report; installing from that same source would not be, because a template is a file anyone can drop into `DASHBOARD_PIPELINES_DIR` — it would turn "edit a JSON file" into "run this vendor's install command on the next session". Auto-install consults `AUTO_INSTALLABLE`, a constant in code (`superpowers` today), and reports every other requirement as skipped. Tests inject a `run` seam so no case reaches a real `claude plugin install`, and cover the refusal explicitly.
This commit is contained in:
@@ -113,6 +113,51 @@ describe("fast path", () => {
|
||||
boot.writeState({ pluginVersion: pkg.version, depsHash: boot.depsHash() }, RT);
|
||||
assert.equal(boot.fastPathOk(RT), false);
|
||||
});
|
||||
|
||||
it("is false while the skill-plugin check has never settled", () => {
|
||||
// A machine can be fully installed and still lack the plugins the
|
||||
// pipelines drive; the worker has to run once to find that out.
|
||||
const pkg = require("../../package.json");
|
||||
fs.mkdirSync(path.join(RT, "node_modules"), { recursive: true });
|
||||
boot.writeState(
|
||||
{
|
||||
pluginVersion: pkg.version,
|
||||
depsHash: boot.depsHash(),
|
||||
pluginRoot: path.resolve(__dirname, "..", ".."),
|
||||
},
|
||||
RT
|
||||
);
|
||||
assert.equal(boot.fastPathOk(RT), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("skill-plugin auto-install gate", () => {
|
||||
// Both cases here must return WITHOUT reaching server/lib/skill-plugins.js —
|
||||
// that module shells out to `claude plugin install`, and no test may.
|
||||
beforeEach(resetRuntime);
|
||||
after(() => delete process.env.CCAM_AUTO_INSTALL_PLUGINS);
|
||||
|
||||
for (const flag of ["0", "false", "off"]) {
|
||||
it(`is off when CCAM_AUTO_INSTALL_PLUGINS=${flag}`, () => {
|
||||
process.env.CCAM_AUTO_INSTALL_PLUGINS = flag;
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT), "off");
|
||||
});
|
||||
}
|
||||
|
||||
it("runs once: a settled state is returned verbatim, not re-attempted", () => {
|
||||
delete process.env.CCAM_AUTO_INSTALL_PLUGINS;
|
||||
boot.writeState({ skillPluginsSettled: "installed" }, RT);
|
||||
|
||||
// Verbatim matters: writeState records whatever comes back, so a marker
|
||||
// here would erase the only record of what actually happened.
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT), "installed");
|
||||
});
|
||||
|
||||
it("is off under the flag even when forced, so /ccam-update honours the opt-out", () => {
|
||||
process.env.CCAM_AUTO_INSTALL_PLUGINS = "0";
|
||||
boot.writeState({ skillPluginsSettled: "failed" }, RT);
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT, { force: true }), "off");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bootstrap lock", () => {
|
||||
|
||||
Reference in New Issue
Block a user