@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
52 lines (47 loc) • 1.94 kB
JavaScript
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
import {
basename,
dirname,
isAbsolute,
join,
relative,
resolve,
} from "node:path";
const MARKER =
'{"schemaVersion":"sellable-hermes-snapshot-dirty/v1","dirty":true}\n';
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
async function main() {
// Do not wait for stdin EOF. Hermes keeps the shell-hook input pipe open while
// it waits for the hook process, so draining stdin here deadlocks until Hermes
// kills the helper at its timeout. The marker does not depend on tool payload
// bytes; leaving stdin untouched also guarantees that none are parsed or logged.
const configuredHome = process.env.HERMES_HOME || process.env.HOME || "";
if (!isAbsolute(configuredHome)) throw new Error("identity");
const home = resolve(configuredHome);
const inferredProfile =
basename(dirname(home)) === "profiles" ? basename(home) : "";
const profile = process.env.SELLABLE_HERMES_PROFILE_ID || inferredProfile;
if (!SAFE_ID.test(profile)) throw new Error("identity");
const named =
basename(home) === profile && basename(dirname(home)) === "profiles";
if (basename(dirname(home)) === "profiles" && !named)
throw new Error("identity");
const root = named ? home : join(home, "profiles", profile);
const marker = resolve(
process.env.SELLABLE_HERMES_MEMORY_DIRTY_MARKER ||
join(root, ".sellable-agent", "memory", "dirty.json")
);
const confined = relative(root, marker);
if (confined.startsWith("..") || isAbsolute(confined))
throw new Error("marker");
mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
chmodSync(dirname(marker), 0o700);
const temporary = `${marker}.${process.pid}.tmp`;
writeFileSync(temporary, MARKER, { mode: 0o600 });
renameSync(temporary, marker);
chmodSync(marker, 0o600);
}
main().catch(() => {
process.exitCode = 1;
});