Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ program
// guessing (pre-0.8 this silently wired every agent the machine had ever
// installed — see --yes to get that back).
const home = homedir();
const plan = planInit(repo, { home });
const plan = planInit(repo, { home, mcp: opts.mcp, hooks: opts.hooks, global: opts.global });
const detectedIds = plan.filter((p) => p.detected).map((p) => p.id);
const noAgents = (opts as { agents?: unknown }).agents === false;

Expand Down Expand Up @@ -998,7 +998,7 @@ program
if (opts.dryRun) {
console.error(formatPlan(plan, ids, repo, home));
for (const child of children)
console.error(`\n— ${child}/ (workspace child)\n` + formatPlan(planInit(join(repo, child), { home }), ids, join(repo, child), home));
console.error(`\n— ${child}/ (workspace child)\n` + formatPlan(planInit(join(repo, child), { home, mcp: opts.mcp, hooks: opts.hooks, global: opts.global }), ids, join(repo, child), home));
return;
}
if (ids.length === 0) {
Expand Down Expand Up @@ -1099,7 +1099,12 @@ function wireTarget(
for (const m of r.mcp) console.error(`✓ mcp ${m.id}: ${m.path} (${m.action})`);
for (const h of r.hooks) console.error(`✓ hook ${h.id}: ${h.path} (${h.action})`);
// Only worth saying when there was actually something out-of-repo to skip.
if (opts.global === false && selectedWrites(plan, ids).some((w) => w.scope === "global"))
// `plan` already reflects --no-global, so ask the unsuppressed plan whether
// there was anything out-of-repo to skip.
if (
opts.global === false &&
selectedWrites(planInit(repo, { home, mcp: opts.mcp, hooks: opts.hooks }), ids).some((w) => w.scope === "global")
)
console.error("· skipped out-of-repo writes (--no-global)");
}

Expand Down
41 changes: 35 additions & 6 deletions src/hosts/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,55 @@ function instructionTarget(repo: string, host: HostTarget): PlannedWrite {
* touch. Claude Code comes first — it's the deep integration and the picker's
* default. `ids`, when given, restricts the plan to those hosts.
*/
export function planInit(repo: string, opts: { home?: string; ids?: string[] } = {}): HostPlan[] {
export interface PlanInitOptions {
home?: string;
ids?: string[];
/** `--no-mcp`: skip MCP server registration for the non-Claude hosts. */
mcp?: boolean;
/** `--no-hooks`: skip hook installation for the non-Claude hosts. */
hooks?: boolean;
/** `--no-global`: skip every write outside the repo. */
global?: boolean;
}

/**
* The same predicates `runHostsInit()` applies when it writes, so `--dry-run`
* describes the operation the flags actually select (#329):
* - `--no-mcp` drops the MCP registrations;
* - `--no-hooks` drops the Codex and Cursor hooks;
* - `--no-global` drops everything outside the repo: the `~/.claude` copy, the
* global MCP configs (Codex, Antigravity), the Codex hooks and the
* Antigravity skill. Cursor's hooks are repo-local and stay.
*/
export function planInit(repo: string, opts: PlanInitOptions = {}): HostPlan[] {
const home = opts.home ?? homedir();
const probe = probeFor(home, repo);
const detected = new Set(detectHosts(probe).map((h) => h.id));
const withGlobal = opts.global !== false;
const withMcp = opts.mcp !== false;
const withHooks = opts.hooks !== false;
const inScope = (w: PlannedWrite) => withGlobal || w.scope !== 'global';

const plans: HostPlan[] = [
// Repo writes plus the user-level copy under `~/.claude` — the picker and
// `--dry-run` render 'global' writes in their own section, so a user sees
// what lands outside the repo before agreeing to it.
{ id: 'claude', name: 'Claude Code', detected: true, writes: [...claudeTargets(repo), ...claudeGlobalTargets(home)] },
{
id: 'claude',
name: 'Claude Code',
detected: true,
writes: [...claudeTargets(repo), ...(withGlobal ? claudeGlobalTargets(home) : [])],
},
...HOSTS.map((host) => ({
id: host.id,
name: host.name,
detected: detected.has(host.id),
writes: [
instructionTarget(repo, host),
...mcpTargets(repo, [host.id], { home }),
...(host.id === 'agents' ? hookTargets(home) : []),
...(host.id === 'cursor' ? cursorHookTargets(repo) : []),
...(host.id === 'antigravity' ? antigravitySkillTargets(home) : []),
...(withMcp ? mcpTargets(repo, [host.id], { home }).filter(inScope) : []),
...(host.id === 'agents' && withHooks && withGlobal ? hookTargets(home) : []),
...(host.id === 'cursor' && withHooks ? cursorHookTargets(repo) : []),
...(host.id === 'antigravity' && withGlobal ? antigravitySkillTargets(home) : []),
],
})),
];
Expand Down
29 changes: 29 additions & 0 deletions test/hosts-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,32 @@ test('selectedWrites filters to the chosen hosts only', () => {
const writes = selectedWrites(plan, ['claude', 'adal']);
assert.deepEqual([...new Set(writes.map((w) => w.hostId))].sort(), ['adal', 'claude']);
});

test('planInit honors --no-mcp / --no-hooks / --no-global like runHostsInit does (#329)', () => {
const repo = fresh(); const home = fullHome();
const byId = (plan: ReturnType<typeof planInit>, id: string) => plan.find((p) => p.id === id)!.writes;

const full = planInit(repo, { home });
assert.ok(byId(full, 'agents').some((w) => w.scope === 'global'), 'default plan lists the ~/.codex writes');
assert.ok(byId(full, 'cursor').some((w) => w.path.endsWith('hooks.json')), 'default plan lists the Cursor hooks');

const noMcp = planInit(repo, { home, mcp: false });
for (const p of noMcp) for (const w of p.writes) {
if (p.id === 'claude') continue;
assert.ok(!/mcp|config\.toml|settings\.json/.test(w.path) || w.path.endsWith('hooks.json'), `--no-mcp still plans ${w.path}`);
}

const noHooks = planInit(repo, { home, hooks: false });
assert.ok(!byId(noHooks, 'agents').some((w) => /hooks/.test(w.path)), '--no-hooks drops the Codex hooks');
assert.ok(!byId(noHooks, 'cursor').some((w) => w.path.endsWith('hooks.json')), '--no-hooks drops the Cursor hooks');

const noGlobal = planInit(repo, { home, global: false });
for (const p of noGlobal) for (const w of p.writes) {
assert.notEqual(w.scope, 'global', `--no-global still plans ${w.path}`);
assert.ok(!toPosixPath(w.path).startsWith(toPosixPath(home)), `--no-global still plans ${w.path} under ~`);
}
assert.ok(byId(noGlobal, 'cursor').some((w) => w.path.endsWith('hooks.json')), 'Cursor hooks are repo-local and survive --no-global');

const suppressed = planInit(repo, { home, mcp: false, hooks: false, global: false });
assert.deepEqual(byId(suppressed, 'agents').map((w) => toPosixPath(w.path)), [toPosixPath(join(repo, 'AGENTS.md'))]);
});
Loading