用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/spotify/confidence-cli --skill architecture命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | architecture |
| description | Architecture guidelines and constraints for the Confidence Wizard CLI project |
| version | 0.1 |
This skill defines the structural rules, domain boundaries, and constraints that govern all work on the Confidence Wizard CLI. Follow these when adding features, refactoring, or reviewing changes.
The Confidence Wizard is a CLI tool for quickly setting up and integrating Confidence with users' projects. It works together with a Claude Code Skill backed by Confidence MCP tools (confidence-ai-plugins) — the Skill handles product knowledge, the CLI handles user interaction.
The project is organized into decoupled top-level concerns. Each has its own subdirectory and must not depend on the others' internals.
src/commands/)CLI command definitions using yargs. Each command is a self-contained module exporting a Command object.
default (launches TUI) and help.src/ui/ or src/frameworks/ but never contain UI rendering or framework detection logic themselves.src/commands/index.ts.src/frameworks/)One subdirectory per supported framework (e.g. react/, nextjs/, node/). Each exports a FrameworkConfig implementing a shared interface from src/frameworks/types.ts.
detect() function.src/frameworks/index.ts) exposes getFrameworks() and detectFramework().index.ts — no changes to other framework dirs.src/ui/ or src/commands/.src/providers/)Provider detection and configuration for competing feature flag platforms (e.g. Statsig, Eppo, PostHog, Optimizely). One subdirectory per provider, plus shared types and dependency-reading utilities.
src/providers/index.ts) exposes detectProviders().ProviderConfig implementing the shared interface from src/providers/types.ts.src/ui/, src/commands/, or src/integrations/.src/ui/)Terminal user interface built with Ink and React. Organized into:
tui/screens/ — Every screen is organized as a slice: a subdirectory (e.g. screens/system-check/, screens/welcome/) containing the screen component, a barrel index.ts, and collocated log-messages.ts, telemetry-events.ts, and actions.ts files for screen-specific debug log factories, telemetry event factories, and typed prompt-option constants. Slices with side-effect hooks also contain their hooks (e.g. useSystemCheck.ts). Slices that need initial state computed at mount time have a dedicated useInitial* hook (e.g. useInitialAuth.ts) — see Initialization hooks below. Slices that need sub-components (presentational pieces too large to keep inline in the screen file) place them in a collocated components/ subdirectory within the slice (e.g. screens/select-goal/components/LeftPanel.tsx). These sub-components are private to the slice — they import from sibling files within the slice and from shared tui/ modules, but must not be imported by other slices. To add a screen: create a subdir in screens/, add the component + barrel + log-messages.ts + telemetry-events.ts + actions.ts, register in screen-registry.tsx, add transitions in screen-transitions.ts.tui/components/ — Reusable building blocks and composites (TextBlock, Divider, ScreenContainer, KeyboardHintsBar, TitleBar, etc.). Barrel-exported.tui/styles.ts — Theme constants: Colors, Icons, HAlign, VAlign. All visual styling imports from here.tui/hooks/ — Shared hooks used across screens (useStore, useRouter, useLog, useTerminalSize, etc.). Screen-specific hooks live inside their slice, not here.tui/lib/ — Shared utilities and types used across the TUI (status-line.ts, , , , ). Also contains (shared type and cross-screen helpers like ) and (shared telemetry factories like used by the hook). Screen-specific log and telemetry factories live in each screen's slice, not here.UI modules must not import from src/commands/. They may import from src/frameworks/ only to read framework metadata for display.
The TUI is a generic wizard shell. It must not contain Confidence-specific domain logic such as:
All of this belongs in the Claude Code Skill and is delivered via Confidence MCP tools (confidence-flags, confidence-docs). The TUI only renders what the Skill provides.
Why: The CLI tool and the Skill are separate products with different release cycles. Embedding product knowledge in the TUI creates coupling that makes both harder to evolve. The Skill has access to live documentation and flag state via MCP — the TUI does not.
commands → ui, frameworks, lib
ui → lib, providers (and frameworks for display metadata only)
frameworks → lib
providers → lib
lib → nothing in src/
No circular dependencies. No upward imports. If two domains need to communicate, it flows through src/lib/ shared types.
Within the UI layer, the same principle applies at a finer grain:
screen slices → hooks/, lib/, components/
components/ → lib/, hooks/
hooks/ → lib/
lib/ → nothing in tui/
Shared modules (hooks/, lib/, components/) must never import from screen slices. If a type or utility is needed by both a slice and a shared module, it belongs in tui/lib/ — not re-exported from the slice.
Always use ScreenId enum values from src/lib/session.ts. Never use raw strings for screen identification or navigation.
All session state changes go through WizardStore setters. Never mutate the session object directly — the reactivity system depends on emitChange() being called.
Use @inkjs/ui components (Select, TextInput, Spinner, ConfirmInput, MultiSelect, Badge, StatusMessage, ProgressBar) over standalone ink-* packages. The standalone packages are unmaintained and @inkjs/ui supersedes them.
Each framework gets its own subdirectory. Framework-specific logic stays inside that subdir. The shared FrameworkConfig interface in types.ts is the contract — adding a framework must not require changes to existing framework dirs.
Slices that compute initial state at mount time (e.g. reading persisted tokens, detecting installed plugins, checking the filesystem) use a dedicated useInitial* hook collocated in the same slice directory. This separates the one-time "resolve initial state + sync side effects to the store" concern from the ongoing interaction logic in the main hook.
Each init hook follows the same shape:
resolve* function (defined outside the hook) that computes initial values from dryRun and session data — called once via useState(() => resolve*(...)).useEffect that syncs any store side effects (e.g. store.setAuthState, store.setInstalledPlugins) derived from the resolved values.useState calls.The parent hook consumes it as const initial = useInitial*() and initializes its own state: useState<Phase>(initial.phase).
Existing init hooks:
useInitialAuth — resolves persisted credentials, syncs auth state to storeuseInitialDetection — resolves installed plugins, syncs to storeuseInitialOnboarding — resolves project emptiness / framework, syncs empty-project flaguseInitialMcpDetection — resolves dry-run phase for MCP detectionuseInitialSystemCheck — resolves dry-run checks, syncs check results to storeHooks that support dry-run mode must keep dry-run logic in a separate function from the real implementation. Never interleave them with conditionals, ternaries, or early returns inside a shared function body.
Pattern — the dispatch function checks dryRun and delegates:
function startAuth(mode: 'signup' | 'login') {
setPhase('waiting-browser');
// shared setup...
if ($session.get().dryRun) return startDryRunAuth();
startRealAuth(mode);
}
function startDryRunAuth() {
// simulated behavior only
}
function startRealAuth(mode: 'signup' | 'login') {
// real implementation only
}
When the dispatch function is inside useCallback, define the dry-run and real functions as nested function declarations within the callback body:
const start = useCallback(
function start(fwName: string | null) {
// shared setup...
if (s.dryRun) return startDryRun();
startReal();
function startDryRun() {
/* ... */
}
function startReal() {
/* ... */
}
},
[deps],
);
Init hooks (useInitial*) handle the dry-run initial state separately via the resolve* function — see Initialization Hooks. Effects that only need to skip real work in dry-run mode (e.g. if (session.dryRun) return; before runAllChecks()) are fine as simple guards when the init hook already set up the dry-run state.
The project uses eslint-plugin-react-hooks with the recommended-latest rule set (React Compiler rules). All rules are set to error — no warnings. Key rules enforced:
rules-of-hooks — Hooks must be called at the top level. Functions returned from hooks must not start with use (the linter treats them as hooks and flags calls in callbacks as violations).exhaustive-deps — All values referenced inside useEffect/useMemo/useCallback must appear in the dependency array. Wrap unstable functions in useCallback when they're needed as effect deps.set-state-in-effect — Never call setState synchronously in an effect body. For mount-time initialization, use lazy useState(() => computeInitial()) and the useInitial* hook pattern instead. Async callbacks (.then(), event handlers) inside effects are fine.immutability — Never reference a function or variable before its declaration inside a hook body. Either move the declaration above the call site, inline it into the effect, or use useCallback.When syncing derived React state to the store (or vice versa), always compare values before calling a store setter. Store setters call emitChange(), which can trigger re-renders through subscriptions. Without a guard, setting the same value repeatedly creates render loops — especially when Ink re-mounts components that re-process buffered input.
Pattern:
useEffect(
function syncToStore() {
const current = store.session.someField;
if (derived.length === current.length && derived.every((v, i) => v === current[i])) return;
store.setSomeField(derived);
},
[derived, store],
);
Similarly, applyStatuses-style functions that merge new data into existing state should bail out when nothing actually changed:
function applyStatuses(updated: Record<string, Status>) {
if (Object.entries(updated).every(([k, v]) => current[k] === v)) return;
// ...proceed with setState
}
Use path aliases (@commands/, @frameworks/, @integrations/, @providers/, @ui/, @lib/) for all imports that cross top-level domain boundaries under src/. Keep relative imports for references within the same domain.
Aliases are configured in tsconfig.build.json (paths) and vitest.config.ts (resolve.alias). When adding a new top-level domain under src/, add its alias to both files.
// Cross-domain — use alias
import { ScreenId } from '@lib/session.js';
import { getIntegration } from '@integrations/index.js';
import { detectFramework } from '@frameworks/index.js';
// Within-domain — use relative
import { store } from '../../store.js';
import { Colors } from '../styles.js';
Test files also use aliases for imports from src/:
import { WelcomeScreen } from '@ui/tui/screens/welcome/index.js';
import { ScreenId } from '@lib/session.js';
@lib/, @integrations/, etc.), neighboring modules.type instead of interface for all type definitions.satisfies, using, etc.useEffect, use named functions instead of anonymous lambdas for the effect callback.AbortController for removing event listeners instead of manually calling removeEventListener. Pass { signal: controller.signal } to addEventListener and call controller.abort() in cleanup. This avoids needing to keep a reference to the exact same handler function and scales cleanly when multiple listeners share a lifetime.useRef only when there is no pure-function alternative (e.g. holding a DOM node, a timer ID, or an instance that must survive re-renders without triggering one).ChosenIde | 'skip'), define a named type in the slice's actions.ts and reference it — don't write inline unions like value: SomeType | 'extra' in function signatures. Composition is fine (type DetectedSelectValue = IdeSelectValue | 'continue'), but it must be named and exported from actions.ts.switch statements, the default case must use an exhaustive check via satisfies never to catch unhandled variants at compile time:
default: {
const _exhaustive: never = value satisfies never;
throw new Error(`Unhandled: ${_exhaustive}`);
}
index.ts) re-export only the public API; they are not a place to dump everything the module contains.eslint-plugin-react-hooks (recommended-latest flat config) with all warning-level rules upgraded to errors. See React Hooks Rules above for the key rules.Never suppress, silence, or filter runtime warnings (e.g. --no-warnings, --disable-warning, NODE_NO_WARNINGS) or compiler/linter diagnostics without explicit consent from the developers. Warnings exist to surface real issues — fix the root cause instead of hiding the symptom.
When the project grows, new top-level concerns (e.g. src/agent/ for agent harness logic, src/detection/ for project analysis) follow the same pattern:
src/src/lib/ or own types.tsindex.tsThe wizard works alongside two Confidence MCP servers from confidence-ai-plugins:
confidence-flags — Feature flag management: create, list, resolve, target, archive flags; manage clients, metrics, context schema, and warehouses.confidence-docs — Documentation access: search docs, get SDK integration guides, code snippets, and deployment guides.These are accessed via the Claude Code Skill, never directly from the TUI or CLI code.
wizard-tasks.tslogger.tslayout-budget.tstips.tslog-messages.tsLogMessageskipped()telemetry-events.tsscreenEntered()useTelemetrytui/store.ts — Reactive state via nanostores. All mutations through explicit setters that call emitChange().tui/router.ts — State-machine navigation via WizardRouter. Uses a TransitionMap (screen → event → target screen) with a history stack for back navigation. Screens navigate by calling nav.to(event) or nav.back() through the type-safe useNavigation hook.tui/screen-transitions.ts — Transition map defining all valid screen-to-screen navigation edges. Adding a branch (e.g., a warehouse sub-flow) means adding new edges here — no structural changes needed.tui/screen-registry.tsx — Maps ScreenId → React component. Single place to wire up new screens.