oh-my-design-cli 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +38 -1
  4. package/agents/omd-master.md +13 -1
  5. package/data/reference-fingerprints.json +380 -2
  6. package/dist/bin/oh-my-design.js +1 -1
  7. package/dist/{install-skills-IETT2TBJ.js → install-skills-UKEVE3KT.js} +11 -9
  8. package/dist/{install-skills-IETT2TBJ.js.map → install-skills-UKEVE3KT.js.map} +1 -1
  9. package/package.json +10 -4
  10. package/scripts/ctx-prime.cjs +266 -0
  11. package/scripts/postinstall.cjs +6 -6
  12. package/skills/omd-harness/SKILL.md +135 -7
  13. package/web/references/91app/DESIGN.md +151 -0
  14. package/web/references/airtable/DESIGN.md +16 -2
  15. package/web/references/bithumb/DESIGN.md +170 -0
  16. package/web/references/bunjang/DESIGN.md +19 -0
  17. package/web/references/cakeresume/DESIGN.md +162 -0
  18. package/web/references/catchtable/DESIGN.md +19 -0
  19. package/web/references/classum/DESIGN.md +19 -0
  20. package/web/references/dabang/DESIGN.md +19 -0
  21. package/web/references/dji/DESIGN.md +0 -1
  22. package/web/references/fastcampus/DESIGN.md +19 -0
  23. package/web/references/flex/DESIGN.md +19 -0
  24. package/web/references/gmarket/DESIGN.md +19 -0
  25. package/web/references/gogolook/DESIGN.md +126 -0
  26. package/web/references/hahow/DESIGN.md +158 -0
  27. package/web/references/hyundaicard/DESIGN.md +172 -0
  28. package/web/references/inflearn/DESIGN.md +19 -0
  29. package/web/references/kbank/DESIGN.md +18 -0
  30. package/web/references/kdan/DESIGN.md +160 -0
  31. package/web/references/kkbox/DESIGN.md +114 -0
  32. package/web/references/kream/DESIGN.md +18 -0
  33. package/web/references/lunit/DESIGN.md +19 -0
  34. package/web/references/melon/DESIGN.md +153 -0
  35. package/web/references/nhncloud/DESIGN.md +174 -0
  36. package/web/references/oliveyoung/DESIGN.md +19 -0
  37. package/web/references/rayark/DESIGN.md +132 -0
  38. package/web/references/sendbird/DESIGN.md +285 -0
  39. package/web/references/socar/DESIGN.md +18 -0
  40. package/web/references/toss-securities/DESIGN.md +19 -0
  41. package/web/references/tving/DESIGN.md +18 -0
  42. package/web/references/upbit/DESIGN.md +19 -0
  43. package/web/references/upstage/DESIGN.md +18 -0
  44. package/web/references/velog/DESIGN.md +168 -0
  45. package/web/references/wadiz/DESIGN.md +19 -0
  46. package/web/references/webflow/DESIGN.md +16 -2
  47. package/web/references/yeogiotte/DESIGN.md +19 -0
  48. package/data/architecture-proposals/2026-05-13-thin-install-fresh-fetch.md +0 -189
  49. package/data/issues/2026-05-13-multi-surface-schema-rfc.md +0 -67
  50. package/data/reference-audits/2026-05-13-kr10.md +0 -132
  51. package/data/reference-audits/2026-05-14-kr10.md +0 -72
  52. package/data/reference-audits/2026-05-15-kr10.md +0 -124
  53. package/data/research/2026-05-18-agent-landscape.md +0 -69
  54. package/data/research/2026-05-18-kr-style-presets.md +0 -572
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli/install-skills.ts","../src/core/agent-detect.ts"],"sourcesContent":["import * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport {\n readFileSync,\n readdirSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n} from 'node:fs';\nimport { join, dirname, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { detectInstalledAgents } from '../core/agent-detect.js';\n\nexport type SkillTarget = 'claude-code' | 'codex' | 'opencode';\n\nexport interface InstallSkillsOptions {\n dir?: string;\n agents?: SkillTarget[];\n force?: boolean;\n /** Non-interactive: install all skills + all agents without TUI prompt.\n * Default false → interactive multiselect when TTY available. */\n all?: boolean;\n /** Pre-select specific skill names from CLI flag (`--skills omd-init,omd-apply`).\n * Overrides interactive prompt when set. */\n skillsFilter?: string[];\n /** Pre-select specific agent names. Overrides interactive prompt when set. */\n agentsFilter?: string[];\n}\n\ninterface InstallPlan {\n target: SkillTarget;\n destDir: string;\n layout: 'folder' | 'flat';\n}\n\nfunction findPackageRoot(): string | null {\n let cur = dirname(fileURLToPath(import.meta.url));\n for (let i = 0; i < 8; i++) {\n if (existsSync(join(cur, 'skills'))) return cur;\n const parent = dirname(cur);\n if (parent === cur) break;\n cur = parent;\n }\n return null;\n}\n\nfunction listShippedSkills(packageRoot: string): string[] {\n const skillsDir = join(packageRoot, 'skills');\n if (!existsSync(skillsDir)) return [];\n return readdirSync(skillsDir)\n .filter((name) => existsSync(join(skillsDir, name, 'SKILL.md')))\n .sort();\n}\n\n/**\n * Canonical agent definitions live at `agents/<name>.md` (markdown with YAML\n * frontmatter). Channel-specific files (.claude/agents/*.md, .codex/agents/*.toml)\n * are generated artifacts — never the source of truth.\n *\n * The package ships only `agents/` and the generator emits per-channel files\n * into the user's project on `omd install-skills`.\n */\nfunction listCanonicalAgents(packageRoot: string): string[] {\n const dir = join(packageRoot, 'agents');\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((name) => name.startsWith('omd-') && name.endsWith('.md'))\n .sort();\n}\n\ninterface ParsedAgent {\n name: string;\n description: string;\n tools: string[];\n model: string;\n body: string;\n}\n\n/** Parse `agents/<name>.md` YAML frontmatter + body into structured form. */\nfunction parseCanonicalAgent(packageRoot: string, filename: string): ParsedAgent {\n const src = readFileSync(join(packageRoot, 'agents', filename), 'utf8');\n const match = /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/.exec(src);\n if (!match) {\n throw new Error(`agents/${filename}: missing YAML frontmatter`);\n }\n const fm = match[1];\n const body = match[2];\n const grab = (key: string): string => {\n const re = new RegExp(`^${key}:\\\\s*(.+)$`, 'm');\n const m = re.exec(fm);\n return m ? m[1].trim().replace(/^[\"']|[\"']$/g, '') : '';\n };\n return {\n name: grab('name') || filename.replace(/\\.md$/, ''),\n description: grab('description'),\n tools: grab('tools')\n .split(',')\n .map((t) => t.trim())\n .filter(Boolean),\n model: grab('model') || 'sonnet',\n body,\n };\n}\n\n/** Map Claude tool names to Codex tool names (best-effort). */\nfunction claudeToolsToCodex(tools: string[]): string[] {\n const m: Record<string, string> = {\n Read: 'read_file',\n Write: 'write_file',\n Edit: 'edit_file',\n Bash: 'shell',\n Glob: 'search',\n Grep: 'search',\n WebFetch: 'web_fetch',\n WebSearch: 'search',\n Agent: 'spawn_agent',\n TaskCreate: 'task',\n TaskUpdate: 'task',\n TaskList: 'task',\n };\n const out = new Set<string>();\n for (const t of tools) out.add(m[t] ?? t.toLowerCase());\n return [...out].sort();\n}\n\n/** Map Claude model alias to Codex/OpenAI model id (best-effort). */\nfunction claudeModelToCodex(model: string): string {\n const m: Record<string, string> = {\n haiku: 'gpt-4.1-mini',\n sonnet: 'gpt-4.1',\n opus: 'gpt-4.1',\n };\n return m[model.toLowerCase()] ?? 'gpt-4.1';\n}\n\n/** Render a canonical agent as a Claude Code subagent file.\n * IMPORTANT: Claude Code's subagent parser requires YAML frontmatter (`---`)\n * as the FIRST line of the file. Any preceding content (HTML comments, blank\n * lines) breaks discovery. So we encode the managed-by-omd marker as a\n * custom frontmatter field (`omd_managed: true`) instead of an HTML comment.\n */\nfunction renderClaudeAgent(a: ParsedAgent): string {\n const fm = [\n '---',\n `name: ${a.name}`,\n `description: ${a.description}`,\n `tools: ${a.tools.join(', ')}`,\n `model: ${a.model}`,\n `omd_managed: true`,\n '---',\n '',\n ].join('\\n');\n return fm + a.body;\n}\n\n/** Render a canonical agent as a Codex TOML file (declarative pointer). */\nfunction renderCodexAgent(a: ParsedAgent): string {\n const tools = claudeToolsToCodex(a.tools);\n const model = claudeModelToCodex(a.model);\n const desc = a.description.replace(/\"/g, '\\\\\"');\n return [\n `[agent]`,\n `name = \"${a.name}\"`,\n `description = \"${desc}\"`,\n `model = \"${model}\"`,\n `max_threads = 1`,\n `allowed_tools = [${tools.map((t) => `\"${t}\"`).join(', ')}]`,\n '',\n `instructions = \"\"\"`,\n `Source of truth: agents/${a.name}.md (canonical). The full role spec is`,\n `mirrored to .claude/agents/${a.name}.md when installed for Claude Code.`,\n `Follow that spec verbatim regardless of channel.`,\n '',\n `Codex notes:`,\n `- Spawn sub-agents via spawn_agent with names matching .codex/agents/<name>.toml`,\n `- Use shell to invoke CLI helpers (omd init prepare, omd remember, git apply, npx axe-core, npx lighthouse)`,\n `- All artifacts go inside .omd/runs/run-<latest>/ (or skills/omd-lab-02-design-harness/runs/<lab-version>-...)`,\n `\"\"\"`,\n '',\n ].join('\\n');\n}\n\nfunction planForTarget(projectRoot: string, target: SkillTarget): InstallPlan {\n switch (target) {\n case 'claude-code':\n return {\n target,\n destDir: join(projectRoot, '.claude', 'skills'),\n layout: 'folder',\n };\n case 'codex':\n return {\n target,\n destDir: join(projectRoot, '.codex', 'skills'),\n layout: 'folder',\n };\n case 'opencode':\n return {\n target,\n destDir: join(projectRoot, '.opencode', 'agents'),\n layout: 'flat',\n };\n }\n}\n\nconst MANAGED_HEADER =\n '<!-- omd:installed-skill — managed by `omd install-skills`. Do not edit; rerun the command to refresh. -->';\n\ninterface InstallResult {\n target: SkillTarget;\n skill: string;\n destPath: string;\n status: 'created' | 'updated' | 'unchanged' | 'skipped-drift';\n}\n\nfunction installOne(\n packageRoot: string,\n plan: InstallPlan,\n skill: string,\n force: boolean\n): InstallResult {\n const src = readFileSync(\n join(packageRoot, 'skills', skill, 'SKILL.md'),\n 'utf8'\n );\n const managed = MANAGED_HEADER + '\\n\\n' + src;\n const destPath =\n plan.layout === 'folder'\n ? join(plan.destDir, skill, 'SKILL.md')\n : join(plan.destDir, skill + '.md');\n\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n if (exists && existing === managed) {\n return { target: plan.target, skill, destPath, status: 'unchanged' };\n }\n\n if (exists && !existing.startsWith(MANAGED_HEADER) && !force) {\n return { target: plan.target, skill, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, managed, 'utf8');\n return {\n target: plan.target,\n skill,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\n/** Install a hook script from package's .claude/hooks/ to project. */\nfunction installHookFile(\n packageRoot: string,\n projectRoot: string,\n filename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = 'claude-code';\n const skillLabel = `hook:${filename}`;\n const srcPath = join(packageRoot, '.claude', 'hooks', filename);\n const destPath = join(projectRoot, '.claude', 'hooks', filename);\n\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src);\n return { target, skill: skillLabel, destPath, status: exists ? 'updated' : 'created' };\n}\n\n/**\n * Install / merge .claude/settings.json. We MERGE hooks (don't clobber user\n * customizations) by checking if the omd-managed `_doc` field is present.\n * Without --force, if a user-edited settings.json exists (no _doc field),\n * we skip with `skipped-drift`.\n */\nfunction installSettingsJson(\n packageRoot: string,\n projectRoot: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = 'claude-code';\n const skillLabel = 'settings:.claude/settings.json';\n const srcPath = join(packageRoot, '.claude', 'settings.json');\n const destPath = join(projectRoot, '.claude', 'settings.json');\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n // Check if it's the omd-managed version\n try {\n const parsed = JSON.parse(existing);\n if (typeof parsed._doc === 'string' && parsed._doc.includes('OmD')) {\n // managed — overwrite\n } else {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n } catch {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n }\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src);\n return { target, skill: skillLabel, destPath, status: exists ? 'updated' : 'created' };\n}\n\n/**\n * Copy a read-only data asset (reference-fingerprints.json, vocabulary.json, …)\n * from the package's `data/` into the project's `.claude/data/` or `.codex/data/`.\n * The skill reads these at runtime — they replace the deprecated `omd init recommend` CLI.\n */\nfunction installDataFile(\n packageRoot: string,\n projectRoot: string,\n channelDir: string,\n dataFilename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = channelDir === '.claude' ? 'claude-code' : 'codex';\n const skillLabel = `data:${dataFilename}`;\n\n const srcPath = join(packageRoot, 'data', dataFilename);\n const destPath = join(projectRoot, channelDir, 'data', dataFilename);\n\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n // Data files are pure copies — no managed header (would corrupt JSON).\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n // Honor user customization unless --force\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src, 'utf8');\n return {\n target,\n skill: skillLabel,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\n/**\n * Generate a per-channel agent file from the canonical `agents/<name>.md`.\n *\n * Channel = 'claude' → emits `.claude/agents/<name>.md` (markdown w/ frontmatter)\n * Channel = 'codex' → emits `.codex/agents/<name>.toml` (TOML pointer)\n */\nfunction installAgentFile(\n packageRoot: string,\n projectRoot: string,\n channel: 'claude' | 'codex',\n filename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = channel === 'claude' ? 'claude-code' : 'codex';\n const skillLabel = `agent:${filename}`;\n\n const parsed = parseCanonicalAgent(packageRoot, filename);\n const rendered =\n channel === 'claude' ? renderClaudeAgent(parsed) : renderCodexAgent(parsed);\n\n const destFilename =\n channel === 'claude' ? filename : filename.replace(/\\.md$/, '.toml');\n const destPath = join(\n projectRoot,\n channel === 'claude' ? '.claude' : '.codex',\n 'agents',\n destFilename\n );\n\n // For Claude Code: managed marker is encoded as `omd_managed: true` INSIDE the\n // frontmatter (rendered above) — no HTML comment can precede `---` or the\n // subagent loader rejects the file.\n // For Codex: TOML allows leading comments, so `# omd:installed-agent ...` is fine.\n const managed =\n channel === 'claude'\n ? rendered\n : '# omd:installed-agent — generated from agents/' +\n filename +\n ' by `omd install-skills`. Do not edit; rerun the command to refresh.\\n\\n' +\n rendered;\n\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n if (exists && existing === managed) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n\n // Drift detection sentinels:\n // Claude — look for `omd_managed: true` line inside frontmatter\n // Codex — look for `# omd:installed-agent` comment\n const isManaged =\n channel === 'claude'\n ? /\\nomd_managed:\\s*true\\b/.test(existing)\n : existing.startsWith('# omd:installed-agent');\n\n if (exists && !isManaged && !force) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, managed, 'utf8');\n return {\n target,\n skill: skillLabel,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\nconst STATUS_LABEL: Record<InstallResult['status'], string> = {\n created: pc.green('created'),\n updated: pc.cyan('updated'),\n unchanged: pc.dim('unchanged'),\n 'skipped-drift': pc.yellow('skipped'),\n};\n\nfunction autoDetectTargets(projectRoot: string): SkillTarget[] {\n const presence = detectInstalledAgents(projectRoot);\n const targets: SkillTarget[] = [];\n if (presence.claudeCode) targets.push('claude-code');\n if (presence.codex) targets.push('codex');\n if (presence.opencode) targets.push('opencode');\n // Cursor uses .mdc rules, not skills — installed via `omd sync`.\n if (targets.length === 0) {\n // Fallback: install for all three so user gets coverage even without\n // explicit signal. Idempotent so cost is low.\n return ['claude-code', 'codex', 'opencode'];\n }\n return targets;\n}\n\nexport async function runInstallSkills(\n opts: InstallSkillsOptions = {}\n): Promise<number> {\n const projectRoot = opts.dir ?? process.cwd();\n const packageRoot = findPackageRoot();\n if (!packageRoot) {\n console.error(pc.red('omd install-skills: package data not found'));\n return 1;\n }\n\n const allSkills = listShippedSkills(packageRoot);\n if (allSkills.length === 0) {\n console.error(pc.red('omd install-skills: no skills found in package'));\n return 1;\n }\n const allAgents = listCanonicalAgents(packageRoot);\n\n const force = opts.force ?? false;\n\n p.intro(\n pc.bold('omd install-skills') +\n pc.dim(` (${relative(process.cwd(), projectRoot) || '.'})`)\n );\n\n // Resolve selection: --all flag, --skills/--agents/--agent filter, or interactive TUI.\n // TUI runs only when stdin is a TTY and the corresponding flag isn't set.\n const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);\n const nonInteractive = opts.all || !isTTY || opts.skillsFilter || opts.agentsFilter;\n\n const detected = autoDetectTargets(projectRoot);\n // Real presence (not the all-3 fallback) — used purely for hint labels.\n const presence = detectInstalledAgents(projectRoot);\n const actuallyDetected: SkillTarget[] = [\n presence.claudeCode ? 'claude-code' : null,\n presence.codex ? 'codex' : null,\n presence.opencode ? 'opencode' : null,\n ].filter((x): x is SkillTarget => x !== null);\n\n let skills: string[];\n let canonicalAgents: string[];\n let targets: SkillTarget[];\n\n if (nonInteractive) {\n // Non-interactive resolution\n skills = opts.skillsFilter\n ? allSkills.filter((s) => opts.skillsFilter!.includes(s))\n : allSkills;\n canonicalAgents = opts.agentsFilter\n ? allAgents.filter((a) => opts.agentsFilter!.includes(a.replace(/\\.md$/, '')))\n : allAgents;\n targets = opts.agents\n ? opts.agents\n : opts.all\n ? (['claude-code', 'codex', 'opencode'] as SkillTarget[])\n : detected;\n } else {\n // === Interactive TUI — skill → subagent → channel order ===\n // 1. Skills (default = ALL selected)\n const skillResult = await p.multiselect({\n message:\n 'Skills · space = 토글 · a = 전체 · enter = 확인 (default ALL)',\n options: allSkills.map((s) => ({ value: s, label: s, hint: 'omd skill' })),\n initialValues: allSkills,\n required: true,\n });\n if (p.isCancel(skillResult)) {\n p.cancel('Install cancelled.');\n return 130;\n }\n skills = skillResult as string[];\n\n // 2. Sub-agents (default = ALL selected)\n if (allAgents.length > 0) {\n const agentResult = await p.multiselect({\n message:\n 'Sub-agents · space = 토글 · a = 전체 · enter = 확인 (default ALL)',\n options: allAgents.map((a) => ({\n value: a,\n label: a.replace(/\\.md$/, ''),\n hint: 'subagent',\n })),\n initialValues: allAgents,\n required: false,\n });\n if (p.isCancel(agentResult)) {\n p.cancel('Install cancelled.');\n return 130;\n }\n canonicalAgents = agentResult as string[];\n } else {\n canonicalAgents = [];\n }\n\n // 3. Agent channels (default = NONE — user explicitly picks)\n if (opts.agents) {\n targets = opts.agents;\n } else {\n const targetResult = await p.multiselect({\n message:\n 'Agent channels · space = 토글 · enter = 확인 · 최소 1개 선택',\n options: [\n {\n value: 'claude-code',\n label: 'Claude Code',\n hint: actuallyDetected.includes('claude-code') ? '.claude/ detected' : '',\n },\n {\n value: 'codex',\n label: 'Codex',\n hint: actuallyDetected.includes('codex') ? '.codex/ detected' : '',\n },\n {\n value: 'opencode',\n label: 'OpenCode',\n hint: actuallyDetected.includes('opencode') ? '.opencode/ detected' : '',\n },\n ] as { value: SkillTarget; label: string; hint?: string }[],\n initialValues: [] as SkillTarget[],\n required: true,\n });\n if (p.isCancel(targetResult)) {\n p.cancel('Install cancelled.');\n return 130;\n }\n targets = targetResult as SkillTarget[];\n }\n }\n\n const plans = targets.map((t) => planForTarget(projectRoot, t));\n\n p.log.message(\n pc.bold(`Skills (${skills.length}): `) +\n skills.map((s) => pc.cyan(s)).join(', ')\n );\n if (canonicalAgents.length > 0) {\n p.log.message(\n pc.bold(`Agents (${canonicalAgents.length}): `) +\n canonicalAgents.map((a) => pc.cyan(a.replace(/\\.md$/, ''))).join(', ')\n );\n }\n p.log.message(\n pc.bold('Targets: ') + targets.map((t) => pc.cyan(t)).join(', ')\n );\n\n const results: InstallResult[] = [];\n for (const plan of plans) {\n for (const skill of skills) {\n results.push(installOne(packageRoot, plan, skill, force));\n }\n }\n\n // Generate per-channel sub-agent definitions from the canonical `agents/`.\n // This is the v2 portable source-of-truth pattern (oh-my-agent style).\n // `canonicalAgents` is already resolved above by the TUI / --agents filter.\n for (const target of targets) {\n if (target === 'claude-code') {\n for (const filename of canonicalAgents) {\n results.push(installAgentFile(packageRoot, projectRoot, 'claude', filename, force));\n }\n } else if (target === 'codex') {\n for (const filename of canonicalAgents) {\n results.push(installAgentFile(packageRoot, projectRoot, 'codex', filename, force));\n }\n }\n // OpenCode currently has no agent-definition channel — skills only.\n }\n\n // Ship the read-only data assets (reference fingerprints, controlled vocab,\n // human-readable tag matrix, opt-out corpus) into the project so skills + hooks\n // can run entirely on the host CLI's own model — no external API keys.\n const dataFiles = [\n 'reference-fingerprints.json',\n 'reference-tags.md',\n 'vocabulary.json',\n 'synonyms.json',\n 'opt-out-corpus.json',\n ];\n for (const target of targets) {\n if (target === 'claude-code') {\n for (const dataFile of dataFiles) {\n results.push(installDataFile(packageRoot, projectRoot, '.claude', dataFile, force));\n }\n } else if (target === 'codex') {\n for (const dataFile of dataFiles) {\n results.push(installDataFile(packageRoot, projectRoot, '.codex', dataFile, force));\n }\n }\n }\n\n // Install hooks (Claude Code only — Codex / OpenCode have separate hook systems)\n if (targets.includes('claude-code')) {\n for (const hookFile of [\n 'skill-activation.cjs',\n 'session-state-loader.cjs',\n 'post-edit-watch.cjs',\n 'session-end-foldin.cjs',\n ]) {\n results.push(installHookFile(packageRoot, projectRoot, hookFile, force));\n }\n // settings.json (with merge, never clobber user)\n results.push(installSettingsJson(packageRoot, projectRoot, force));\n }\n\n p.log.message(pc.bold('\\nResults:'));\n for (const r of results) {\n const rel = relative(projectRoot, r.destPath);\n p.log.message(\n ` ${STATUS_LABEL[r.status]} ${pc.dim(r.target.padEnd(12))} ${rel}`\n );\n }\n\n const driftCount = results.filter((r) => r.status === 'skipped-drift').length;\n const writtenCount = results.filter(\n (r) => r.status === 'created' || r.status === 'updated'\n ).length;\n\n if (driftCount > 0) {\n p.outro(\n pc.yellow(\n `${writtenCount} written, ${driftCount} skipped (existing files lack the omd marker — rerun with --force to overwrite).`\n )\n );\n return 0;\n }\n\n // Friendly next-step nudge after successful install\n const nextSteps = [\n `${pc.bold('Open Claude Code (or Codex). Just say what you want:')}`,\n '',\n ` ${pc.dim('\"토스 스타일 가족 식단 공유 앱 메인 화면 디자인해줘\"')}`,\n ` ${pc.dim('\"Linear-clone B2B SaaS dashboard 만들고 싶어\"')}`,\n ` ${pc.dim('\"이 카드 좀 더 세련되게\"')} ${pc.dim('# 작업 중 자연어 — 자동 라우팅')}`,\n '',\n `${pc.bold('Claude가 description 매칭으로 자동 라우팅')} ${pc.dim('— 슬래시 명령 안 쳐도 됨. Hook은 DESIGN.md 부재 시 omd:init 안내만.')}`,\n '',\n `${pc.dim('Power user shortcut: ')}${pc.cyan('/omd-harness <task>')} ${pc.dim('— 즉시 진입.')}`,\n '',\n `${pc.yellow('⚠ Already-running Claude Code session?')} ${pc.dim('Run `/agents` inside the session to reload — or quit (Cmd+Q on macOS) and relaunch. Without reload, hooks/agents do not load.')}`,\n ].join('\\n');\n p.note(nextSteps, 'Next');\n\n p.outro(\n pc.green(\n `Done. 6 skills · 11 sub-agents · 4 hooks installed (${writtenCount} files).`,\n ),\n );\n return 0;\n}\n\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type AgentId = 'claude-code' | 'codex' | 'opencode' | 'cursor' | 'unknown';\n\nexport function detectCallingAgent(): AgentId {\n const env = process.env;\n\n if (env.CLAUDECODE === '1' || env.CLAUDE_CODE === '1' || env.CLAUDE_CODE_TASK_ID) {\n return 'claude-code';\n }\n if (env.CODEX_SESSION_ID || env.CODEX || env.OPENAI_CODEX) {\n return 'codex';\n }\n if (env.OPENCODE || env.OPENCODE_SESSION) {\n return 'opencode';\n }\n if (env.CURSOR_SESSION_ID || env.CURSOR_AGENT) {\n return 'cursor';\n }\n\n return 'unknown';\n}\n\nexport interface AgentPresence {\n claudeCode: boolean;\n codex: boolean;\n opencode: boolean;\n cursor: boolean;\n}\n\nexport function detectInstalledAgents(projectRoot: string): AgentPresence {\n return {\n claudeCode:\n existsSync(join(projectRoot, '.claude')) ||\n existsSync(join(projectRoot, 'CLAUDE.md')),\n codex:\n existsSync(join(projectRoot, '.codex')) ||\n existsSync(join(projectRoot, 'AGENTS.md')) ||\n existsSync(join(projectRoot, 'AGENTS.override.md')),\n opencode:\n existsSync(join(projectRoot, '.opencode')) ||\n existsSync(join(projectRoot, 'opencode.json')) ||\n existsSync(join(projectRoot, 'opencode.jsonc')),\n cursor:\n existsSync(join(projectRoot, '.cursor')) ||\n existsSync(join(projectRoot, '.cursorrules')),\n };\n}\n"],"mappings":"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","names":["existsSync","join","existsSync","join"]}
1
+ {"version":3,"sources":["../src/cli/install-skills.ts","../src/core/agent-detect.ts"],"sourcesContent":["import * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport {\n readFileSync,\n readdirSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n} from 'node:fs';\nimport { join, dirname, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { detectInstalledAgents } from '../core/agent-detect.js';\n\nexport type SkillTarget = 'claude-code' | 'codex' | 'opencode';\n\nexport interface InstallSkillsOptions {\n dir?: string;\n agents?: SkillTarget[];\n force?: boolean;\n /** Non-interactive: install all skills + all agents without TUI prompt.\n * Default false → interactive multiselect when TTY available. */\n all?: boolean;\n /** Pre-select specific skill names from CLI flag (`--skills omd-init,omd-apply`).\n * Overrides interactive prompt when set. */\n skillsFilter?: string[];\n /** Pre-select specific agent names. Overrides interactive prompt when set. */\n agentsFilter?: string[];\n}\n\ninterface InstallPlan {\n target: SkillTarget;\n destDir: string;\n layout: 'folder' | 'flat';\n}\n\nfunction findPackageRoot(): string | null {\n let cur = dirname(fileURLToPath(import.meta.url));\n for (let i = 0; i < 8; i++) {\n if (existsSync(join(cur, 'skills'))) return cur;\n const parent = dirname(cur);\n if (parent === cur) break;\n cur = parent;\n }\n return null;\n}\n\nfunction listShippedSkills(packageRoot: string): string[] {\n const skillsDir = join(packageRoot, 'skills');\n if (!existsSync(skillsDir)) return [];\n return readdirSync(skillsDir)\n .filter((name) => existsSync(join(skillsDir, name, 'SKILL.md')))\n .sort();\n}\n\n/**\n * Canonical agent definitions live at `agents/<name>.md` (markdown with YAML\n * frontmatter). Channel-specific files (.claude/agents/*.md, .codex/agents/*.toml)\n * are generated artifacts — never the source of truth.\n *\n * The package ships only `agents/` and the generator emits per-channel files\n * into the user's project on `omd install-skills`.\n */\nfunction listCanonicalAgents(packageRoot: string): string[] {\n const dir = join(packageRoot, 'agents');\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((name) => name.startsWith('omd-') && name.endsWith('.md'))\n .sort();\n}\n\ninterface ParsedAgent {\n name: string;\n description: string;\n tools: string[];\n model: string;\n body: string;\n}\n\n/** Parse `agents/<name>.md` YAML frontmatter + body into structured form. */\nfunction parseCanonicalAgent(packageRoot: string, filename: string): ParsedAgent {\n const src = readFileSync(join(packageRoot, 'agents', filename), 'utf8');\n const match = /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/.exec(src);\n if (!match) {\n throw new Error(`agents/${filename}: missing YAML frontmatter`);\n }\n const fm = match[1];\n const body = match[2];\n const grab = (key: string): string => {\n const re = new RegExp(`^${key}:\\\\s*(.+)$`, 'm');\n const m = re.exec(fm);\n return m ? m[1].trim().replace(/^[\"']|[\"']$/g, '') : '';\n };\n return {\n name: grab('name') || filename.replace(/\\.md$/, ''),\n description: grab('description'),\n tools: grab('tools')\n .split(',')\n .map((t) => t.trim())\n .filter(Boolean),\n model: grab('model') || 'sonnet',\n body,\n };\n}\n\n/** Map Claude tool names to Codex tool names (best-effort). */\nfunction claudeToolsToCodex(tools: string[]): string[] {\n const m: Record<string, string> = {\n Read: 'read_file',\n Write: 'write_file',\n Edit: 'edit_file',\n Bash: 'shell',\n Glob: 'search',\n Grep: 'search',\n WebFetch: 'web_fetch',\n WebSearch: 'search',\n Agent: 'spawn_agent',\n TaskCreate: 'task',\n TaskUpdate: 'task',\n TaskList: 'task',\n };\n const out = new Set<string>();\n for (const t of tools) out.add(m[t] ?? t.toLowerCase());\n return [...out].sort();\n}\n\n/** Map Claude model alias to Codex/OpenAI model id (best-effort). */\nfunction claudeModelToCodex(model: string): string {\n const m: Record<string, string> = {\n haiku: 'gpt-4.1-mini',\n sonnet: 'gpt-4.1',\n opus: 'gpt-4.1',\n };\n return m[model.toLowerCase()] ?? 'gpt-4.1';\n}\n\n/** Render a canonical agent as a Claude Code subagent file.\n * IMPORTANT: Claude Code's subagent parser requires YAML frontmatter (`---`)\n * as the FIRST line of the file. Any preceding content (HTML comments, blank\n * lines) breaks discovery. So we encode the managed-by-omd marker as a\n * custom frontmatter field (`omd_managed: true`) instead of an HTML comment.\n */\nfunction renderClaudeAgent(a: ParsedAgent): string {\n const fm = [\n '---',\n `name: ${a.name}`,\n `description: ${a.description}`,\n `tools: ${a.tools.join(', ')}`,\n `model: ${a.model}`,\n `omd_managed: true`,\n '---',\n '',\n ].join('\\n');\n return fm + a.body;\n}\n\n/** Render a canonical agent as a Codex TOML file (declarative pointer). */\nfunction renderCodexAgent(a: ParsedAgent): string {\n const tools = claudeToolsToCodex(a.tools);\n const model = claudeModelToCodex(a.model);\n const desc = a.description.replace(/\"/g, '\\\\\"');\n return [\n `[agent]`,\n `name = \"${a.name}\"`,\n `description = \"${desc}\"`,\n `model = \"${model}\"`,\n `max_threads = 1`,\n `allowed_tools = [${tools.map((t) => `\"${t}\"`).join(', ')}]`,\n '',\n `instructions = \"\"\"`,\n `Source of truth: agents/${a.name}.md (canonical). The full role spec is`,\n `mirrored to .claude/agents/${a.name}.md when installed for Claude Code.`,\n `Follow that spec verbatim regardless of channel.`,\n '',\n `Codex notes:`,\n `- Spawn sub-agents via spawn_agent with names matching .codex/agents/<name>.toml`,\n `- Use shell to invoke CLI helpers (omd init prepare, omd remember, git apply, npx axe-core, npx lighthouse)`,\n `- All artifacts go inside .omd/runs/run-<latest>/ (or skills/omd-lab-02-design-harness/runs/<lab-version>-...)`,\n `\"\"\"`,\n '',\n ].join('\\n');\n}\n\nfunction planForTarget(projectRoot: string, target: SkillTarget): InstallPlan {\n switch (target) {\n case 'claude-code':\n return {\n target,\n destDir: join(projectRoot, '.claude', 'skills'),\n layout: 'folder',\n };\n case 'codex':\n return {\n target,\n destDir: join(projectRoot, '.codex', 'skills'),\n layout: 'folder',\n };\n case 'opencode':\n return {\n target,\n destDir: join(projectRoot, '.opencode', 'agents'),\n layout: 'flat',\n };\n }\n}\n\nconst MANAGED_HEADER =\n '<!-- omd:installed-skill — managed by `omd install-skills`. Do not edit; rerun the command to refresh. -->';\n\ninterface InstallResult {\n target: SkillTarget;\n skill: string;\n destPath: string;\n status: 'created' | 'updated' | 'unchanged' | 'skipped-drift';\n}\n\nfunction installOne(\n packageRoot: string,\n plan: InstallPlan,\n skill: string,\n force: boolean\n): InstallResult {\n const src = readFileSync(\n join(packageRoot, 'skills', skill, 'SKILL.md'),\n 'utf8'\n );\n const managed = MANAGED_HEADER + '\\n\\n' + src;\n const destPath =\n plan.layout === 'folder'\n ? join(plan.destDir, skill, 'SKILL.md')\n : join(plan.destDir, skill + '.md');\n\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n if (exists && existing === managed) {\n return { target: plan.target, skill, destPath, status: 'unchanged' };\n }\n\n if (exists && !existing.startsWith(MANAGED_HEADER) && !force) {\n return { target: plan.target, skill, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, managed, 'utf8');\n return {\n target: plan.target,\n skill,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\n/** Install a hook script from package's .claude/hooks/ to project. */\nfunction installHookFile(\n packageRoot: string,\n projectRoot: string,\n filename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = 'claude-code';\n const skillLabel = `hook:${filename}`;\n const srcPath = join(packageRoot, '.claude', 'hooks', filename);\n const destPath = join(projectRoot, '.claude', 'hooks', filename);\n\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src);\n return { target, skill: skillLabel, destPath, status: exists ? 'updated' : 'created' };\n}\n\n/**\n * Install / merge .claude/settings.json. We MERGE hooks (don't clobber user\n * customizations) by checking if the omd-managed `_doc` field is present.\n * Without --force, if a user-edited settings.json exists (no _doc field),\n * we skip with `skipped-drift`.\n */\nfunction installSettingsJson(\n packageRoot: string,\n projectRoot: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = 'claude-code';\n const skillLabel = 'settings:.claude/settings.json';\n const srcPath = join(packageRoot, '.claude', 'settings.json');\n const destPath = join(projectRoot, '.claude', 'settings.json');\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n // Check if it's the omd-managed version\n try {\n const parsed = JSON.parse(existing);\n if (typeof parsed._doc === 'string' && parsed._doc.includes('OmD')) {\n // managed — overwrite\n } else {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n } catch {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n }\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src);\n return { target, skill: skillLabel, destPath, status: exists ? 'updated' : 'created' };\n}\n\n/**\n * Copy a read-only data asset (reference-fingerprints.json, vocabulary.json, …)\n * from the package's `data/` into the project's `.claude/data/` or `.codex/data/`.\n * The skill reads these at runtime — they replace the deprecated `omd init recommend` CLI.\n */\nfunction installDataFile(\n packageRoot: string,\n projectRoot: string,\n channelDir: string,\n dataFilename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = channelDir === '.claude' ? 'claude-code' : 'codex';\n const skillLabel = `data:${dataFilename}`;\n\n const srcPath = join(packageRoot, 'data', dataFilename);\n const destPath = join(projectRoot, channelDir, 'data', dataFilename);\n\n if (!existsSync(srcPath)) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n const src = readFileSync(srcPath, 'utf8');\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n // Data files are pure copies — no managed header (would corrupt JSON).\n if (exists && existing === src) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n if (exists && !force) {\n // Honor user customization unless --force\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, src, 'utf8');\n return {\n target,\n skill: skillLabel,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\n/**\n * Generate a per-channel agent file from the canonical `agents/<name>.md`.\n *\n * Channel = 'claude' → emits `.claude/agents/<name>.md` (markdown w/ frontmatter)\n * Channel = 'codex' → emits `.codex/agents/<name>.toml` (TOML pointer)\n */\nfunction installAgentFile(\n packageRoot: string,\n projectRoot: string,\n channel: 'claude' | 'codex',\n filename: string,\n force: boolean\n): InstallResult {\n const target: SkillTarget = channel === 'claude' ? 'claude-code' : 'codex';\n const skillLabel = `agent:${filename}`;\n\n const parsed = parseCanonicalAgent(packageRoot, filename);\n const rendered =\n channel === 'claude' ? renderClaudeAgent(parsed) : renderCodexAgent(parsed);\n\n const destFilename =\n channel === 'claude' ? filename : filename.replace(/\\.md$/, '.toml');\n const destPath = join(\n projectRoot,\n channel === 'claude' ? '.claude' : '.codex',\n 'agents',\n destFilename\n );\n\n // For Claude Code: managed marker is encoded as `omd_managed: true` INSIDE the\n // frontmatter (rendered above) — no HTML comment can precede `---` or the\n // subagent loader rejects the file.\n // For Codex: TOML allows leading comments, so `# omd:installed-agent ...` is fine.\n const managed =\n channel === 'claude'\n ? rendered\n : '# omd:installed-agent — generated from agents/' +\n filename +\n ' by `omd install-skills`. Do not edit; rerun the command to refresh.\\n\\n' +\n rendered;\n\n const exists = existsSync(destPath);\n const existing = exists ? readFileSync(destPath, 'utf8') : '';\n\n if (exists && existing === managed) {\n return { target, skill: skillLabel, destPath, status: 'unchanged' };\n }\n\n // Drift detection sentinels:\n // Claude — look for `omd_managed: true` line inside frontmatter\n // Codex — look for `# omd:installed-agent` comment\n const isManaged =\n channel === 'claude'\n ? /\\nomd_managed:\\s*true\\b/.test(existing)\n : existing.startsWith('# omd:installed-agent');\n\n if (exists && !isManaged && !force) {\n return { target, skill: skillLabel, destPath, status: 'skipped-drift' };\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, managed, 'utf8');\n return {\n target,\n skill: skillLabel,\n destPath,\n status: exists ? 'updated' : 'created',\n };\n}\n\nconst STATUS_LABEL: Record<InstallResult['status'], string> = {\n created: pc.green('created'),\n updated: pc.cyan('updated'),\n unchanged: pc.dim('unchanged'),\n 'skipped-drift': pc.yellow('skipped'),\n};\n\nfunction autoDetectTargets(projectRoot: string): SkillTarget[] {\n const presence = detectInstalledAgents(projectRoot);\n const targets: SkillTarget[] = [];\n if (presence.claudeCode) targets.push('claude-code');\n if (presence.codex) targets.push('codex');\n if (presence.opencode) targets.push('opencode');\n // Cursor uses .mdc rules, not skills — installed via `omd sync`.\n if (targets.length === 0) {\n // Fallback: install for all three so user gets coverage even without\n // explicit signal. Idempotent so cost is low.\n return ['claude-code', 'codex', 'opencode'];\n }\n return targets;\n}\n\nexport async function runInstallSkills(\n opts: InstallSkillsOptions = {}\n): Promise<number> {\n const projectRoot = opts.dir ?? process.cwd();\n const packageRoot = findPackageRoot();\n if (!packageRoot) {\n console.error(pc.red('omd install-skills: package data not found'));\n return 1;\n }\n\n const allSkills = listShippedSkills(packageRoot);\n if (allSkills.length === 0) {\n console.error(pc.red('omd install-skills: no skills found in package'));\n return 1;\n }\n const allAgents = listCanonicalAgents(packageRoot);\n\n const force = opts.force ?? false;\n\n p.intro(\n pc.bold('omd install-skills') +\n pc.dim(` (${relative(process.cwd(), projectRoot) || '.'})`)\n );\n\n // Resolve selection: --all flag, --skills/--agents/--agent filter, or interactive TUI.\n // TUI runs only when stdin is a TTY and the corresponding flag isn't set.\n const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);\n const nonInteractive = opts.all || !isTTY || opts.skillsFilter || opts.agentsFilter;\n\n const detected = autoDetectTargets(projectRoot);\n // Real presence (not the all-3 fallback) — used purely for hint labels.\n const presence = detectInstalledAgents(projectRoot);\n const actuallyDetected: SkillTarget[] = [\n presence.claudeCode ? 'claude-code' : null,\n presence.codex ? 'codex' : null,\n presence.opencode ? 'opencode' : null,\n ].filter((x): x is SkillTarget => x !== null);\n\n let skills: string[];\n let canonicalAgents: string[];\n let targets: SkillTarget[];\n\n if (nonInteractive) {\n // Non-interactive resolution\n skills = opts.skillsFilter\n ? allSkills.filter((s) => opts.skillsFilter!.includes(s))\n : allSkills;\n canonicalAgents = opts.agentsFilter\n ? allAgents.filter((a) => opts.agentsFilter!.includes(a.replace(/\\.md$/, '')))\n : allAgents;\n targets = opts.agents\n ? opts.agents\n : opts.all\n ? (['claude-code', 'codex', 'opencode'] as SkillTarget[])\n : detected;\n } else {\n // === Interactive TUI — skill → subagent → channel order ===\n // 1. Skills (default = ALL selected)\n const skillResult = await p.multiselect({\n message:\n 'Skills · space = 토글 · a = 전체 · enter = 확인 (default ALL)',\n options: allSkills.map((s) => ({ value: s, label: s, hint: 'omd skill' })),\n initialValues: allSkills,\n required: true,\n });\n if (p.isCancel(skillResult)) {\n p.cancel('Install cancelled.');\n return 130;\n }\n skills = skillResult as string[];\n\n // 2. Sub-agents (default = ALL selected)\n if (allAgents.length > 0) {\n const agentResult = await p.multiselect({\n message:\n 'Sub-agents · space = 토글 · a = 전체 · enter = 확인 (default ALL)',\n options: allAgents.map((a) => ({\n value: a,\n label: a.replace(/\\.md$/, ''),\n hint: 'subagent',\n })),\n initialValues: allAgents,\n required: false,\n });\n if (p.isCancel(agentResult)) {\n p.cancel('Install cancelled.');\n return 130;\n }\n canonicalAgents = agentResult as string[];\n } else {\n canonicalAgents = [];\n }\n\n // 3. Agent channels (default = NONE — user explicitly picks)\n if (opts.agents) {\n targets = opts.agents;\n } else {\n const targetResult = await p.multiselect({\n message:\n 'Agent channels · space = 토글 · enter = 확인 · 최소 1개 선택',\n options: [\n {\n value: 'claude-code',\n label: 'Claude Code',\n hint: actuallyDetected.includes('claude-code') ? '.claude/ detected' : '',\n },\n {\n value: 'codex',\n label: 'Codex',\n hint: actuallyDetected.includes('codex') ? '.codex/ detected' : '',\n },\n {\n value: 'opencode',\n label: 'OpenCode',\n hint: actuallyDetected.includes('opencode') ? '.opencode/ detected' : '',\n },\n ] as { value: SkillTarget; label: string; hint?: string }[],\n initialValues: [] as SkillTarget[],\n required: true,\n });\n if (p.isCancel(targetResult)) {\n p.cancel('Install cancelled.');\n return 130;\n }\n targets = targetResult as SkillTarget[];\n }\n }\n\n const plans = targets.map((t) => planForTarget(projectRoot, t));\n\n p.log.message(\n pc.bold(`Skills (${skills.length}): `) +\n skills.map((s) => pc.cyan(s)).join(', ')\n );\n if (canonicalAgents.length > 0) {\n p.log.message(\n pc.bold(`Agents (${canonicalAgents.length}): `) +\n canonicalAgents.map((a) => pc.cyan(a.replace(/\\.md$/, ''))).join(', ')\n );\n }\n p.log.message(\n pc.bold('Targets: ') + targets.map((t) => pc.cyan(t)).join(', ')\n );\n\n const results: InstallResult[] = [];\n for (const plan of plans) {\n for (const skill of skills) {\n results.push(installOne(packageRoot, plan, skill, force));\n }\n }\n\n // Generate per-channel sub-agent definitions from the canonical `agents/`.\n // This is the v2 portable source-of-truth pattern (oh-my-agent style).\n // `canonicalAgents` is already resolved above by the TUI / --agents filter.\n for (const target of targets) {\n if (target === 'claude-code') {\n for (const filename of canonicalAgents) {\n results.push(installAgentFile(packageRoot, projectRoot, 'claude', filename, force));\n }\n } else if (target === 'codex') {\n for (const filename of canonicalAgents) {\n results.push(installAgentFile(packageRoot, projectRoot, 'codex', filename, force));\n }\n }\n // OpenCode currently has no agent-definition channel — skills only.\n }\n\n // Ship the read-only data assets (reference fingerprints, controlled vocab,\n // human-readable tag matrix, opt-out corpus) into the project so skills + hooks\n // can run entirely on the host CLI's own model — no external API keys.\n const dataFiles = [\n 'reference-fingerprints.json',\n 'reference-tags.md',\n 'vocabulary.json',\n 'synonyms.json',\n 'opt-out-corpus.json',\n ];\n for (const target of targets) {\n if (target === 'claude-code') {\n for (const dataFile of dataFiles) {\n results.push(installDataFile(packageRoot, projectRoot, '.claude', dataFile, force));\n }\n } else if (target === 'codex') {\n for (const dataFile of dataFiles) {\n results.push(installDataFile(packageRoot, projectRoot, '.codex', dataFile, force));\n }\n }\n }\n\n // Install hooks (Claude Code only — Codex / OpenCode have separate hook systems)\n if (targets.includes('claude-code')) {\n for (const hookFile of [\n 'skill-activation.cjs',\n 'session-state-loader.cjs',\n 'post-edit-watch.cjs',\n 'session-end-foldin.cjs',\n ]) {\n results.push(installHookFile(packageRoot, projectRoot, hookFile, force));\n }\n // settings.json (with merge, never clobber user)\n results.push(installSettingsJson(packageRoot, projectRoot, force));\n }\n\n p.log.message(pc.bold('\\nResults:'));\n for (const r of results) {\n const rel = relative(projectRoot, r.destPath);\n p.log.message(\n ` ${STATUS_LABEL[r.status]} ${pc.dim(r.target.padEnd(12))} ${rel}`\n );\n }\n\n const driftCount = results.filter((r) => r.status === 'skipped-drift').length;\n const writtenCount = results.filter(\n (r) => r.status === 'created' || r.status === 'updated'\n ).length;\n\n if (driftCount > 0) {\n p.outro(\n pc.yellow(\n `${writtenCount} written, ${driftCount} skipped (existing files lack the omd marker — rerun with --force to overwrite).`\n )\n );\n return 0;\n }\n\n // Friendly next-step nudge after successful install.\n // The first prompt is kept identical to the README's \"Your first 60 seconds\"\n // block so the README, the terminal, and the postinstall message all teach\n // the same activation moment. Bilingual (EN + KR) so an English reader is not\n // handed a Korean-only outro.\n const nextSteps = [\n `${pc.bold('Restart your agent, then type your first prompt:')}`,\n '',\n ` ${pc.cyan('EN')} ${pc.dim('Set up our design system — Toss-style, for a family meal-tracking app.')}`,\n ` ${pc.cyan('KR')} ${pc.dim('토스 스타일로 가족 식단 공유 앱 디자인 시스템 잡아줘')}`,\n '',\n `${pc.dim('Your agent runs omd:init and writes DESIGN.md. Then build against it:')}`,\n ` ${pc.cyan('EN')} ${pc.dim('Design the home screen.')} ${pc.cyan('KR')} ${pc.dim('홈 화면 디자인해줘')}`,\n '',\n `${pc.dim('Full walkthrough → \"Your first 60 seconds\" in the README. Routing is automatic — no slash command needed.')}`,\n `${pc.dim('Power user: ')}${pc.cyan('/omd-harness <task>')}${pc.dim(' — jump straight into the pipeline.')}`,\n '',\n `${pc.yellow('⚠ Already-running session?')} ${pc.dim('Run `/agents` to reload — or quit (Cmd+Q on macOS) and relaunch. Without reload, hooks/agents do not load.')}`,\n ].join('\\n');\n p.note(nextSteps, 'Next');\n\n // Counts derived from what was actually resolved/installed — never hardcoded,\n // so the outro can't drift from the real skill/agent/hook set (or the README).\n const hookCount = targets.includes('claude-code') ? 4 : 0;\n p.outro(\n pc.green(\n `Done. ${skills.length} skills · ${canonicalAgents.length} sub-agents · ${hookCount} hooks installed (${writtenCount} files).`,\n ),\n );\n return 0;\n}\n\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type AgentId = 'claude-code' | 'codex' | 'opencode' | 'cursor' | 'unknown';\n\nexport function detectCallingAgent(): AgentId {\n const env = process.env;\n\n if (env.CLAUDECODE === '1' || env.CLAUDE_CODE === '1' || env.CLAUDE_CODE_TASK_ID) {\n return 'claude-code';\n }\n if (env.CODEX_SESSION_ID || env.CODEX || env.OPENAI_CODEX) {\n return 'codex';\n }\n if (env.OPENCODE || env.OPENCODE_SESSION) {\n return 'opencode';\n }\n if (env.CURSOR_SESSION_ID || env.CURSOR_AGENT) {\n return 'cursor';\n }\n\n return 'unknown';\n}\n\nexport interface AgentPresence {\n claudeCode: boolean;\n codex: boolean;\n opencode: boolean;\n cursor: boolean;\n}\n\nexport function detectInstalledAgents(projectRoot: string): AgentPresence {\n return {\n claudeCode:\n existsSync(join(projectRoot, '.claude')) ||\n existsSync(join(projectRoot, 'CLAUDE.md')),\n codex:\n existsSync(join(projectRoot, '.codex')) ||\n existsSync(join(projectRoot, 'AGENTS.md')) ||\n existsSync(join(projectRoot, 'AGENTS.override.md')),\n opencode:\n existsSync(join(projectRoot, '.opencode')) ||\n existsSync(join(projectRoot, 'opencode.json')) ||\n existsSync(join(projectRoot, 'opencode.jsonc')),\n cursor:\n existsSync(join(projectRoot, '.cursor')) ||\n existsSync(join(projectRoot, '.cursorrules')),\n };\n}\n"],"mappings":"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","names":["existsSync","join","existsSync","join"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-design-cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "description": "Bootstrap oh-my-design skills + agents into your project. After install, talk to your AI coding agent in natural language — no other CLI commands.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,13 +25,18 @@
25
25
  "skills/omd-final-qa",
26
26
  "skills/omd-codex-image",
27
27
  "agents",
28
- "data",
28
+ "data/opt-out-corpus.json",
29
+ "data/synonyms.json",
30
+ "data/vocabulary.json",
31
+ "data/reference-fingerprints.json",
32
+ "data/reference-tags.md",
29
33
  "web/references/*/DESIGN.md",
30
34
  ".claude/hooks/*.cjs",
31
35
  ".claude/settings.json",
32
36
  "AGENTS.md",
33
37
  "scripts/postinstall.cjs",
34
- "scripts/context.cjs"
38
+ "scripts/context.cjs",
39
+ "scripts/ctx-prime.cjs"
35
40
  ],
36
41
  "scripts": {
37
42
  "build": "tsup",
@@ -41,7 +46,8 @@
41
46
  "test:watch": "vitest",
42
47
  "lint": "tsc --noEmit",
43
48
  "prepare": "npm run build",
44
- "prepublishOnly": "npm run build",
49
+ "prepublishOnly": "npm run build && node scripts/gen-llms-full.cjs && bash scripts/check-release-hygiene.sh pack",
50
+ "gen:llms-full": "node scripts/gen-llms-full.cjs",
45
51
  "postinstall": "node scripts/postinstall.cjs"
46
52
  },
47
53
  "keywords": [
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ // scripts/ctx-prime.cjs — richer, run-scoped repo prime for omd-harness 1.6+.
3
+ //
4
+ // Pure node, no deps. Sub-5s on typical repos (<5k files). Emits
5
+ // <RUN_DIR>/ctx-prime.json with:
6
+ // stack / brand_signal / surface_inventory / audience_hypothesis /
7
+ // wow_moment_candidates / scanned_at / scan_duration_ms
8
+ //
9
+ // Usage: node scripts/ctx-prime.cjs <cwd> <run_dir>
10
+ // Falls back to cwd/.omd if run_dir omitted (for ad-hoc use).
11
+
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const startedAt = Date.now();
16
+ const cwd = path.resolve(process.argv[2] || process.cwd());
17
+ const runDir = process.argv[3]
18
+ ? path.resolve(process.argv[3])
19
+ : path.join(cwd, '.omd');
20
+ const outFile = path.join(runDir, 'ctx-prime.json');
21
+
22
+ const SKIP_DIRS = new Set([
23
+ 'node_modules', 'dist', 'build', '.next', '.nuxt', '.output',
24
+ 'coverage', '.git', '.cache', '.turbo', '.vercel', '.svelte-kit',
25
+ 'out', 'storybook-static', '__pycache__',
26
+ ]);
27
+
28
+ function safeRead(p) {
29
+ try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
30
+ }
31
+
32
+ function walk(dir, opts) {
33
+ const { exts, max = 200, maxDepth = 6 } = opts;
34
+ const results = [];
35
+ (function rec(d, depth) {
36
+ if (depth > maxDepth || results.length >= max) return;
37
+ let entries;
38
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); }
39
+ catch { return; }
40
+ for (const e of entries) {
41
+ if (e.name.startsWith('.') && e.name !== '.well-known') continue;
42
+ if (SKIP_DIRS.has(e.name)) continue;
43
+ const full = path.join(d, e.name);
44
+ if (e.isDirectory()) { rec(full, depth + 1); continue; }
45
+ if (!exts || exts.some((x) => e.name.endsWith(x))) {
46
+ try {
47
+ const stat = fs.statSync(full);
48
+ results.push({ path: path.relative(cwd, full), size: stat.size });
49
+ if (results.length >= max) return;
50
+ } catch {}
51
+ }
52
+ }
53
+ })(dir, 0);
54
+ return results;
55
+ }
56
+
57
+ // --- 1. stack ---
58
+ const pkgRaw = safeRead(path.join(cwd, 'package.json'));
59
+ const pkg = pkgRaw ? safeJson(pkgRaw) : null;
60
+ const deps = pkg ? Object.assign({}, pkg.dependencies, pkg.devDependencies) : {};
61
+
62
+ function safeJson(s) { try { return JSON.parse(s); } catch { return null; } }
63
+
64
+ const stack = {
65
+ framework: 'unknown',
66
+ kind: pkg ? 'node' : (fs.existsSync(path.join(cwd, 'index.html')) ? 'static-html' : 'unknown'),
67
+ ts: !!safeRead(path.join(cwd, 'tsconfig.json')),
68
+ tailwind: !!deps['tailwindcss'] || !!safeRead(path.join(cwd, 'tailwind.config.js')) || !!safeRead(path.join(cwd, 'tailwind.config.ts')),
69
+ has_shadcn: !!deps['@shadcn/ui'] || !!deps['shadcn-ui'],
70
+ has_charts: !!(deps['recharts'] || deps['chart.js'] || deps['d3'] || deps['visx']),
71
+ has_motion: !!(deps['framer-motion'] || deps['motion']),
72
+ };
73
+ if (deps['next']) stack.framework = 'next';
74
+ else if (deps['nuxt']) stack.framework = 'nuxt';
75
+ else if (deps['vite']) stack.framework = 'vite';
76
+ else if (deps['react']) stack.framework = 'react';
77
+ else if (deps['vue']) stack.framework = 'vue';
78
+ else if (deps['svelte'] || deps['@sveltejs/kit']) stack.framework = 'svelte';
79
+ else if (deps['solid-js']) stack.framework = 'solid';
80
+ else if (deps['astro']) stack.framework = 'astro';
81
+
82
+ // --- 2. brand signal (color + font + voice + language) ---
83
+ const cssLike = walk(cwd, { exts: ['.css', '.scss', '.tsx', '.jsx', '.vue', '.svelte', '.html'], max: 80 });
84
+
85
+ const hexCounts = new Map();
86
+ const fontFamilies = new Set();
87
+ let sampledChars = 0;
88
+ const SAMPLE_BUDGET = 600_000; // bytes
89
+
90
+ for (const f of cssLike) {
91
+ if (sampledChars > SAMPLE_BUDGET) break;
92
+ const content = safeRead(path.join(cwd, f.path));
93
+ if (!content) continue;
94
+ sampledChars += content.length;
95
+ // hex colors
96
+ const hexes = content.match(/#[0-9a-fA-F]{6}\b/g) || [];
97
+ for (const h of hexes) {
98
+ const norm = h.toLowerCase();
99
+ if (norm === '#ffffff' || norm === '#000000') {
100
+ hexCounts.set(norm, (hexCounts.get(norm) || 0) + 1);
101
+ continue;
102
+ }
103
+ hexCounts.set(norm, (hexCounts.get(norm) || 0) + 1);
104
+ }
105
+ // font-family declarations
106
+ const fams = content.match(/font-family\s*:\s*[^;]+/g) || [];
107
+ for (const decl of fams) {
108
+ const inside = decl.split(':').slice(1).join(':');
109
+ const parts = inside.split(',').map((s) => s.trim().replace(/['"]/g, '').replace(/;.*/, ''));
110
+ for (const p of parts.slice(0, 2)) {
111
+ if (p && p.length < 40 && !p.startsWith('var(') && !/^(sans-serif|serif|monospace|system-ui|-apple-system|inherit|initial)$/i.test(p)) {
112
+ fontFamilies.add(p);
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // dominant color: highest count excluding pure white/black
119
+ const sortedHex = [...hexCounts.entries()]
120
+ .filter(([h]) => h !== '#ffffff' && h !== '#000000')
121
+ .sort((a, b) => b[1] - a[1]);
122
+ const dominantColor = sortedHex[0]?.[0] || (hexCounts.get('#ffffff') ? '#ffffff' : null);
123
+ const secondaryColors = sortedHex.slice(1, 4).map(([h]) => h);
124
+
125
+ // language detection (README + commit msgs ratio of hangul vs ascii)
126
+ const readmeContent = safeRead(path.join(cwd, 'README.md')) || safeRead(path.join(cwd, 'README')) || '';
127
+ let language = 'en';
128
+ if (readmeContent) {
129
+ const hangul = (readmeContent.match(/[가-힣]/g) || []).length;
130
+ const ascii = (readmeContent.match(/[a-zA-Z]/g) || []).length;
131
+ if (hangul > ascii * 0.3) language = 'ko';
132
+ else if ((readmeContent.match(/[ぁ-んァ-ヶ一-龯]/g) || []).length > ascii * 0.3) language = 'ja';
133
+ }
134
+
135
+ // voice keywords (heuristic — categorize content keywords)
136
+ const voiceKeywords = [];
137
+ const lowerReadme = readmeContent.toLowerCase();
138
+ if (/data|metrics|dashboard|chart|table|kpi/.test(lowerReadme)) voiceKeywords.push('data-dense');
139
+ if (/fintech|finance|invest|trading|bank|pay/.test(lowerReadme)) voiceKeywords.push('fintech');
140
+ if (/ai|llm|gpt|claude|agent/.test(lowerReadme)) voiceKeywords.push('ai-tools');
141
+ if (/community|social|share|connect/.test(lowerReadme)) voiceKeywords.push('community');
142
+ if (/dev|developer|api|sdk|cli/.test(lowerReadme)) voiceKeywords.push('developer-tools');
143
+ if (/health|wellness|fitness|medical/.test(lowerReadme)) voiceKeywords.push('health');
144
+ if (/commerce|shop|store|product|buy/.test(lowerReadme)) voiceKeywords.push('commerce');
145
+ if (/design|brand|aesthetic|visual/.test(lowerReadme)) voiceKeywords.push('design-tools');
146
+ if (voiceKeywords.length === 0) voiceKeywords.push('general');
147
+
148
+ const brand_signal = {
149
+ dominant_color_hex: dominantColor,
150
+ secondary_colors: secondaryColors,
151
+ font_families: [...fontFamilies].slice(0, 5),
152
+ voice_keywords: voiceKeywords.slice(0, 4),
153
+ language,
154
+ };
155
+
156
+ // --- 3. surface inventory (dedicated wider walk) ---
157
+ function classifySurface(p) {
158
+ const lc = p.toLowerCase();
159
+ const seg = lc.split('/');
160
+ // route segment heuristic — look at parent dir name
161
+ const parent = seg[seg.length - 2] || '';
162
+ if (/builder|editor|create|new|compose/.test(parent)) return 'product';
163
+ if (/dashboard|console|admin|manage/.test(parent)) return 'dashboard';
164
+ if (/doc|guide|help|reference/.test(parent)) return 'docs';
165
+ if (/onboard|signup|signin|login|auth/.test(parent)) return 'onboarding';
166
+ if (/about|contact|company|team|marketing/.test(parent)) return 'marketing';
167
+ if (/setting|profile|account/.test(parent)) return 'settings';
168
+ if (/playground|sandbox|test|qa-/.test(parent)) return 'playground';
169
+ // top-level page = landing
170
+ if (/page\.(tsx|jsx|vue|svelte)$|index\.(tsx|jsx|vue|svelte)$|App\.(tsx|jsx|vue|svelte)$/.test(lc) &&
171
+ seg.length <= 4) return 'landing';
172
+ return 'other';
173
+ }
174
+
175
+ const routeFiles = walk(cwd, { exts: ['.tsx', '.jsx', '.vue', '.svelte'], max: 400, maxDepth: 8 })
176
+ .filter((f) => /page\.(tsx|jsx|vue|svelte)$|index\.(tsx|jsx|vue|svelte)$|App\.(tsx|jsx|vue|svelte)$|\/(pages|routes)\//.test(f.path));
177
+ const pageFiles = routeFiles
178
+ .slice(0, 30)
179
+ .map((f) => ({ path: f.path, kind: classifySurface(f.path), size_kb: +(f.size / 1024).toFixed(1) }));
180
+
181
+ // --- 4. audience hypothesis ---
182
+ const audience_hypothesis = [];
183
+ const hasMarketingCopy = pageFiles.some((p) => p.kind === 'landing' || p.kind === 'marketing');
184
+ const hasInternalSurfaces = pageFiles.some((p) => p.kind === 'product' || p.kind === 'dashboard');
185
+ const readmeMentionsVisitors = /visitor|customer|user|investor|vc|prospect|signup|conversion/i.test(readmeContent);
186
+
187
+ if (readmeMentionsVisitors || hasMarketingCopy) {
188
+ audience_hypothesis.push({
189
+ label: '외부 트래픽 — SEO/conversion 우선, 톤 일탈 허용',
190
+ confidence: 0.5,
191
+ evidence: hasMarketingCopy ? 'landing/marketing surface 존재' : 'README가 외부 사용자 언급',
192
+ });
193
+ }
194
+ if (hasInternalSurfaces) {
195
+ audience_hypothesis.push({
196
+ label: '기존 코드베이스 사용자 — 시각 일관성 우선',
197
+ confidence: 0.35,
198
+ evidence: `product/dashboard surface ${pageFiles.filter((p) => p.kind === 'product' || p.kind === 'dashboard').length}개`,
199
+ });
200
+ }
201
+ if (audience_hypothesis.length === 0) {
202
+ audience_hypothesis.push({
203
+ label: '신규 사용자 — 첫인상 우선 (greenfield 추정)',
204
+ confidence: 0.6,
205
+ evidence: 'surface 인벤토리 비어있음',
206
+ });
207
+ }
208
+ if (audience_hypothesis.length === 2) {
209
+ audience_hypothesis.push({
210
+ label: '둘 다 — 모듈러 컴포넌트로 분기',
211
+ confidence: 0.15,
212
+ evidence: 'fallback',
213
+ });
214
+ }
215
+ // normalize confidence
216
+ const sum = audience_hypothesis.reduce((a, b) => a + b.confidence, 0) || 1;
217
+ for (const h of audience_hypothesis) h.confidence = +(h.confidence / sum).toFixed(2);
218
+ audience_hypothesis.sort((a, b) => b.confidence - a.confidence);
219
+
220
+ // --- 5. wow moment candidates ---
221
+ const wow_moment_candidates = [];
222
+ if (pageFiles.length >= 3) {
223
+ wow_moment_candidates.push({
224
+ label: `기존 ${pageFiles.length}-surface 통합 nav / 시각 일관성`,
225
+ evidence: pageFiles.slice(0, 3).map((p) => p.path).join(', '),
226
+ });
227
+ }
228
+ if (dominantColor && dominantColor !== '#ffffff') {
229
+ wow_moment_candidates.push({
230
+ label: `${dominantColor} 브랜드 컬러 + ${stack.framework} 모던 스택`,
231
+ evidence: `dominant hex ${dominantColor} (${sortedHex[0]?.[1] || 0} occurrences)`,
232
+ });
233
+ }
234
+ if (stack.has_charts) {
235
+ wow_moment_candidates.push({
236
+ label: '데이터 시각화 hero (이미 charts 의존성 보유)',
237
+ evidence: 'recharts/d3/chart.js detected',
238
+ });
239
+ }
240
+ if (brand_signal.voice_keywords.includes('fintech')) {
241
+ wow_moment_candidates.push({ label: '핀테크 advisor 톤 — 숫자 강조 hero', evidence: 'voice_keywords includes fintech' });
242
+ }
243
+ if (brand_signal.voice_keywords.includes('ai-tools')) {
244
+ wow_moment_candidates.push({ label: 'AI-first center-text hero (Anthropic/Vercel 패턴)', evidence: 'voice_keywords includes ai-tools' });
245
+ }
246
+ if (wow_moment_candidates.length === 0) {
247
+ wow_moment_candidates.push({ label: '깔끔한 minimal hero — first impression', evidence: 'greenfield default' });
248
+ }
249
+
250
+ // --- emit ---
251
+ const ctx = {
252
+ version: '1.6.0',
253
+ cwd,
254
+ package_name: pkg?.name ?? null,
255
+ stack,
256
+ brand_signal,
257
+ surface_inventory: pageFiles,
258
+ audience_hypothesis,
259
+ wow_moment_candidates: wow_moment_candidates.slice(0, 5),
260
+ scanned_at: new Date().toISOString(),
261
+ scan_duration_ms: Date.now() - startedAt,
262
+ };
263
+
264
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
265
+ fs.writeFileSync(outFile, JSON.stringify(ctx, null, 2), 'utf8');
266
+ process.stdout.write(outFile + '\n');
@@ -36,15 +36,15 @@ const lines = [
36
36
  ` ${c.cyan('cd')} ${c.dim('<your project>')}`,
37
37
  ` ${c.cyan('omd install-skills')} ${c.dim('# one time per project — installs the design harness')}`,
38
38
  '',
39
- ` ${c.bold('Then open Claude Code (or Codex). Just say what you want:')}`,
39
+ ` ${c.bold('Then restart your agent and type your first prompt:')}`,
40
40
  '',
41
- ` ${c.dim('"토스 스타일 가족 식단 공유 메인 화면 디자인해줘"')}`,
42
- ` ${c.dim('"Linear-clone B2B SaaS dashboard 만들고 싶어"')}`,
43
- ` ${c.dim('"이 카드 좀 더 세련되게"')}`,
41
+ ` ${c.cyan('EN')} ${c.dim('Set up our design system Toss-style, for a family meal-tracking app.')}`,
42
+ ` ${c.cyan('KR')} ${c.dim('토스 스타일로 가족 식단 공유 앱 디자인 시스템 잡아줘')}`,
44
43
  '',
45
- ` ${c.bold('하네스가 알아서 호출됨')} ${c.dim(' Claude Code hook이 디자인 요청 감지해서 자동 라우팅. 별도 슬래시 안 쳐도 됩니다.')}`,
44
+ ` ${c.dim('Your agent runs omd:init and writes DESIGN.md. Then: "Design the home screen."')}`,
45
+ ` ${c.dim('Full walkthrough → "Your first 60 seconds" in the README. Routing is automatic.')}`,
46
46
  '',
47
- ` ${c.dim('Power user shortcut: ')} ${c.cyan('/omd-harness <task>')} ${c.dim('— hook 우회하고 즉시 진입.')}`,
47
+ ` ${c.dim('Power user shortcut: ')} ${c.cyan('/omd-harness <task>')} ${c.dim('— jump straight into the pipeline.')}`,
48
48
  '',
49
49
  ` ${c.bold('⚠ Already running Claude Code?')} ${c.dim('Quit (Cmd+Q on macOS) and relaunch — subagents only load at session start.')}`,
50
50
  '',