소스 정보
- 저장소
- spotify/confidence-cli
- 최근 소스 활동
- 2026년 8월 14일 16:10
- 감지된 SKILL.md 언어
- 영어
- 스타
- 0
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/spotify/confidence-cli --skill architecture명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
SOC 직업 분류 기준
| 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.