Files
“mkczarkowski” 857a825da6 feat(cli): auto-detect tool profile and add coherence invariants
Wave 2.1 PR1 lands two low-risk CLI polish items ahead of the prework
release so first-run students benefit from a smarter default and the
profile layer gains a drift-safety net.

- Auto-detection: scan the project root for tool-native markers
  (.cursor/rules, .claude/, CLAUDE.md, .github/copilot-instructions.md,
  AGENTS.md, .agents/, .ai/) and rank signals by confidence. The
  interactive first-run prompt pre-fills initialValue with the detected
  tool and prints a "Detected: <tool> (<reason>)" hint. User still
  confirms — no silent selection. Non-TTY paths are unchanged.
- Coherence invariants: SENTINEL_BEGIN/END hoisted to shared constants;
  every profile now references them. New describe block asserts 7
  invariants (paths rooted under manifestDir, safe leaf names, non-empty
  rulesFile, identical sentinels, unique manifestDir, unique toolId,
  non-empty displayName) so refactors fail fast on CI.

Plan: thoughts/shared/plans/2026-04-17-wave2.1-cli-polish.md (Phases 1-2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:15:40 +02:00

75 lines
2.0 KiB
TypeScript

/**
* Shared module mock for @clack/prompts.
*
* Same singleton pattern as auth-flow-mock.ts so that any test file
* importing this helper installs the mock exactly once for the whole
* `bun test` process. The interactive `text()` / `select()` are steerable
* via clackMockState; `note()` messages are captured for assertions.
*
* The mock is harmless for tests that don't reach the interactive path —
* it only replaces the module, and every call site is covered below.
*/
import { mock } from "bun:test";
export interface SelectOption {
value: string;
label: string;
hint?: string | undefined;
}
export interface SelectOpts {
message: string;
options: SelectOption[];
initialValue?: string;
}
export interface ClackMockState {
textImpl: () => unknown;
selectImpl: (opts: SelectOpts) => unknown;
noteMessages: string[];
lastSelect: SelectOpts | null;
selectCalls: SelectOpts[];
}
function defaultSelectImpl(opts: SelectOpts): unknown {
return opts.initialValue ?? opts.options[0]?.value;
}
export const clackMockState: ClackMockState = {
textImpl: () => "you@example.com",
selectImpl: defaultSelectImpl,
noteMessages: [],
lastSelect: null,
selectCalls: [],
};
mock.module("@clack/prompts", () => ({
intro: () => undefined,
outro: () => undefined,
cancel: () => undefined,
note: (msg: string) => {
clackMockState.noteMessages.push(msg);
},
spinner: () => ({
start: () => undefined,
stop: () => undefined,
message: () => undefined,
}),
text: () => Promise.resolve(clackMockState.textImpl()),
select: (opts: SelectOpts) => {
clackMockState.lastSelect = opts;
clackMockState.selectCalls.push(opts);
return Promise.resolve(clackMockState.selectImpl(opts));
},
isCancel: (value: unknown) => typeof value === "symbol",
}));
export function resetClackMock(): void {
clackMockState.textImpl = () => "you@example.com";
clackMockState.selectImpl = defaultSelectImpl;
clackMockState.noteMessages = [];
clackMockState.lastSelect = null;
clackMockState.selectCalls = [];
}