oh-my-design-cli 1.6.1 → 1.6.3
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.
- package/README.ko.md +14 -0
- package/README.md +16 -0
- package/data/reference-fingerprints.json +979 -402
- package/dist/bin/oh-my-design.js +5 -3
- package/dist/bin/oh-my-design.js.map +1 -1
- package/dist/{install-skills-UKEVE3KT.js → install-skills-52LCRBZZ.js} +125 -40
- package/dist/install-skills-52LCRBZZ.js.map +1 -0
- package/package.json +2 -1
- package/skills/claude-design/SKILL.md +385 -0
- package/skills/claude-design/references/claude-design-flow.md +425 -0
- package/skills/claude-design/references/codebase-analysis.md +373 -0
- package/skills/claude-design/scripts/analyze_codebase.py +1369 -0
- package/skills/claude-design/scripts/clickable_link.sh +48 -0
- package/skills/claude-design/scripts/collect_source.py +178 -0
- package/skills/claude-design/scripts/drive_claude_design.cjs +378 -0
- package/skills/claude-design/scripts/gather_references.py +437 -0
- package/web/references/bunjang/DESIGN.md +1 -1
- package/web/references/classting/DESIGN.md +251 -0
- package/web/references/coinone/DESIGN.md +218 -0
- package/web/references/devsisters/DESIGN.md +253 -0
- package/web/references/drnow/DESIGN.md +331 -0
- package/web/references/flo/DESIGN.md +306 -0
- package/web/references/fugle/DESIGN.md +250 -0
- package/web/references/gogolook/DESIGN.md +5 -0
- package/web/references/grip/DESIGN.md +250 -0
- package/web/references/hogangnono/DESIGN.md +308 -0
- package/web/references/hyundaicard/DESIGN.md +5 -0
- package/web/references/jkopay/DESIGN.md +249 -0
- package/web/references/jobkorea/DESIGN.md +310 -0
- package/web/references/krafton/DESIGN.md +230 -0
- package/web/references/laftel/DESIGN.md +253 -0
- package/web/references/lezhin/DESIGN.md +301 -0
- package/web/references/momoshop/DESIGN.md +279 -0
- package/web/references/mustit/DESIGN.md +282 -0
- package/web/references/payco/DESIGN.md +227 -0
- package/web/references/piccollage/DESIGN.md +277 -0
- package/web/references/riiid/DESIGN.md +228 -0
- package/web/references/trenbe/DESIGN.md +252 -0
- package/web/references/voicetube/DESIGN.md +227 -0
- package/dist/install-skills-UKEVE3KT.js.map +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: trenbe
|
|
3
|
+
name: "Trenbe"
|
|
4
|
+
country: KR
|
|
5
|
+
category: ecommerce
|
|
6
|
+
homepage: "https://www.trenbe.com"
|
|
7
|
+
primary_color: "#7620F6"
|
|
8
|
+
logo:
|
|
9
|
+
type: favicon
|
|
10
|
+
slug: "https://www.google.com/s2/favicons?domain=trenbe.com&sz=256"
|
|
11
|
+
verified: "2026-06-03"
|
|
12
|
+
omd: "0.1"
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Trenbe
|
|
16
|
+
|
|
17
|
+
Global No.1 luxury shopping platform from Korea, combining tech-driven authentication and real-time global price scanning with a purple-accented monochrome design language.
|
|
18
|
+
|
|
19
|
+
## 1. Visual Theme & Atmosphere
|
|
20
|
+
|
|
21
|
+
Trenbe's interface balances the accessibility of mass-market ecommerce with the calm restraint expected of a luxury goods destination. The canvas is predominantly white with a warm near-black text system and deliberate use of Trenbe's signature vivid purple (#7620F6) as the sole accent color, signaling trust, exclusivity, and forward-facing technology. Product photography is given maximum breathing room within a tight typographic grid; decorative ornamentation is avoided in favor of clean rule lines and generous whitespace. On marketing surfaces the purple can appear in gradients and saturated CTAs, while within the shopping UI it is reserved for interactive states, labels, and primary links — preventing visual noise against high-resolution luxury imagery. The result feels simultaneously confident and approachable: global in aspiration, Korean in precision.
|
|
22
|
+
|
|
23
|
+
## 2. Color Palette & Roles
|
|
24
|
+
|
|
25
|
+
- **Purple (Primary):** `#7620F6` — brand primary; maps to `--primary` and `--purple` across all Trenbe sub-domains; used for interactive labels, uiPrimary text, hover states on marketing CTAs
|
|
26
|
+
- **Purple 500 (UI Primary):** `#7351EC` — semantic `uiPrimary` / `textPrimary` token in app DS v4; active focus rings, highlight text
|
|
27
|
+
- **Black:** `#000000` — primary action button background (btn-primary, btn-black); high-emphasis foreground
|
|
28
|
+
- **White:** `#FFFFFF` — page canvas, card backgrounds, field01 input surface; theme-color meta
|
|
29
|
+
- **Gray 1000 (text01):** `#2F2E2B` — body text, headings, highest-contrast foreground
|
|
30
|
+
- **Gray 900 (text02):** `#4F4E4B` — secondary body text, descriptions
|
|
31
|
+
- **Gray 800 (text03):** `#6F6E6B` — tertiary text, subheadings
|
|
32
|
+
- **Gray 400 (border):** `#CFCECB` — hairline dividers, card borders
|
|
33
|
+
- **Gray 100 (surface):** `#F7F6F5` — page background, input field02 fill
|
|
34
|
+
- **Red 500 (Secondary):** `#EC5151` — semantic `uiSecondary` / `textSecondary`; sale badges, error states, price emphasis
|
|
35
|
+
- **Green 500 (Success):** `#1EB789` — success states, confirmation labels
|
|
36
|
+
- **Yellow 500 (Caution):** `#FFAB1E` — caution / promotional badges
|
|
37
|
+
|
|
38
|
+
## 3. Typography Rules
|
|
39
|
+
|
|
40
|
+
- **Primary font:** Pretendard (weights 100–900 loaded via subset WOFF2), fallback: "Apple SD Gothic Neo", "Helvetica Neue", sans-serif
|
|
41
|
+
- **Feature settings:** `"ss03"`, `"cv01"` enabled on mobile — applied via `@media (max-width: 768px)` on `#app`
|
|
42
|
+
- **Type scale (v4 rem-based, base 10px root):**
|
|
43
|
+
- overline: 1.0rem / line-height 1.8rem
|
|
44
|
+
- caption: 1.2rem / 2.0rem
|
|
45
|
+
- body: 1.4rem / 2.2rem
|
|
46
|
+
- headline: 1.6rem / 2.4rem
|
|
47
|
+
- title: 1.8rem / 2.6rem
|
|
48
|
+
- display03: 2.0rem / 2.8rem
|
|
49
|
+
- display02: 2.4rem / 3.2rem
|
|
50
|
+
- display01: 3.2rem / 4.2rem
|
|
51
|
+
- **Legacy CSS (global fallback):** body font-size 13px, line-height 1.2, color `#444444`
|
|
52
|
+
- **Weight usage:** 400 for body, 500 for subheadings, 700 for CTAs and prices, 600 for section titles
|
|
53
|
+
|
|
54
|
+
## 4. Component Stylings
|
|
55
|
+
|
|
56
|
+
### Buttons
|
|
57
|
+
|
|
58
|
+
**Primary (Black CTA)**
|
|
59
|
+
- Background: `#000000`
|
|
60
|
+
- Text: `#FFFFFF`
|
|
61
|
+
- Border: 1px solid `#000000`
|
|
62
|
+
- Radius: 2px
|
|
63
|
+
- Padding: 6px 12px
|
|
64
|
+
- Font: 13px / 400
|
|
65
|
+
|
|
66
|
+
**Default (Ghost)**
|
|
67
|
+
- Background: `#FFFFFF`
|
|
68
|
+
- Text: `#000000`
|
|
69
|
+
- Border: 1px solid `#88888E`
|
|
70
|
+
- Radius: 2px
|
|
71
|
+
- Padding: 6px 12px
|
|
72
|
+
- Font: 13px / 400
|
|
73
|
+
|
|
74
|
+
**Disabled**
|
|
75
|
+
- Background: `#FFFFFF`
|
|
76
|
+
- Text: `#AFAEAB`
|
|
77
|
+
- Border: 1px solid `#CFCECB`
|
|
78
|
+
- Radius: 2px
|
|
79
|
+
|
|
80
|
+
**Marketing CTA (Brand Purple)**
|
|
81
|
+
- Background: `#7618F1`
|
|
82
|
+
- Text: `#FFFFFF`
|
|
83
|
+
- Border: none
|
|
84
|
+
- Radius: 4px
|
|
85
|
+
- Height: 55px
|
|
86
|
+
- Font: 18px / 600
|
|
87
|
+
|
|
88
|
+
### Form Fields
|
|
89
|
+
|
|
90
|
+
**Default Input**
|
|
91
|
+
- Background: `#FFFFFF`
|
|
92
|
+
- Text: `#555555`
|
|
93
|
+
- Border: 1px solid `#CCCCCC`
|
|
94
|
+
- Radius: 0px
|
|
95
|
+
- Height: 29px
|
|
96
|
+
- Padding: 6px 12px
|
|
97
|
+
- Font: 13px / 400
|
|
98
|
+
|
|
99
|
+
**Focus Input**
|
|
100
|
+
- Background: `#FFFFFF`
|
|
101
|
+
- Text: `#555555`
|
|
102
|
+
- Border: 1px solid `#66AFE9`
|
|
103
|
+
|
|
104
|
+
### Badges / Labels
|
|
105
|
+
|
|
106
|
+
**Primary Badge (Black)**
|
|
107
|
+
- Background: `#000000`
|
|
108
|
+
- Text: `#FFFFFF`
|
|
109
|
+
- Radius: 0px
|
|
110
|
+
- Padding: 0.2em 0.6em 0.3em
|
|
111
|
+
|
|
112
|
+
**Sale / Accent Badge (Red)**
|
|
113
|
+
- Background: `#EC5151`
|
|
114
|
+
- Text: `#FFFFFF`
|
|
115
|
+
- Radius: 4px
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
**Verified:** 2026-06-03
|
|
119
|
+
**Tier 1 sources:** https://www.trenbe.com (HTML + theme-color meta), https://assets.trenbe.com/20260602.151834.7400ea3/main.696cafc00c26f4e3cdcd.js (embedded CSS + design token object), https://www.trenbecorp.com (corp CSS: common.css + main.css), https://trenbe.github.io (tech blog CSS: --primary: #7620F6), https://assets.trenbe.com/font/pretendard/pretendard-subset.css (confirmed Pretendard font)
|
|
120
|
+
**Tier 2 sources:** getdesign.md/trenbe — NOT LISTED (no data). refero — no result for Trenbe.
|
|
121
|
+
**Conflicts unresolved:** Two purple primary values co-exist in the codebase: #7620F6 (older token, used on tech blog and trenbecorp.com as `--primary`) and #7351EC (newer v4 DS token `purple500` / `uiPrimary`). The tech blog and corporate site both use #7620F6 as `--primary`, which is treated as the canonical brand primary here. The v4 DS purple500 (#7351EC) is the current interactive UI token in the product app.
|
|
122
|
+
|
|
123
|
+
## 5. Layout Principles
|
|
124
|
+
|
|
125
|
+
- Single-column mobile-first grid; horizontal padding: 20px mobile, 41px tablet, 48px desktop
|
|
126
|
+
- Vertical rhythm follows spacing token scale (0.2rem–6.4rem in 8-step increments)
|
|
127
|
+
- Product grid: 2-column on mobile, 3–4 column on tablet/desktop, with consistent gutter matching horizontal padding
|
|
128
|
+
- Maximum content width: 1200px (pc breakpoint), centered with auto margins
|
|
129
|
+
- Cards have 8px radius (main borderRadius token), hairline 1px `#CFCECB` borders
|
|
130
|
+
- zIndex layers: header 1399, modal 1500, ProductCard 1
|
|
131
|
+
|
|
132
|
+
## 6. Depth & Elevation
|
|
133
|
+
|
|
134
|
+
- Minimal shadow philosophy: UI avoids drop-shadows on cards; separation achieved via border `#CFCECB` and background-color contrast (`#F7F6F5` vs `#FFFFFF`)
|
|
135
|
+
- Modals use `rgba(0,0,0,0.5)` scrim backdrop (alpha50 token = `#2F2E2B80`)
|
|
136
|
+
- Sticky header separates with a subtle bottom border, no shadow
|
|
137
|
+
- Drawers and bottom sheets slide in with `0.325s ease-out` without elevation shadow
|
|
138
|
+
|
|
139
|
+
## 7. Do's and Don'ts
|
|
140
|
+
|
|
141
|
+
### Do
|
|
142
|
+
- Use `#7620F6` (or `#7351EC` in v4 DS contexts) exclusively as the brand accent; do not introduce other accent hues
|
|
143
|
+
- Keep primary CTAs black (#000) on white surfaces inside the product UI to respect luxury product photography
|
|
144
|
+
- Reserve the purple CTA only for high-emphasis marketing surfaces (landing pages, onboarding)
|
|
145
|
+
- Use Pretendard for all Korean and Latin text; ensure feature settings `"ss03"` `"cv01"` are active on mobile
|
|
146
|
+
- Apply 8px radius to product cards; 4px to chips, badges, and secondary UI elements
|
|
147
|
+
- Use red (#EC5151) exclusively for sale prices, error states, and count-down labels
|
|
148
|
+
- Respect the 20px mobile / 48px desktop horizontal padding consistently
|
|
149
|
+
|
|
150
|
+
### Don't
|
|
151
|
+
- Don't use the purple as a button background inside the main shopping UI — the primary action is always black
|
|
152
|
+
- Don't add gradients or decorative patterns behind product images
|
|
153
|
+
- Don't reduce line-height below 1.5× for body text; the Pretendard variable-weight range demands adequate leading
|
|
154
|
+
- Don't mix the legacy 13px/1.2 line-height scale with the v4 rem scale in new components
|
|
155
|
+
- Don't use border-radius above 8px for cards or 4px for chips in the current DS
|
|
156
|
+
|
|
157
|
+
## 8. Responsive Behavior
|
|
158
|
+
|
|
159
|
+
- **Mobile (≤480px):** 20px horizontal padding; 2-column product grid; bottom navigation bar replaces desktop header links; font sizes follow v4 rem scale with `font-feature-settings: "ss03","cv01"`
|
|
160
|
+
- **Tablet mini (480–768px):** 41px horizontal padding; 3-column grid; header retains logo + search; secondary nav collapsed
|
|
161
|
+
- **Tablet big (768–1024px):** full desktop header; 3–4 column grid; xPadding 41px
|
|
162
|
+
- **Desktop (1024–1200px):** 48px horizontal padding; max-content 1200px; full navigation visible
|
|
163
|
+
- **Large desktop (≥1200px):** content remains capped at 1200px; outer gutters expand
|
|
164
|
+
|
|
165
|
+
## 9. Agent Prompt Guide
|
|
166
|
+
|
|
167
|
+
When generating Trenbe UI:
|
|
168
|
+
- Use white (`#FFFFFF`) canvas with warm near-black body text (`#2F2E2B`)
|
|
169
|
+
- Primary interactive element: black button (bg `#000`, text `#fff`, radius 2px)
|
|
170
|
+
- Use Pretendard font; specify weights 400 / 500 / 700
|
|
171
|
+
- Accent color: `#7620F6` for labels, links, and focus indicators only
|
|
172
|
+
- Red (`#EC5151`) for price reductions and error messages
|
|
173
|
+
- Green (`#1EB789`) for success confirmations
|
|
174
|
+
- Card containers: white bg, 1px `#CFCECB` border, 8px radius
|
|
175
|
+
- Do not add gradients, shadows, or decorative flourishes behind product images
|
|
176
|
+
- Layout: 20px mobile gutter, 48px desktop gutter, max-width 1200px
|
|
177
|
+
|
|
178
|
+
## 10. Voice & Tone
|
|
179
|
+
|
|
180
|
+
**Three defining adjectives:** Trustworthy, Direct, Savvy
|
|
181
|
+
|
|
182
|
+
| Do | Don't |
|
|
183
|
+
|----|-------|
|
|
184
|
+
| Use clear, benefit-forward language: "verified by our experts" | Use vague luxury clichés: "elevate your lifestyle" |
|
|
185
|
+
| Be specific about tech advantages: "AI price matching across 10M items" | Use jargon: "synergize your fashion portfolio" |
|
|
186
|
+
| Acknowledge the user's desire directly: "가지고 싶은 것, 가지고 싶을 때" (what you want, when you want it) | Be passive or hedged: "you might consider exploring..." |
|
|
187
|
+
| Frame authentication as protection, not gatekeeping | Shame or intimidate around budget |
|
|
188
|
+
|
|
189
|
+
**Voice samples (illustrative):**
|
|
190
|
+
- "전세계 명품을 한 눈에. 최저가 스캐너로 가장 빠르게." — All the world's luxury, at a glance. Fastest with our lowest-price scanner. *(illustrative tone sample)*
|
|
191
|
+
- "가지고 싶은 것, 가지고 싶을 때, 가질 수 있도록." — What you want, when you want it, yours to have. *(illustrative brand line)*
|
|
192
|
+
- "300% 보상 정책으로 안심하고 쇼핑하세요." — Shop with peace of mind backed by our 300% compensation policy. *(illustrative trust copy)*
|
|
193
|
+
|
|
194
|
+
## 11. Brand Narrative
|
|
195
|
+
|
|
196
|
+
Trenbe was founded in November 2016 in Seoul, South Korea, by CEO Park Kyung-hoon with a clear thesis: luxury shopping online did not have to mean counterfeit risk, opaque pricing, or limited selection. The name "Trenbe" blends "trend" and "be," signaling the brand's aspiration for customers to inhabit the trends they love rather than merely observe them. From the start, the company invested in physical infrastructure — overseas offices and fulfillment centers in the UK, US, France, Germany, Italy, and Japan — so that its platform could offer direct buying, direct inspection, and direct shipping rather than relying on third-party resellers.
|
|
197
|
+
|
|
198
|
+
The company describes itself as a "luxury tech-commerce" business rather than a simple marketplace. Its proprietary Trenbot AI scans roughly 10 million products in real time across global department stores, brand official sites, and outlets to surface the lowest available price. The Korean Authentic Center, initially an in-house inspection team of over 40 expert authenticators, was later spun off as an independent subsidiary to reinforce the credibility of Trenbe's authentication signal. In 2022 Trenbe raised a KRW 35 billion Series D round, and the platform's MAU reached 4.5 million by late 2020. By Q1 2026 the pre-owned luxury segment contributed to its first operating profit of KRW 300 million.
|
|
199
|
+
|
|
200
|
+
Trenbe's corporate manifesto — "We change the rules. We change the commerce." — anchors its design and product philosophy. The purple accent in its design system represents this disruption: vivid and technologically forward in a category traditionally expressed through ivory and gold. Yet the dominant use of black-and-white in the shopping UI maintains the respectful restraint that luxury buyers expect when their attention is focused on the products themselves.
|
|
201
|
+
|
|
202
|
+
## 12. Principles
|
|
203
|
+
|
|
204
|
+
1. **Trust through transparency** — Every price displayed shows where it came from. Every product that passes through Trenbe's fulfillment is inspected and logged. *UI implication:* Inspection status, price history, and authentication certificates appear prominently on product detail pages without requiring the user to dig for them.
|
|
205
|
+
|
|
206
|
+
2. **Desire, not delay** — The user's intent is to acquire something desirable; friction between intent and checkout must be minimized. *UI implication:* Add-to-cart and purchase flows are surfaced within one thumb's reach on mobile; payment providers including Naver Pay, Apple Pay, and card-based installments are pre-integrated.
|
|
207
|
+
|
|
208
|
+
3. **Global reach, local precision** — Trenbe operates in seven countries but the experience is built for Korean consumers first, then localized. *UI implication:* Korean-language copy is the primary string; KRW pricing is canonical; Pretendard is loaded as a subset for performance at Korean character density.
|
|
209
|
+
|
|
210
|
+
4. **Technology as service, not spectacle** — AI and automation power the platform but should never make the user feel surveilled or overwhelmed. *UI implication:* Trenbot recommendations are labeled clearly as price comparisons, not "personalization magic"; data-driven badges use factual language ("15% below market average").
|
|
211
|
+
|
|
212
|
+
5. **Authenticity as table stakes** — Counterfeit risk is the sector's defining anxiety. Trenbe's response is operational, not just copywritten. *UI implication:* Authentic guarantee badges and Korean Authentic Center certification marks appear at the same visual weight as the price; they are never buried in footers.
|
|
213
|
+
|
|
214
|
+
## 13. Personas
|
|
215
|
+
|
|
216
|
+
*Illustrative Persona A — The Aspirational Accumulator:* A 28-year-old woman in Seoul in her first management-level role. She buys one statement luxury piece per quarter as a self-reward. She comparison-shops obsessively before committing. She trusts Trenbe's price scanner because she has verified its results against department-store prices herself. Her primary device is iPhone; she browses during commute and completes purchases at home.
|
|
217
|
+
|
|
218
|
+
*Illustrative Persona B — The Resale Strategist:* A 35-year-old man who buys luxury goods both to enjoy and to trade. He uses the Shuffle exchange feature to upgrade without net spend. He values the authentication certificate because it protects the resale value of his items. He reads the tech blog and trusts the brand's engineering transparency.
|
|
219
|
+
|
|
220
|
+
*Illustrative Persona C — The Gifting Professional:* A 40-year-old professional who buys 3–5 luxury gifts per year for clients and family. She needs fast, reliable delivery and gift-wrap options. She cares deeply about authenticity guarantees because a counterfeit gift would be a professional embarrassment. The 300% compensation policy is the feature that converted her.
|
|
221
|
+
|
|
222
|
+
*Illustrative Persona D — The Global Shopper:* A Korean expat in London who relies on Trenbe's UK office logistics to ship authenticated pieces home to family at prices competitive with local boutiques. She reads prices in KRW and filters by brand country of origin.
|
|
223
|
+
|
|
224
|
+
## 14. States
|
|
225
|
+
|
|
226
|
+
- **Empty:** Category page with no matching items shows a centered purple icon + "검색 결과가 없어요" (No results found) in gray900 text with a suggestion to broaden the filter — no full-bleed illustration, no decorative artwork
|
|
227
|
+
- **Loading (skeleton):** Product cards show a gray100 (#F7F6F5) block for the image area and two gray200 (#EFEEEB) lines for title/price; animated with a left-to-right shimmer using linear gradient
|
|
228
|
+
- **Error — Network:** Full-page or modal overlay on a white background with a neutral icon, headline in gray1000, sub-copy in gray800, and a black "다시 시도" (Retry) CTA button
|
|
229
|
+
- **Error — Validation:** Inline beneath form fields; border turns red (#EC5151), helper text appears in red500 at 1.2rem (12px), field label does not change color
|
|
230
|
+
- **Success:** Green (#1EB789) inline confirmation text or toast; checkmark icon at 24px; auto-dismisses after 3s with 0.2s ease-in-out fade-out
|
|
231
|
+
- **Skeleton (product detail):** Hero image area dims to gray200, product title and price lines show as gray100 bars; below-fold specs show 3-line skeleton
|
|
232
|
+
- **Disabled:** Button opacity 0.65; text color switches to #AFAEAB (gray600/textDisabled); cursor changes to not-allowed; no background color change
|
|
233
|
+
|
|
234
|
+
## 15. Motion & Easing
|
|
235
|
+
|
|
236
|
+
**Duration scale:**
|
|
237
|
+
- Micro: 150ms — border-color focus transitions (`ease-in-out`)
|
|
238
|
+
- Short: 200ms — hover state color changes, icon rotations (`ease-in-out`)
|
|
239
|
+
- Medium: 240ms — drawer bottom position animations (`ease`)
|
|
240
|
+
- Standard: 325ms — modal entrance/exit, overlay opacity (`ease-out`)
|
|
241
|
+
- Long: 400ms — page-level slide carousels (`linear`)
|
|
242
|
+
|
|
243
|
+
**Primary easing functions:**
|
|
244
|
+
- `ease-in-out` — default for interactive state changes (border, background-color)
|
|
245
|
+
- `ease-out` — modal fade-in and slide-up (enters fast, settles gently)
|
|
246
|
+
- `linear` — carousel swipe momentum, progress bars
|
|
247
|
+
|
|
248
|
+
**Rules:**
|
|
249
|
+
- All transitions are applied via CSS `transition` properties; no JavaScript-driven animation for routine UI state changes
|
|
250
|
+
- Skeleton shimmer uses a CSS linear-gradient animation at 1.5s infinite linear (AOS library handles scroll-triggered reveals)
|
|
251
|
+
- Carousel interactions use Swiper.js inertia; drag velocity maps naturally without artificial easing override
|
|
252
|
+
- Reduced-motion: no explicit `prefers-reduced-motion` override is in the current CSS; agents generating new components must add it
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: voicetube
|
|
3
|
+
name: "VoiceTube"
|
|
4
|
+
country: TW
|
|
5
|
+
category: education
|
|
6
|
+
homepage: "https://tw.voicetube.com"
|
|
7
|
+
primary_color: "#7E3AAF"
|
|
8
|
+
logo:
|
|
9
|
+
type: favicon
|
|
10
|
+
slug: "https://www.google.com/s2/favicons?domain=tw.voicetube.com&sz=256"
|
|
11
|
+
verified: "2026-06-03"
|
|
12
|
+
omd: "0.1"
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# VoiceTube
|
|
16
|
+
|
|
17
|
+
Asia's largest video-based English learning platform, blending immersive YouTube content with AI-powered study tools under a dramatic dark-purple brand identity.
|
|
18
|
+
|
|
19
|
+
## 1. Visual Theme & Atmosphere
|
|
20
|
+
|
|
21
|
+
VoiceTube presents a consistently dark, cinematic atmosphere built around a deep purple-to-near-black canvas. The body background is `#18131D` — a dark aubergine that evokes a premium streaming experience rather than a conventional classroom — while the navigation bar graduates from `#210040` to `#1D102B` as a horizontal gradient, anchoring every page in the brand's deepest signature hues. Primary call-to-action elements arrive as pill-shaped buttons bearing a vivid diagonal gradient from `#653AAF` to `#A73AAF`, injecting a pop of electric violet that reads immediately as the interactive signal layer. Secondary surfaces such as the sidebar and product grid use `#251633`, creating a subtle two-stop dark elevation system. The overall mood is energetic and night-mode-native: think streaming platform meets study app, where video thumbnail cards float on near-black panels, typography in soft white-gray (`#E3E3E3`) maintains clarity without harshness, and playful gradient accents ensure the interface never feels heavy or academic.
|
|
22
|
+
|
|
23
|
+
## 2. Color Palette & Roles
|
|
24
|
+
|
|
25
|
+
- **Brand Purple 400 (Primary):** `#7E3AAF` — interactive accents, badge fills, focused input borders
|
|
26
|
+
- **Brand Purple 300 (Vibrant):** `#A73AAF` — gradient endpoint, hover highlights, accent text on dark
|
|
27
|
+
- **Button Gradient Start:** `#653AAF` — primary CTA gradient origin (135.73°)
|
|
28
|
+
- **Deep Purple 500:** `#210040` — navigation gradient start, deepest surface overlays
|
|
29
|
+
- **Dark Aubergine 800 (Background):** `#18131D` — body canvas, default page background
|
|
30
|
+
- **Surface 600:** `#251633` — sidebar background, secondary card surfaces
|
|
31
|
+
- **Nav Gradient End:** `#1D102B` — navigation bar right edge
|
|
32
|
+
- **Purple Tint 100:** `#E0D0EC` — light tag fill, subtle selected state
|
|
33
|
+
- **White:** `#FFFFFF` — primary text on gradient buttons
|
|
34
|
+
- **Gray 300 (Body Text):** `#E3E3E3` — body text on dark, outline button text and border
|
|
35
|
+
- **Gray 400 (Secondary):** `#B4B4B4` — secondary labels, disabled state text
|
|
36
|
+
- **Gray 500 (Muted):** `#9CA3AF` — placeholder text
|
|
37
|
+
- **Error:** `#ED4F55` — danger borders, validation messages
|
|
38
|
+
- **Success:** `#33991D` — correct answers, achievement indicators
|
|
39
|
+
- **Link/Info:** `#4283E4` — hyperlink color, informational icons
|
|
40
|
+
|
|
41
|
+
## 3. Typography Rules
|
|
42
|
+
|
|
43
|
+
VoiceTube's global body font is a CJK-first stack: `-apple-system, BlinkMacSystemFont, PingFang TC, PingFang SC, Hiragino Sans, Hiragino Kaku Gothic ProN, Microsoft JhengHei, Microsoft YaHei, Meiryo, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif`. This stack ensures Mandarin and Japanese characters render optimally before falling back to Latin fonts. The base font weight is `400`, body text is set in `#E3E3E3` for comfortable contrast on dark backgrounds, and line-height defaults to `1.425` (the `vc-leading-base` token). The proprietary logomark typeface features 60° slanted stroke extensions that echo the play-button triangle — this angular DNA is brand-exclusive and not replicated in UI text. The type scale in use: `xs = 12px`, `sm = 14px`, `base/md = 16px`, `lg = 18px`, `xl = 20px`, `2xl = 24px`. Weight tokens span from `vc-font-light (300)` through `vc-font-normal (400)`, `vc-font-medium (500)`, `vc-font-semibold (600)`, `vc-font-bold (700)`, to `vc-font-black (900)`. Navigation labels and primary UI text use `500 (medium)`.
|
|
44
|
+
|
|
45
|
+
## 4. Component Stylings
|
|
46
|
+
|
|
47
|
+
### Primary CTA Button
|
|
48
|
+
|
|
49
|
+
**Gradient Filled**
|
|
50
|
+
- Background: `linear-gradient(135.73deg, #653AAF, #A73AAF 98.77%)`
|
|
51
|
+
- Text: `#FFFFFF`
|
|
52
|
+
- Radius: 9999px
|
|
53
|
+
- Height: 40px
|
|
54
|
+
- Padding: 0 20px
|
|
55
|
+
- Font: 16px / 500
|
|
56
|
+
|
|
57
|
+
### Outline Button
|
|
58
|
+
|
|
59
|
+
**Ghost / Secondary**
|
|
60
|
+
- Background: `transparent`
|
|
61
|
+
- Text: `#E3E3E3`
|
|
62
|
+
- Border: 1px solid `#E3E3E3`
|
|
63
|
+
- Radius: 9999px
|
|
64
|
+
- Height: 40px
|
|
65
|
+
- Padding: 0 20px
|
|
66
|
+
- Font: 16px / 500
|
|
67
|
+
|
|
68
|
+
### Navigation Bar
|
|
69
|
+
|
|
70
|
+
**Top Nav**
|
|
71
|
+
- Background: `linear-gradient(to right, #210040, #1D102B)`
|
|
72
|
+
- Text: `#E3E3E3`
|
|
73
|
+
- Height: 64px
|
|
74
|
+
|
|
75
|
+
### Sidebar Nav Item
|
|
76
|
+
|
|
77
|
+
**Active / Hover**
|
|
78
|
+
- Background: `rgba(126, 58, 175, 0.2)`
|
|
79
|
+
- Text: `#E3E3E3`
|
|
80
|
+
- Padding: 12px 8px
|
|
81
|
+
- Font: 14px / 400
|
|
82
|
+
- Radius: 0px
|
|
83
|
+
|
|
84
|
+
### Tag / Badge Chip
|
|
85
|
+
|
|
86
|
+
**Brand Tag**
|
|
87
|
+
- Background: `#7E3AAF`
|
|
88
|
+
- Text: `#E3E3E3`
|
|
89
|
+
- Radius: 4px
|
|
90
|
+
- Padding: 2.5px 12px
|
|
91
|
+
- Font: 14px / 400
|
|
92
|
+
|
|
93
|
+
### Text Input
|
|
94
|
+
|
|
95
|
+
**Default State**
|
|
96
|
+
- Background: `transparent`
|
|
97
|
+
- Text: `#E3E3E3`
|
|
98
|
+
- Border: 1px solid `#E3E3E3`
|
|
99
|
+
- Radius: 4px
|
|
100
|
+
|
|
101
|
+
**Focus / Hover**
|
|
102
|
+
- Border: 1px solid `#7E3AAF`
|
|
103
|
+
|
|
104
|
+
**Disabled**
|
|
105
|
+
- Text: `#B4B4B4`
|
|
106
|
+
|
|
107
|
+
**Error**
|
|
108
|
+
- Border: 1px solid `#ED4F55`
|
|
109
|
+
- Text: `#ED4F55`
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
**Verified:** 2026-06-03
|
|
113
|
+
**Tier 1 sources:** https://tw.voicetube.com (homepage HTML + CSS `/_next/static/css/8c750a42d09ad582.css`); https://tw.voicetube.com/branding (brand page — structure + JS chunk `page-7cb732e6cbfefad2.js`); https://tw.voicetube.com/about (brand narrative)
|
|
114
|
+
**Tier 2 sources:** getdesign.md/voicetube — NOT LISTED ("No designs found for 'voicetube'"). refero — no result found for VoiceTube.
|
|
115
|
+
**Conflicts unresolved:** none
|
|
116
|
+
|
|
117
|
+
## 5. Layout Principles
|
|
118
|
+
|
|
119
|
+
VoiceTube uses a responsive two-column shell on desktop: a fixed 110px-wide sidebar navigation column paired with a fluid content area (`vc-grid-cols-[110px_1fr]`), which collapses to single-column on mobile (`lg:vc-grid-cols-1`). Content grids for video cards typically use 3–4 columns on large screens, scaling down through 2-column tablet to single-column mobile layouts. Horizontal container padding follows the Tailwind spacing scale (`p-4` = 16px, `p-5` = 20px, `p-6` = 24px). The overall max content width is not capped artificially narrow — VoiceTube leans into wide viewports for its video-first grid. Sticky positioning is used for the top navigation bar at `z-[800]`, ensuring it floats above all content as users scroll through dense video libraries.
|
|
120
|
+
|
|
121
|
+
## 6. Depth & Elevation
|
|
122
|
+
|
|
123
|
+
VoiceTube's elevation model is purely dark-stack-based — layers are distinguished by background darkness rather than box-shadows. The four tiers in practice: **L0** `#18131D` (body canvas), **L1** `#1D102B–#210040` (navigation, modal backdrops), **L2** `#251633` (sidebar panels, card surfaces), **L3** `#2B2B2B` (dark-500 floating elements, tooltips). Genuine `box-shadow` usage is sparse; focus states use a thin `0 0.1px 0 0 #7E3AAF` outline-shadow rather than a spread shadow, keeping the UI graphically flat. Modal/overlay z-indices follow a defined stack: content at 100, sticky nav at 800, modals at 1200–1299, toasts/notifications at 1500–1600.
|
|
124
|
+
|
|
125
|
+
## 7. Do's and Don'ts
|
|
126
|
+
|
|
127
|
+
### Do
|
|
128
|
+
- Use the `linear-gradient(135.73deg, #653AAF, #A73AAF)` gradient as the primary CTA fill to maintain brand energy
|
|
129
|
+
- Apply `#7E3AAF` for interactive focus rings, active link states, and selected filters
|
|
130
|
+
- Use pill-shaped buttons (`border-radius: 9999px`) for all standalone CTA actions
|
|
131
|
+
- Keep body backgrounds in the `#18131D` to `#251633` range for dark-mode consistency
|
|
132
|
+
- Use `#E3E3E3` (gray-300) as the default text color on dark surfaces
|
|
133
|
+
- Prefer the CJK-inclusive font stack for any Mandarin or Japanese content
|
|
134
|
+
- Apply `ease-out` timing for all UI transitions at 150ms or 300ms
|
|
135
|
+
|
|
136
|
+
### Don't
|
|
137
|
+
- Don't use high-saturation colors from outside the purple / teal / orange product palette without clear context
|
|
138
|
+
- Don't apply gradient buttons to destructive actions (delete, remove) — use plain `#ED4F55` borders instead
|
|
139
|
+
- Don't use light backgrounds (`#FFFFFF`, `#F3F3F3`) as page canvas — VoiceTube is dark-mode-native
|
|
140
|
+
- Don't mix border-radius: square corners would clash with the brand's pill-and-round language
|
|
141
|
+
- Don't stack multiple box-shadows — elevation is communicated through dark tone layering, not shadow spreads
|
|
142
|
+
- Don't reduce font weight below 400 for UI text at `14px` or smaller on dark surfaces
|
|
143
|
+
|
|
144
|
+
## 8. Responsive Behavior
|
|
145
|
+
|
|
146
|
+
VoiceTube's breakpoint system uses Tailwind's `md` (768px) and `lg` (1024px) prefixes extensively. The pattern `md:vc-h-9 md:vc-text-sm` on buttons demonstrates size reduction at tablet widths: button height drops from 40px to 36px and font from 16px to 14px. The sidebar collapses at `lg:vc-hidden`. Video grids shift from a multi-column layout to a vertically stacked, horizontally scrollable carousel on mobile. Hero section typography is set in viewport-relative units (`4.167vw` headline on desktop, `6.4vw` on mobile), ensuring the brand message fills the screen at every size. Padding contracts gracefully: `p-6` on desktop drops to `p-4` on mobile throughout content areas.
|
|
147
|
+
|
|
148
|
+
## 9. Agent Prompt Guide
|
|
149
|
+
|
|
150
|
+
When building VoiceTube-style UI:
|
|
151
|
+
- Dark canvas: body background `#18131D`, sidebar `#251633`, nav `linear-gradient(to right, #210040, #1D102B)`
|
|
152
|
+
- Primary button: `border-radius: 9999px`, `height: 40px`, `padding: 0 20px`, `background: linear-gradient(135.73deg, #653AAF, #A73AAF)`, `color: #FFFFFF`, `font-weight: 500`, `font-size: 16px`
|
|
153
|
+
- Outline button: same shape, `background: transparent`, `border: 1px solid #E3E3E3`, `color: #E3E3E3`
|
|
154
|
+
- Focus ring: `box-shadow: 0 0 0 1px #7E3AAF`
|
|
155
|
+
- Error state: `border-color: #ED4F55`, `color: #ED4F55`
|
|
156
|
+
- Disabled: `color: #B4B4B4`, `cursor: not-allowed`
|
|
157
|
+
- Transitions: `transition-duration: 150ms`, `transition-timing-function: ease-out`
|
|
158
|
+
- Font stack: `-apple-system, BlinkMacSystemFont, "PingFang TC", "PingFang SC", "Microsoft JhengHei", sans-serif`
|
|
159
|
+
|
|
160
|
+
## 10. Voice & Tone
|
|
161
|
+
|
|
162
|
+
VoiceTube's copy voice is **energetic, inclusive, encouraging** — a friendly study partner who celebrates small wins without condescension.
|
|
163
|
+
|
|
164
|
+
| Dimension | Do | Don't |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| Register | Warm, conversational, direct | Academic, formal, passive |
|
|
167
|
+
| Pacing | Short punchy sentences | Long subordinate clause chains |
|
|
168
|
+
| CTA framing | "開始學習" / "免費加入" | "Please register to access content" |
|
|
169
|
+
| Achievement framing | "你做到了!" / "再接再厲" | "Your score was insufficient" |
|
|
170
|
+
| Error framing | "再試一次" (try again) | "An error has occurred" |
|
|
171
|
+
|
|
172
|
+
Voice samples (illustrative):
|
|
173
|
+
- *Illustrative — homepage headline register:* "連結世界、享受學習樂趣!" — Connect to the world, enjoy learning. No jargon, no friction, pure invitation.
|
|
174
|
+
- *Illustrative — product description:* "每天20分鐘跟著練習,即可獲得30天免費訂閱制" — Specific promise, specific time, specific reward. Numbers over adjectives.
|
|
175
|
+
- *Illustrative — branding tagline:* "Connect. Have fun!" — Two words each. The brand thinks in micro-copy that works across any language boundary.
|
|
176
|
+
|
|
177
|
+
## 11. Brand Narrative
|
|
178
|
+
|
|
179
|
+
VoiceTube launched in 2013 when three co-founders — Zenn, Lai, and Tsai — were teaching themselves English after work using YouTube videos. What started as a personal workaround became a community: a place where millions of Mandarin speakers could learn the world's lingua franca through the authentic, conversational content they actually wanted to watch. The insight was simple but powerful — context-rich video beats rote drilling, and the best classroom is one you choose to enter.
|
|
180
|
+
|
|
181
|
+
From that origin story, VoiceTube built "Asia's largest online education platform," expanding from a curated video library into a full learning ecosystem: the core VoiceTube app for daily video-driven practice, Hero for AI-adaptive test preparation, Vclass for live celebrity-instructor courses, and Dictionary for deep vocabulary acquisition with dual-accent audio. The brand's name fuses "voice" (spoken language output, the hardest skill to acquire) with "tube" (YouTube's video-native learning environment), and the logo literalises this: a play triangle, the letter V, and a checkmark merged into a single mark whose 60°-angle typography echoes the geometry of the play button across every letterform.
|
|
182
|
+
|
|
183
|
+
Today, with over 5 million users and recognition including Facebook App of the Year 2016, VoiceTube positions itself not as a language-learning tool but as a social platform for human connection through English — summarised in its rallying cry: **"Connect. Have fun!"**
|
|
184
|
+
|
|
185
|
+
## 12. Principles
|
|
186
|
+
|
|
187
|
+
1. **Video-first learning.** Real language lives in authentic video, not curated sentences. Every feature exists to make video consumption study-worthy. *UI implication:* Video thumbnails are the primary content atom; card grids prioritize visual poster art with minimal text overlay.
|
|
188
|
+
|
|
189
|
+
2. **Connection over completion.** Learning English is a means to connect with people and ideas worldwide, not a checkmark. *UI implication:* Social indicators (learner counts, challenge rankings) are surfaced prominently to reinforce the community dimension.
|
|
190
|
+
|
|
191
|
+
3. **Guided, not gatekept.** The platform meets learners where they are and provides direction without testing walls. *UI implication:* Free content is broadly accessible; premium features are surfaced contextually via gentle gradient CTAs rather than locked-content banners.
|
|
192
|
+
|
|
193
|
+
4. **Innovation in every layer.** From AI vocabulary recommendations to gamified pronunciation challenges, technology should make learning feel effortless. *UI implication:* AI-powered elements are woven into the core flow — word lookup inline, difficulty auto-adjustment, personalised ranking — rather than siloed as premium add-ons.
|
|
194
|
+
|
|
195
|
+
5. **Diverse and compound identity.** The brand serves Mandarin, Japanese, and Vietnamese speakers, and the design must flex accordingly. *UI implication:* CJK-optimized font stacks, locale-aware copy, and multi-lingual navigation are non-negotiable baseline requirements.
|
|
196
|
+
|
|
197
|
+
## 13. Personas
|
|
198
|
+
|
|
199
|
+
*Illustrative archetype — The Ambitious Professional:* A 28-year-old Taipei product manager who needs business English to advance her career. She has 20–30 minutes on her commute. She uses VoiceTube for daily video practice and Hero for TOEIC prep. She values progress indicators and streak mechanics that prove the time investment is working.
|
|
200
|
+
|
|
201
|
+
*Illustrative archetype — The Pop-Culture Learner:* A 19-year-old university student in Taichung who watches English-language YouTube unsubbed but misses nuance. He found VoiceTube through a gaming channel and now uses the dictionary for slang. He values content discovery and social sharing over structured curricula.
|
|
202
|
+
|
|
203
|
+
*Illustrative archetype — The Late-Stage Returner:* A 45-year-old parent in Kaohsiung who studied English in school, let it lapse, and now wants to rebuild for their child's international schooling applications. She learns through Vclass with established instructors. She values warmth, patience, and celebrity-instructor credibility over gamification.
|
|
204
|
+
|
|
205
|
+
*Illustrative archetype — The Japanese Exchange Student:* A 22-year-old from Osaka studying at NTU who uses VoiceTube's Japanese-locale version to practice English while picking up Mandarin via Taiwanese content. He values the multi-locale product suite and dual-accent dictionary features.
|
|
206
|
+
|
|
207
|
+
## 14. States
|
|
208
|
+
|
|
209
|
+
- **Empty — no search results:** Dark panel with centered illustration and copy "找不到相關影片,試試其他關鍵字" — purple-tinted icon at low opacity against `#18131D`, no hard borders.
|
|
210
|
+
- **Loading — video grid:** Skeleton cards in `#251633` with subtle shimmer animation sweeping left-to-right in `#1D102B`, matching the existing dark elevation tier so the layout shift is imperceptible.
|
|
211
|
+
- **Error — form validation:** Input border shifts to `1px solid #ED4F55`, placeholder text and error label render in `#ED4F55`, the field label color also shifts to match.
|
|
212
|
+
- **Error — page/server:** Full-page dark overlay at `#18131D` with a centered 404/500 message in `#E3E3E3`, no gradient backgrounds — keeps the dark canvas consistent.
|
|
213
|
+
- **Success — quiz answer correct:** `#33991D` confirmation background at 10% opacity (`rgba(51, 153, 29, 0.1)`) with a full-opacity `#33991D` border and checkmark icon.
|
|
214
|
+
- **Skeleton — video card:** `#251633` block sized to thumbnail aspect-ratio with a `#1D102B` shimmer pulse at 0.8s ease-in-out; title line represented by two `#251633` rounded bars.
|
|
215
|
+
- **Disabled — button:** `opacity: 0.6`, `cursor: not-allowed`, text shifts to `#B4B4B4`, gradient or border color desaturates; no hover effect fires.
|
|
216
|
+
|
|
217
|
+
## 15. Motion & Easing
|
|
218
|
+
|
|
219
|
+
VoiceTube uses a compact three-step duration scale anchored in Tailwind custom tokens:
|
|
220
|
+
|
|
221
|
+
| Token | Value | Use |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `vc-duration-sm` | 86ms | Micro-interactions: hover color shifts, focus ring appearance |
|
|
224
|
+
| `vc-duration-md` | 150ms | Standard transitions: button state changes, border-color, background-color |
|
|
225
|
+
| `vc-duration-xl` | 300ms | Page-level: slide-in panels, carousel advances, modal entrance |
|
|
226
|
+
|
|
227
|
+
The default easing is `ease-out` (`vc-ease-base`) — elements decelerate into their resting position, conveying arrival and completion. `cubic-bezier(0.4, 0, 0.2, 1)` (`vc-ease-in-out`) appears on keyboard-focus indicators and bi-directional carousels where enter and exit need to be symmetrical. Carousel progress dots use a width-transition at `vc-duration-md / ease-out` (active dot expands from 10px to 30px). No spring/bounce physics — the brand is energetic through color and gradient, not through over-engineered motion.
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|