claude-pet 2.0.0
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.
Potentially problematic release.
This version of claude-pet might be problematic. Click here for more details.
- package/.claude/commands/feed.md +28 -0
- package/.claude/commands/name.md +28 -0
- package/.claude/commands/pet.md +29 -0
- package/.claude/commands/play.md +29 -0
- package/.claude/settings.local.json +41 -0
- package/.github/workflows/AGENTS.md +60 -0
- package/.github/workflows/build.yml +87 -0
- package/AGENTS.md +66 -0
- package/LICENSE +15 -0
- package/README.md +292 -0
- package/bin/claude-pet.js +42 -0
- package/build/AGENTS.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/entitlements.mac.plist +14 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/installerHeader.bmp +0 -0
- package/build/installerSidebar.bmp +0 -0
- package/build/tray-icon.png +0 -0
- package/dist/main/core/badge-manager.js +49 -0
- package/dist/main/core/badge-registry.js +72 -0
- package/dist/main/core/badge-triggers.js +45 -0
- package/dist/main/core/contextual-messages.js +372 -0
- package/dist/main/core/messages.js +440 -0
- package/dist/main/core/mood-engine.js +145 -0
- package/dist/main/core/pet-messages.js +612 -0
- package/dist/main/core/pet-state-engine.js +232 -0
- package/dist/main/core/quote-collection.js +60 -0
- package/dist/main/core/quote-registry.js +175 -0
- package/dist/main/core/quote-triggers.js +62 -0
- package/dist/main/core/usage-tracker.js +625 -0
- package/dist/main/main/auto-launch.js +39 -0
- package/dist/main/main/auto-updater.js +98 -0
- package/dist/main/main/event-watcher.js +174 -0
- package/dist/main/main/ipc-handlers.js +89 -0
- package/dist/main/main/main.js +422 -0
- package/dist/main/main/preload.js +93 -0
- package/dist/main/main/settings-window.js +49 -0
- package/dist/main/main/share-card.js +139 -0
- package/dist/main/main/skin-manager.js +118 -0
- package/dist/main/main/tray.js +88 -0
- package/dist/main/shared/i18n.js +392 -0
- package/dist/main/shared/types.js +25 -0
- package/dist/main/shared/utils.js +9 -0
- package/dist/renderer/assets/claude-pet.png +0 -0
- package/dist/renderer/assets/index-BMnMEuOf.js +9 -0
- package/dist/renderer/assets/index-qzlrlqpX.css +1 -0
- package/dist/renderer/index.html +30 -0
- package/dist/renderer/share-card-template/card.html +148 -0
- package/docs/AGENTS.md +42 -0
- package/docs/images/angry.png +0 -0
- package/docs/images/character.webp +0 -0
- package/docs/images/claude-mama.png +0 -0
- package/docs/images/happy.png +0 -0
- package/docs/images/proud.png +0 -0
- package/docs/images/share-card-example.png +0 -0
- package/docs/images/worried_1.png +0 -0
- package/docs/images/worried_2.png +0 -0
- package/docs/spritesheet-bugs.md +240 -0
- package/docs/superpowers/plans/2026-03-10-compact-widget.md +888 -0
- package/docs/superpowers/plans/2026-03-10-viral-features.md +1874 -0
- package/docs/superpowers/plans/2026-03-14-update-ux.md +362 -0
- package/docs/superpowers/plans/2026-03-14-v1.1-features.md +2139 -0
- package/docs/superpowers/specs/2026-03-10-compact-widget-design.md +150 -0
- package/docs/superpowers/specs/2026-03-10-viral-features-design.md +217 -0
- package/docs/superpowers/specs/2026-03-14-streak-calendar-design.md +26 -0
- package/docs/superpowers/specs/2026-03-14-update-ux-design.md +172 -0
- package/docs/superpowers/specs/2026-03-14-v1.1-features-design.md +342 -0
- package/electron-builder.yml +75 -0
- package/package.json +48 -0
- package/scripts/AGENTS.md +60 -0
- package/scripts/install.ps1 +47 -0
- package/scripts/install.sh +98 -0
- package/scripts/make-icon.js +119 -0
- package/scripts/notarize.js +18 -0
- package/src/AGENTS.md +47 -0
- package/src/core/AGENTS.md +58 -0
- package/src/core/__tests__/AGENTS.md +60 -0
- package/src/core/__tests__/badge-triggers.test.ts +83 -0
- package/src/core/__tests__/contextual-messages.test.ts +87 -0
- package/src/core/__tests__/pet-state-engine.test.ts +350 -0
- package/src/core/__tests__/quote-collection.test.ts +62 -0
- package/src/core/__tests__/quote-triggers.test.ts +110 -0
- package/src/core/badge-manager.ts +50 -0
- package/src/core/badge-registry.ts +71 -0
- package/src/core/badge-triggers.ts +41 -0
- package/src/core/contextual-messages.ts +381 -0
- package/src/core/pet-messages.ts +615 -0
- package/src/core/pet-state-engine.ts +272 -0
- package/src/core/quote-collection.ts +63 -0
- package/src/core/quote-registry.ts +181 -0
- package/src/core/quote-triggers.ts +64 -0
- package/src/core/usage-tracker.ts +680 -0
- package/src/main/AGENTS.md +70 -0
- package/src/main/auto-launch.ts +38 -0
- package/src/main/auto-updater.ts +106 -0
- package/src/main/event-watcher.ts +159 -0
- package/src/main/ipc-handlers.ts +107 -0
- package/src/main/main.ts +425 -0
- package/src/main/preload.ts +111 -0
- package/src/main/settings-window.ts +50 -0
- package/src/main/share-card.ts +153 -0
- package/src/main/skin-manager.ts +119 -0
- package/src/main/tray.ts +94 -0
- package/src/renderer/AGENTS.md +62 -0
- package/src/renderer/App.tsx +270 -0
- package/src/renderer/assets/claude-mama.png +0 -0
- package/src/renderer/assets/claude-pet.png +0 -0
- package/src/renderer/components/AGENTS.md +50 -0
- package/src/renderer/components/Character.tsx +327 -0
- package/src/renderer/components/SpeechBubble.tsx +182 -0
- package/src/renderer/components/UsageIndicator.tsx +268 -0
- package/src/renderer/electron.d.ts +34 -0
- package/src/renderer/hooks/AGENTS.md +55 -0
- package/src/renderer/hooks/usePetState.ts +59 -0
- package/src/renderer/hooks/useWidgetMode.ts +18 -0
- package/src/renderer/index.html +29 -0
- package/src/renderer/main.tsx +13 -0
- package/src/renderer/pages/AGENTS.md +53 -0
- package/src/renderer/pages/Collection.tsx +252 -0
- package/src/renderer/pages/Settings.tsx +815 -0
- package/src/renderer/share-card-template/card.html +148 -0
- package/src/renderer/styles/AGENTS.md +50 -0
- package/src/renderer/styles/styles.css +166 -0
- package/src/shared/AGENTS.md +48 -0
- package/src/shared/i18n.ts +395 -0
- package/src/shared/types.ts +163 -0
- package/src/shared/utils.ts +6 -0
- package/tsconfig.json +16 -0
- package/tsconfig.main.json +12 -0
- package/tsconfig.renderer.json +12 -0
- package/vite.config.ts +47 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# v1.1 Features Design Spec
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-03-14
|
|
4
|
+
**Features:** Always on Top, Contextual Messages, Achievement Badges, Custom Character Skins
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Feature A: Always on Top
|
|
9
|
+
|
|
10
|
+
### Overview
|
|
11
|
+
데스크톱 마스코트 특성상 항상 다른 윈도우 위에 표시되어야 하므로, `alwaysOnTop` 설정을 추가한다.
|
|
12
|
+
|
|
13
|
+
### Design
|
|
14
|
+
- `MamaSettings`에 `alwaysOnTop: boolean` 추가 (기본값 `true`)
|
|
15
|
+
- `createWindow()`에서 기존 하드코딩된 `alwaysOnTop: true` (main.ts:115)를 `alwaysOnTop: store.get('alwaysOnTop', true)`로 교체
|
|
16
|
+
- `ipc-handlers.ts`의 `SETTINGS_SET` 핸들러에서 `alwaysOnTop` 변경 시 `mainWindow.setAlwaysOnTop(value)` 호출 (mainWindow 파라미터는 `registerIpcHandlers()`에서 이미 전달됨)
|
|
17
|
+
- `ipc-handlers.ts`의 store `defaults`에 `alwaysOnTop: true` 추가
|
|
18
|
+
- Settings 페이지에 토글 UI 추가
|
|
19
|
+
- 트레이 메뉴에 체크박스 토글 추가 (빠른 접근)
|
|
20
|
+
|
|
21
|
+
### Files to Modify
|
|
22
|
+
| File | Change |
|
|
23
|
+
|------|--------|
|
|
24
|
+
| `src/shared/types.ts` | `MamaSettings`에 `alwaysOnTop` 필드 추가 |
|
|
25
|
+
| `src/main/main.ts` | `createWindow()`에 `alwaysOnTop` 옵션, 설정 변경 리스너 |
|
|
26
|
+
| `src/main/ipc-handlers.ts` | `SETTINGS_SET`에서 `alwaysOnTop` 변경 시 `setAlwaysOnTop()` 호출 |
|
|
27
|
+
| `src/main/tray.ts` | 트레이 메뉴에 "Always on Top" 체크박스 추가 |
|
|
28
|
+
| `src/renderer/pages/Settings.tsx` | 토글 UI 추가 |
|
|
29
|
+
| `src/shared/i18n.ts` | "항상 위에" 번역 문자열 |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Feature B: Contextual Messages (상황 인식 메시지)
|
|
34
|
+
|
|
35
|
+
### Overview
|
|
36
|
+
기존 무드 기반 메시지에 상황 인식을 추가하여 더 자연스러운 메시지를 보여준다. 상황 조건이 매칭되면 상황 메시지를 우선 표시하고, 아니면 기존 무드 메시지로 fallback한다.
|
|
37
|
+
|
|
38
|
+
### Contextual Triggers
|
|
39
|
+
|
|
40
|
+
| 상황 | 조건 | 적용 무드 |
|
|
41
|
+
|------|------|----------|
|
|
42
|
+
| **요일 (주말)** | `now.getDay()` === 0 or 6 | 모든 무드 |
|
|
43
|
+
| **연속 미사용** | `dailyHistory`에서 N일 연속 percent === 0 (N >= 2) | angry, worried |
|
|
44
|
+
| **급증** | 전일 대비 30%+ 증가 | happy, proud |
|
|
45
|
+
| **리셋 임박** | 리셋까지 3시간 이내 + 주간 50% 미만 | worried, angry |
|
|
46
|
+
|
|
47
|
+
### Message Selection Logic
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
getContextualMessage(mood, locale, context):
|
|
51
|
+
1. Evaluate context triggers in priority order:
|
|
52
|
+
- 연속 미사용 (highest - most urgent)
|
|
53
|
+
- 리셋 임박
|
|
54
|
+
- 급증
|
|
55
|
+
- 요일 (lowest - most general)
|
|
56
|
+
2. If trigger matches AND has messages for current mood:
|
|
57
|
+
return contextual message (time-seeded rotation within context pool)
|
|
58
|
+
3. Else:
|
|
59
|
+
return getMessage(mood, locale) // existing fallback
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Context Message Pools
|
|
63
|
+
|
|
64
|
+
Contextual messages only apply to `MamaMood` types (angry, worried, happy, proud) — not to error expressions (confused, sleeping) or special states (fiveHourWarning, rateLimited). The `MoodKey` type in `messages.ts` is a local alias and should NOT be reused here.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const CONTEXTUAL_POOLS: Record<ContextTrigger, Partial<Record<MamaMood, Record<Locale, string[]>>>> = {
|
|
68
|
+
weekend: {
|
|
69
|
+
angry: {
|
|
70
|
+
ko: ["주말인데 토큰 안 쓰면 월요일에 후회한다?", ...],
|
|
71
|
+
en: ["Not using tokens on a weekend? Monday-you will regret it!", ...],
|
|
72
|
+
ja: [...],
|
|
73
|
+
zh: [...],
|
|
74
|
+
},
|
|
75
|
+
happy: {
|
|
76
|
+
ko: ["주말에도 열심히 하네~ 역시 내 자식!", ...],
|
|
77
|
+
...
|
|
78
|
+
},
|
|
79
|
+
// ... other moods
|
|
80
|
+
},
|
|
81
|
+
unusedStreak: { ... },
|
|
82
|
+
spike: { ... },
|
|
83
|
+
resetImminent: { ... },
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Required Context Extensions
|
|
88
|
+
|
|
89
|
+
기존 `TriggerContext`에 `resetsAt` 필드를 추가하여 확장. 별도 `MessageContext`를 만들지 않고 `TriggerContext`를 재사용하여 Feature C의 `BadgeTriggerContext extends TriggerContext`와도 일관성 유지.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// types.ts — TriggerContext에 추가
|
|
93
|
+
interface TriggerContext {
|
|
94
|
+
// ... existing fields ...
|
|
95
|
+
resetsAt: string | null; // ISO timestamp — for resetImminent trigger
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Integration with `computeMood` pipeline:** 현재 `main.ts`에서 `getMessage(mood, locale)`을 직접 호출하는 부분을 `getContextualMessage(mood, locale, triggerContext)`로 교체. `computeMood()` 자체는 수정하지 않음 — mood 계산과 메시지 선택을 분리.
|
|
100
|
+
|
|
101
|
+
**`dailyHistory` 제약:** `dailyHistory`는 최근 14일로 제한됨 (`main.ts`에서 `slice(-14)`). 따라서 `unusedStreak`은 최대 14일까지만 감지 가능.
|
|
102
|
+
|
|
103
|
+
### Message Count Target
|
|
104
|
+
|
|
105
|
+
각 트리거당 무드당 5개 이상의 메시지 (4개 언어 × 4개 트리거 × 해당 무드 × 5개 = 상당량).
|
|
106
|
+
초기에는 트리거당 주요 무드 2~3개만 커버하고 점진적으로 확장.
|
|
107
|
+
|
|
108
|
+
| Trigger | Covered Moods | Messages per mood per locale | Total (4 lang) |
|
|
109
|
+
|---------|--------------|------------------------------|----------------|
|
|
110
|
+
| weekend | angry, worried, happy, proud | 5 | 80 |
|
|
111
|
+
| unusedStreak | angry, worried | 5 | 40 |
|
|
112
|
+
| spike | happy, proud | 5 | 40 |
|
|
113
|
+
| resetImminent | angry, worried | 5 | 40 |
|
|
114
|
+
| **Total** | | | **200** |
|
|
115
|
+
|
|
116
|
+
### Files to Create/Modify
|
|
117
|
+
| File | Change |
|
|
118
|
+
|------|--------|
|
|
119
|
+
| `src/core/contextual-messages.ts` | **New** — context trigger evaluation + message pools |
|
|
120
|
+
| `src/core/messages.ts` | Export helper, keep existing pools unchanged |
|
|
121
|
+
| `src/shared/types.ts` | `MessageContext` interface, `ContextTrigger` type |
|
|
122
|
+
| `src/main/main.ts` | Pass context to message selection |
|
|
123
|
+
| `src/core/__tests__/contextual-messages.test.ts` | **New** — trigger logic tests |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Feature C: Achievement Badges
|
|
128
|
+
|
|
129
|
+
### Overview
|
|
130
|
+
사용량 마일스톤, 연속 사용, 엄마 관계 기반의 업적 배지 시스템. 기존 Collection(도감) 페이지에 탭으로 추가.
|
|
131
|
+
|
|
132
|
+
### Badge Tiers
|
|
133
|
+
|
|
134
|
+
| Tier | Color | 난이도 |
|
|
135
|
+
|------|-------|--------|
|
|
136
|
+
| Bronze | #CD7F32 | 쉬움 — 기본 사용으로 달성 |
|
|
137
|
+
| Silver | #C0C0C0 | 보통 — 꾸준한 사용 필요 |
|
|
138
|
+
| Gold | #FFD700 | 어려움 — 장기 헌신 필요 |
|
|
139
|
+
|
|
140
|
+
### Badge Registry
|
|
141
|
+
|
|
142
|
+
| ID | Tier | Name (ko) | Name (en) | Condition |
|
|
143
|
+
|----|------|-----------|-----------|-----------|
|
|
144
|
+
| `badge_first_call` | Bronze | 첫 걸음 | First Steps | 첫 API 호출 (`firstApiCallSeen` transition) |
|
|
145
|
+
| `badge_streak_3` | Bronze | 3일 연속 | 3-Day Streak | dailyHistory 3일 연속 percent > 0 |
|
|
146
|
+
| `badge_mama_7days` | Bronze | 엄마와 7일 | 7 Days with Mom | 설치 후 7일 경과 |
|
|
147
|
+
| `badge_half` | Silver | 반타작 | Halfway There | 주간 50% 달성 |
|
|
148
|
+
| `badge_streak_7` | Silver | 7일 연속 | 7-Day Streak | dailyHistory 7일 연속 percent > 0 |
|
|
149
|
+
| `badge_proud_10` | Silver | 자랑스러운 아들 | Mom's Pride | proud 상태 누적 10회 |
|
|
150
|
+
| `badge_full` | Gold | 풀 가동 | Full Power | 주간 100% 달성 |
|
|
151
|
+
| `badge_streak_30` | Gold | 30일 연속 | 30-Day Streak | dailyHistory 30일 연속 percent > 0 |
|
|
152
|
+
| `badge_survivor` | Gold | 생존왕 | Survivor | angry 상태 누적 10회 후에도 계속 사용 |
|
|
153
|
+
|
|
154
|
+
### Architecture
|
|
155
|
+
|
|
156
|
+
**QuoteCollectionManager 패턴을 따름:**
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
interface BadgeEntry {
|
|
160
|
+
id: string;
|
|
161
|
+
tier: 'bronze' | 'silver' | 'gold';
|
|
162
|
+
name: Record<Locale, string>;
|
|
163
|
+
description: Record<Locale, string>;
|
|
164
|
+
icon: string; // emoji or icon identifier
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface UnlockedBadge {
|
|
168
|
+
id: string;
|
|
169
|
+
unlockedAt: string; // ISO
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface BadgeState {
|
|
173
|
+
unlocked: UnlockedBadge[];
|
|
174
|
+
totalCount: number;
|
|
175
|
+
byTier: Record<BadgeTier, { unlocked: number; total: number }>;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Pure functions:**
|
|
180
|
+
- `badge-registry.ts` — badge definitions
|
|
181
|
+
- `badge-triggers.ts` — `evaluateBadgeTriggers(ctx): string[]`
|
|
182
|
+
- `badge-manager.ts` — unlock state management
|
|
183
|
+
|
|
184
|
+
**Persistence context extensions:**
|
|
185
|
+
|
|
186
|
+
배지 트리거 평가를 위해 추가 통계가 필요:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface BadgeTriggerContext extends TriggerContext {
|
|
190
|
+
proudCount: number; // 누적 proud 상태 횟수
|
|
191
|
+
angryCount: number; // 누적 angry 상태 횟수
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`electron-store`에 저장 (기존 `(store as any).set()` 패턴 사용):
|
|
196
|
+
- `unlockedBadges: UnlockedBadge[]`
|
|
197
|
+
- `moodCounts: Record<MamaMood, number>` — 무드별 누적 횟수
|
|
198
|
+
|
|
199
|
+
**`moodCounts` 증가 시점:** 무드가 이전 poll과 다를 때만 증가 (매 poll tick이 아님). `main.ts`의 polling loop에서 이전 무드를 추적하고, 무드 전환 시에만 카운트.
|
|
200
|
+
|
|
201
|
+
**`badge_survivor` 조건 명확화:** `angryCount >= 10`이고 현재 `weeklyUtilization > 0`이면 해금. 즉, angry를 10번 이상 경험한 뒤에도 여전히 사용 중이면 달성.
|
|
202
|
+
|
|
203
|
+
### UI
|
|
204
|
+
|
|
205
|
+
- Collection 페이지 내부에 "명언 | 배지" 탭 추가 (기존 Settings의 `settings | collection` 탭 구조 아래 중첩 탭)
|
|
206
|
+
- 배지 그리드: 아이콘 + 이름 + 설명
|
|
207
|
+
- 미해금: 실루엣(grayscale) + "???" 표시
|
|
208
|
+
- 해금 시: `BADGE_UNLOCKED` IPC 채널로 renderer에 통지 → 토스트 알림 (SpeechBubble 스타일 재활용)
|
|
209
|
+
- 티어별 색상 border/glow
|
|
210
|
+
|
|
211
|
+
### Files to Create/Modify
|
|
212
|
+
|
|
213
|
+
| File | Change |
|
|
214
|
+
|------|--------|
|
|
215
|
+
| `src/shared/types.ts` | `BadgeEntry`, `UnlockedBadge`, `BadgeState`, `BadgeTier`, `BadgeTriggerContext`, IPC channels: `BADGE_GET`, `BADGE_UNLOCKED` |
|
|
216
|
+
| `src/core/badge-registry.ts` | **New** — badge definitions (9 badges) |
|
|
217
|
+
| `src/core/badge-triggers.ts` | **New** — `evaluateBadgeTriggers()` pure function |
|
|
218
|
+
| `src/core/badge-manager.ts` | **New** — `BadgeManager` class |
|
|
219
|
+
| `src/core/__tests__/badge-triggers.test.ts` | **New** — trigger tests |
|
|
220
|
+
| `src/main/main.ts` | Badge evaluation in polling loop, mood count tracking |
|
|
221
|
+
| `src/main/ipc-handlers.ts` | Badge IPC handlers |
|
|
222
|
+
| `src/main/preload.ts` | Badge API exposure |
|
|
223
|
+
| `src/renderer/pages/Collection.tsx` | 탭 UI, badge grid |
|
|
224
|
+
| `src/renderer/electron.d.ts` | Badge API types |
|
|
225
|
+
| `src/shared/i18n.ts` | Badge UI strings |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Feature D: Custom Character Skins
|
|
230
|
+
|
|
231
|
+
### Overview
|
|
232
|
+
유저가 자신만의 캐릭터 이미지를 업로드하여 사용. 4가지 모드 지원 (default 포함).
|
|
233
|
+
|
|
234
|
+
### Skin Modes
|
|
235
|
+
|
|
236
|
+
| Mode | Input | Behavior |
|
|
237
|
+
|------|-------|----------|
|
|
238
|
+
| `default` | 없음 | 기본 claude-mama.png 사용 |
|
|
239
|
+
| `single` | PNG 1장 | 전 무드 동일 이미지, MoodOverlay 이펙트 유지 |
|
|
240
|
+
| `per-mood` | PNG 최대 6장 | 무드별 이미지, MoodOverlay 유지. 누락된 무드는 기본 claude-mama.png fallback |
|
|
241
|
+
| `spritesheet` | PNG 1장 + grid config | row/column 그리드로 무드별 프레임 지정. `frameW = naturalWidth / columns`, `frameH = naturalHeight / rows` |
|
|
242
|
+
|
|
243
|
+
### File Validation
|
|
244
|
+
- 최대 파일 크기: 2MB
|
|
245
|
+
- 최대 이미지 크기: 512x512px (프레임 기준)
|
|
246
|
+
- PNG/JPG/GIF만 허용
|
|
247
|
+
- 유효하지 않은 파일 업로드 시 에러 메시지 표시
|
|
248
|
+
|
|
249
|
+
### Settings Data Structure
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
type SkinMode = 'default' | 'single' | 'per-mood' | 'spritesheet';
|
|
253
|
+
type Expression = MamaMood | MamaErrorExpression;
|
|
254
|
+
|
|
255
|
+
interface SkinConfig {
|
|
256
|
+
mode: SkinMode;
|
|
257
|
+
// single mode
|
|
258
|
+
singleImagePath?: string;
|
|
259
|
+
// per-mood mode
|
|
260
|
+
moodImages?: Partial<Record<Expression, string>>;
|
|
261
|
+
// spritesheet mode
|
|
262
|
+
spritesheet?: {
|
|
263
|
+
imagePath: string;
|
|
264
|
+
columns: number;
|
|
265
|
+
rows: number;
|
|
266
|
+
moodMap: Record<Expression, { col: number; row: number }>;
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`MamaSettings`에 `skin?: SkinConfig` 추가. 기본값은 `{ mode: 'default' }`.
|
|
272
|
+
|
|
273
|
+
### File Storage
|
|
274
|
+
|
|
275
|
+
- 업로드 이미지를 `app.getPath('userData')/skins/` 에 복사
|
|
276
|
+
- 파일명: `skin-{timestamp}.png`, `skin-{mood}-{timestamp}.png`
|
|
277
|
+
- 설정에는 경로만 저장
|
|
278
|
+
- **Cleanup:** 새 스킨 업로드 시 이전 스킨 파일 삭제 (orphan 방지). `skin-manager.ts`에서 관리
|
|
279
|
+
|
|
280
|
+
### IPC
|
|
281
|
+
|
|
282
|
+
| Channel | Direction | Purpose |
|
|
283
|
+
|---------|-----------|---------|
|
|
284
|
+
| `mama:upload-skin` | renderer → main | 파일 선택 다이얼로그 + 복사 |
|
|
285
|
+
| `mama:reset-skin` | renderer → main | 기본 스킨으로 리셋 |
|
|
286
|
+
| `mama:get-skin-config` | renderer → main | 현재 스킨 설정 조회 |
|
|
287
|
+
|
|
288
|
+
### Character.tsx Changes
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
기존: mamaPng (하드코딩) → imgStyle에 적용
|
|
292
|
+
변경: skinSrc를 props 또는 IPC로 받아서 적용
|
|
293
|
+
|
|
294
|
+
single: src = singleImagePath
|
|
295
|
+
per-mood: src = moodImages[expression] ?? fallback
|
|
296
|
+
spritesheet: src = imagePath, objectPosition으로 프레임 선택
|
|
297
|
+
- objectFit: 'none'
|
|
298
|
+
- objectPosition: `-${col * frameW}px -${row * frameH}px`
|
|
299
|
+
- width/height: frameW/frameH
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Settings UI
|
|
303
|
+
|
|
304
|
+
Settings 페이지에 "캐릭터" 섹션:
|
|
305
|
+
|
|
306
|
+
1. **모드 선택**: 라디오 (기본 / 단일 이미지 / 무드별 / 스프라이트 시트)
|
|
307
|
+
2. **단일 이미지**: 파일 업로드 버튼 + 미리보기
|
|
308
|
+
3. **무드별**: 6개 업로드 슬롯 (angry, worried, happy, proud, confused, sleeping) + 각각 미리보기
|
|
309
|
+
4. **스프라이트 시트**: 파일 업로드 + columns/rows 숫자 입력 + 무드별 (col, row) 매핑
|
|
310
|
+
5. **기본으로 되돌리기** 버튼
|
|
311
|
+
|
|
312
|
+
### Files to Create/Modify
|
|
313
|
+
|
|
314
|
+
| File | Change |
|
|
315
|
+
|------|--------|
|
|
316
|
+
| `src/shared/types.ts` | `SkinConfig`, `SkinMode` types, IPC channels |
|
|
317
|
+
| `src/main/skin-manager.ts` | **New** — 파일 복사, 경로 관리 |
|
|
318
|
+
| `src/main/ipc-handlers.ts` | Skin upload/reset IPC handlers |
|
|
319
|
+
| `src/main/preload.ts` | Skin API exposure |
|
|
320
|
+
| `src/renderer/components/Character.tsx` | skinSrc 로직, spritesheet 렌더링 |
|
|
321
|
+
| `src/renderer/pages/Settings.tsx` | 캐릭터 섹션 UI |
|
|
322
|
+
| `src/renderer/electron.d.ts` | Skin API types |
|
|
323
|
+
| `src/shared/i18n.ts` | Skin UI strings |
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Implementation Order
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
A (Always on Top) → B (Contextual Messages) → C (Badges) → D (Skins)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
각 feature는 독립적이며 순차적으로 구현 가능. B와 C는 TriggerContext를 공유하므로 B에서 확장한 context를 C에서 재활용.
|
|
334
|
+
|
|
335
|
+
## Testing Strategy
|
|
336
|
+
|
|
337
|
+
| Feature | Test Type |
|
|
338
|
+
|---------|-----------|
|
|
339
|
+
| A | 수동 테스트 (Electron window 동작) |
|
|
340
|
+
| B | Unit test: contextual trigger logic, message selection fallback |
|
|
341
|
+
| C | Unit test: badge trigger evaluation, badge manager unlock logic |
|
|
342
|
+
| D | 수동 테스트 (파일 업로드, 렌더링) |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
appId: com.claudepet.app
|
|
2
|
+
productName: Claude Pet
|
|
3
|
+
directories:
|
|
4
|
+
output: release
|
|
5
|
+
files:
|
|
6
|
+
- dist/**/*
|
|
7
|
+
- package.json
|
|
8
|
+
- "!dist/renderer/**/*.map"
|
|
9
|
+
asar: true
|
|
10
|
+
publish:
|
|
11
|
+
provider: github
|
|
12
|
+
owner: scm1400
|
|
13
|
+
repo: claude-pet
|
|
14
|
+
win:
|
|
15
|
+
target:
|
|
16
|
+
- target: nsis
|
|
17
|
+
arch:
|
|
18
|
+
- x64
|
|
19
|
+
icon: build/icon.ico
|
|
20
|
+
mac:
|
|
21
|
+
target:
|
|
22
|
+
- target: dmg
|
|
23
|
+
arch:
|
|
24
|
+
- x64
|
|
25
|
+
- arm64
|
|
26
|
+
- target: zip
|
|
27
|
+
arch:
|
|
28
|
+
- x64
|
|
29
|
+
- arm64
|
|
30
|
+
icon: build/icon.png
|
|
31
|
+
category: public.app-category.utilities
|
|
32
|
+
hardenedRuntime: true
|
|
33
|
+
gatekeeperAssess: true
|
|
34
|
+
entitlements: build/entitlements.mac.plist
|
|
35
|
+
entitlementsInherit: build/entitlements.mac.plist
|
|
36
|
+
notarize: false
|
|
37
|
+
#afterSign: scripts/notarize.js
|
|
38
|
+
linux:
|
|
39
|
+
target:
|
|
40
|
+
- target: AppImage
|
|
41
|
+
arch:
|
|
42
|
+
- x64
|
|
43
|
+
- target: deb
|
|
44
|
+
arch:
|
|
45
|
+
- x64
|
|
46
|
+
icon: build/icon.png
|
|
47
|
+
category: Utility
|
|
48
|
+
nsis:
|
|
49
|
+
oneClick: true
|
|
50
|
+
allowToChangeInstallationDirectory: false
|
|
51
|
+
perMachine: false
|
|
52
|
+
createDesktopShortcut: true
|
|
53
|
+
createStartMenuShortcut: true
|
|
54
|
+
installerSidebar: build/installerSidebar.bmp
|
|
55
|
+
uninstallerSidebar: build/installerSidebar.bmp
|
|
56
|
+
installerHeader: build/installerHeader.bmp
|
|
57
|
+
dmg:
|
|
58
|
+
writeUpdateInfo: false
|
|
59
|
+
background: build/dmg-background.png
|
|
60
|
+
window:
|
|
61
|
+
width: 540
|
|
62
|
+
height: 380
|
|
63
|
+
contents:
|
|
64
|
+
- x: 140
|
|
65
|
+
y: 250
|
|
66
|
+
type: file
|
|
67
|
+
- x: 400
|
|
68
|
+
y: 250
|
|
69
|
+
type: link
|
|
70
|
+
path: /Applications
|
|
71
|
+
extraResources:
|
|
72
|
+
- from: build/tray-icon.png
|
|
73
|
+
to: tray-icon.png
|
|
74
|
+
extraMetadata:
|
|
75
|
+
main: dist/main/main/main.js
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-pet",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Claude Pet - virtual pet coding companion desktop widget",
|
|
5
|
+
"main": "dist/main/main/main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-pet": "bin/claude-pet.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"prepare": "npm run build",
|
|
11
|
+
"dev": "npm run build:main && concurrently -k -n VITE,ELECTRON -c cyan,yellow \"vite\" \"cross-env NODE_ENV=development electron dist/main/main/main.js\"",
|
|
12
|
+
"build:renderer": "vite build",
|
|
13
|
+
"build:main": "tsc -p tsconfig.main.json",
|
|
14
|
+
"build": "npm run build:renderer && npm run build:main",
|
|
15
|
+
"start": "electron dist/main/main/main.js",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"make-icon": "node scripts/make-icon.js",
|
|
18
|
+
"build:win": "electron-builder --win",
|
|
19
|
+
"build:mac": "electron-builder --mac",
|
|
20
|
+
"build:all": "electron-builder --win --mac"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"author": "scm1400",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^19.2.14",
|
|
28
|
+
"@types/react-dom": "^19.2.3",
|
|
29
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
30
|
+
"concurrently": "^9.2.1",
|
|
31
|
+
"cross-env": "^10.1.0",
|
|
32
|
+
"electron-builder": "^26.8.1",
|
|
33
|
+
"png-to-ico": "^3.0.1",
|
|
34
|
+
"sharp": "^0.34.5",
|
|
35
|
+
"tsx": "^4.21.0",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vite": "^7.3.1",
|
|
38
|
+
"vitest": "^4.0.18"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"auto-launch": "^5.0.6",
|
|
42
|
+
"electron": "^40.8.0",
|
|
43
|
+
"electron-store": "8.2.0",
|
|
44
|
+
"electron-updater": "^6.8.3",
|
|
45
|
+
"react": "^19.2.4",
|
|
46
|
+
"react-dom": "^19.2.4"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-03-14 -->
|
|
3
|
+
|
|
4
|
+
# scripts/
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Build-time utility scripts that run outside the Electron app. `make-icon.js` generates all platform-specific icon and installer image assets from the source character artwork. `notarize.js` is an electron-builder afterSign hook that submits the packaged macOS `.app` to Apple's notarization service.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
| File | Description |
|
|
11
|
+
|------|-------------|
|
|
12
|
+
| `make-icon.js` | Reads `docs/images/character.webp`, uses `sharp` to resize and composite, and writes: `build/icon.png` (512×512, macOS), `build/tray-icon.png` (32×32, runtime tray), `build/icon.ico` (256×256 via `png-to-ico`, Windows), `build/installerSidebar.bmp` (164×314, NSIS sidebar), `build/installerHeader.bmp` (150×57, NSIS header), `build/dmg-background.png` (540×380, macOS DMG). Run with `npm run make-icon`. |
|
|
13
|
+
| `notarize.js` | electron-builder `afterSign` hook. Calls `@electron/notarize` with `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID` environment variables. No-ops on non-macOS platforms. Invoked automatically by electron-builder during `npm run build:mac`. |
|
|
14
|
+
|
|
15
|
+
## Subdirectories
|
|
16
|
+
| Directory | Purpose |
|
|
17
|
+
|-----------|---------|
|
|
18
|
+
| *(none)* | — |
|
|
19
|
+
|
|
20
|
+
## For AI Agents
|
|
21
|
+
|
|
22
|
+
### Working In This Directory
|
|
23
|
+
- `make-icon.js` uses CommonJS `require()` — it is a plain Node script, not compiled TypeScript.
|
|
24
|
+
- The source artwork must be at `docs/images/character.webp`. If the character art changes, re-run `npm run make-icon` to regenerate all build assets.
|
|
25
|
+
- `notarize.js` requires three environment variables at build time: `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`. These are set as GitHub Actions secrets in `.github/workflows/build.yml`.
|
|
26
|
+
- Do not run `notarize.js` manually — it is invoked by electron-builder's `afterSign` hook configured in `electron-builder.yml`.
|
|
27
|
+
- The NSIS installer images (`.bmp`) must be exact pixel dimensions: sidebar 164×314, header 150×57. `sharp` enforces this via `removeAlpha()` + BMP output.
|
|
28
|
+
- The DMG background (540×380 PNG) must have a transparent-safe format — `sharp` outputs PNG here (not BMP).
|
|
29
|
+
|
|
30
|
+
### Testing Requirements
|
|
31
|
+
- Run `npm run make-icon` and verify all six output files appear in `build/` with correct dimensions using any image viewer.
|
|
32
|
+
- `notarize.js` is tested implicitly by the macOS CI job in `.github/workflows/build.yml`.
|
|
33
|
+
|
|
34
|
+
### Common Patterns
|
|
35
|
+
- Sharp pipeline pattern used throughout `make-icon.js`:
|
|
36
|
+
```js
|
|
37
|
+
await sharp(src)
|
|
38
|
+
.resize(W, H, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
39
|
+
.png()
|
|
40
|
+
.toFile(outPath);
|
|
41
|
+
```
|
|
42
|
+
- Composite (SVG background + character image):
|
|
43
|
+
```js
|
|
44
|
+
await sharp(Buffer.from(svgString))
|
|
45
|
+
.composite([{ input: charBuf, left: x, top: y }])
|
|
46
|
+
.removeAlpha()
|
|
47
|
+
.toFile(outPath);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Dependencies
|
|
51
|
+
### Internal
|
|
52
|
+
- Reads from `docs/images/character.webp`
|
|
53
|
+
- Writes to `build/`
|
|
54
|
+
|
|
55
|
+
### External
|
|
56
|
+
- `sharp` ^0.34 — image processing
|
|
57
|
+
- `png-to-ico` ^3 — PNG-to-ICO conversion
|
|
58
|
+
- `@electron/notarize` — Apple notarization (consumed by `notarize.js`)
|
|
59
|
+
|
|
60
|
+
<!-- MANUAL: -->
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Claude Pet Installer for Windows
|
|
2
|
+
# Usage: irm https://raw.githubusercontent.com/scm1400/claude-pet/main/scripts/install.ps1 | iex
|
|
3
|
+
$ErrorActionPreference = "Stop"
|
|
4
|
+
|
|
5
|
+
$Repo = "scm1400/claude-pet"
|
|
6
|
+
$ApiUrl = "https://api.github.com/repos/$Repo/releases/latest"
|
|
7
|
+
|
|
8
|
+
function Info($msg) { Write-Host "→ $msg" -ForegroundColor Blue }
|
|
9
|
+
function Ok($msg) { Write-Host "✓ $msg" -ForegroundColor Green }
|
|
10
|
+
function Err($msg) { Write-Host "✗ $msg" -ForegroundColor Red; exit 1 }
|
|
11
|
+
|
|
12
|
+
Info "Fetching latest release..."
|
|
13
|
+
try {
|
|
14
|
+
$release = Invoke-RestMethod -Uri $ApiUrl -Headers @{ "User-Agent" = "claude-pet-installer" }
|
|
15
|
+
} catch {
|
|
16
|
+
Err "Failed to fetch release info: $_"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
$version = $release.tag_name
|
|
20
|
+
Info "Latest version: $version"
|
|
21
|
+
|
|
22
|
+
# Find .exe installer asset
|
|
23
|
+
$asset = $release.assets | Where-Object { $_.name -match '\.exe$' } | Select-Object -First 1
|
|
24
|
+
if (-not $asset) {
|
|
25
|
+
Err "No Windows installer found in release $version"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$downloadUrl = $asset.browser_download_url
|
|
29
|
+
$fileName = $asset.name
|
|
30
|
+
$tempPath = Join-Path $env:TEMP $fileName
|
|
31
|
+
|
|
32
|
+
Info "Downloading $fileName..."
|
|
33
|
+
try {
|
|
34
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
35
|
+
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempPath -UseBasicParsing
|
|
36
|
+
} catch {
|
|
37
|
+
Err "Download failed: $_"
|
|
38
|
+
}
|
|
39
|
+
Ok "Downloaded to $tempPath"
|
|
40
|
+
|
|
41
|
+
Info "Running installer..."
|
|
42
|
+
Start-Process -FilePath $tempPath -Wait
|
|
43
|
+
Ok "Installation complete!"
|
|
44
|
+
|
|
45
|
+
# Cleanup
|
|
46
|
+
Remove-Item -Path $tempPath -ErrorAction SilentlyContinue
|
|
47
|
+
Info "Claude Pet should now be available in your Start Menu."
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Claude Pet Installer — downloads the latest release from GitHub
|
|
5
|
+
REPO="scm1400/claude-pet"
|
|
6
|
+
API_URL="https://api.github.com/repos/$REPO/releases/latest"
|
|
7
|
+
|
|
8
|
+
info() { printf "\033[1;34m→\033[0m %s\n" "$*"; }
|
|
9
|
+
ok() { printf "\033[1;32m✓\033[0m %s\n" "$*"; }
|
|
10
|
+
err() { printf "\033[1;31m✗\033[0m %s\n" "$*" >&2; exit 1; }
|
|
11
|
+
|
|
12
|
+
# Detect OS and architecture
|
|
13
|
+
OS="$(uname -s)"
|
|
14
|
+
ARCH="$(uname -m)"
|
|
15
|
+
|
|
16
|
+
case "$OS" in
|
|
17
|
+
Darwin) PLATFORM="mac" ;;
|
|
18
|
+
Linux) PLATFORM="linux" ;;
|
|
19
|
+
*) err "Unsupported OS: $OS. Use install.ps1 for Windows." ;;
|
|
20
|
+
esac
|
|
21
|
+
|
|
22
|
+
case "$ARCH" in
|
|
23
|
+
x86_64|amd64) ARCH_LABEL="x64" ;;
|
|
24
|
+
arm64|aarch64) ARCH_LABEL="arm64" ;;
|
|
25
|
+
*) err "Unsupported architecture: $ARCH" ;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
info "Detected: $OS ($ARCH_LABEL)"
|
|
29
|
+
|
|
30
|
+
# Fetch latest release info
|
|
31
|
+
info "Fetching latest release..."
|
|
32
|
+
RELEASE_JSON=$(curl -fsSL "$API_URL") || err "Failed to fetch release info"
|
|
33
|
+
VERSION=$(echo "$RELEASE_JSON" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"//;s/".*//')
|
|
34
|
+
info "Latest version: $VERSION"
|
|
35
|
+
|
|
36
|
+
# Determine download asset
|
|
37
|
+
if [ "$PLATFORM" = "mac" ]; then
|
|
38
|
+
if [ "$ARCH_LABEL" = "arm64" ]; then
|
|
39
|
+
PATTERN="arm64.*\.dmg"
|
|
40
|
+
else
|
|
41
|
+
PATTERN="x64.*\.dmg\|Claude.Pet.*\.dmg"
|
|
42
|
+
fi
|
|
43
|
+
ASSET_URL=$(echo "$RELEASE_JSON" | grep '"browser_download_url"' | grep -i '\.dmg"' | head -1 | sed 's/.*"browser_download_url": *"//;s/".*//')
|
|
44
|
+
FILENAME="Claude-Pet-${VERSION}.dmg"
|
|
45
|
+
elif [ "$PLATFORM" = "linux" ]; then
|
|
46
|
+
# Prefer AppImage
|
|
47
|
+
ASSET_URL=$(echo "$RELEASE_JSON" | grep '"browser_download_url"' | grep -i '\.AppImage"' | head -1 | sed 's/.*"browser_download_url": *"//;s/".*//')
|
|
48
|
+
if [ -z "$ASSET_URL" ]; then
|
|
49
|
+
ASSET_URL=$(echo "$RELEASE_JSON" | grep '"browser_download_url"' | grep -i '\.deb"' | head -1 | sed 's/.*"browser_download_url": *"//;s/".*//')
|
|
50
|
+
FILENAME="claude-pet-${VERSION}.deb"
|
|
51
|
+
else
|
|
52
|
+
FILENAME="Claude-Pet-${VERSION}.AppImage"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
[ -z "${ASSET_URL:-}" ] && err "No installer found for $PLATFORM ($ARCH_LABEL)"
|
|
57
|
+
|
|
58
|
+
TMPDIR="${TMPDIR:-/tmp}"
|
|
59
|
+
DOWNLOAD_PATH="$TMPDIR/$FILENAME"
|
|
60
|
+
|
|
61
|
+
info "Downloading $FILENAME..."
|
|
62
|
+
curl -fSL --progress-bar -o "$DOWNLOAD_PATH" "$ASSET_URL" || err "Download failed"
|
|
63
|
+
ok "Downloaded to $DOWNLOAD_PATH"
|
|
64
|
+
|
|
65
|
+
# Install
|
|
66
|
+
if [ "$PLATFORM" = "mac" ]; then
|
|
67
|
+
info "Mounting DMG..."
|
|
68
|
+
MOUNT_POINT=$(hdiutil attach "$DOWNLOAD_PATH" -nobrowse | tail -1 | awk '{print $NF}')
|
|
69
|
+
APP_PATH=$(find "$MOUNT_POINT" -name "*.app" -maxdepth 1 | head -1)
|
|
70
|
+
if [ -n "$APP_PATH" ]; then
|
|
71
|
+
info "Installing to /Applications..."
|
|
72
|
+
cp -R "$APP_PATH" /Applications/
|
|
73
|
+
hdiutil detach "$MOUNT_POINT" -quiet
|
|
74
|
+
ok "Claude Pet installed to /Applications!"
|
|
75
|
+
info "Run: open '/Applications/Claude Pet.app'"
|
|
76
|
+
else
|
|
77
|
+
hdiutil detach "$MOUNT_POINT" -quiet
|
|
78
|
+
err "No .app found in DMG"
|
|
79
|
+
fi
|
|
80
|
+
elif [ "$PLATFORM" = "linux" ]; then
|
|
81
|
+
if [[ "$FILENAME" == *.AppImage ]]; then
|
|
82
|
+
INSTALL_DIR="${HOME}/.local/bin"
|
|
83
|
+
mkdir -p "$INSTALL_DIR"
|
|
84
|
+
mv "$DOWNLOAD_PATH" "$INSTALL_DIR/claude-pet"
|
|
85
|
+
chmod +x "$INSTALL_DIR/claude-pet"
|
|
86
|
+
ok "Claude Pet installed to $INSTALL_DIR/claude-pet"
|
|
87
|
+
info "Make sure $INSTALL_DIR is in your PATH"
|
|
88
|
+
info "Run: claude-pet"
|
|
89
|
+
elif [[ "$FILENAME" == *.deb ]]; then
|
|
90
|
+
info "Installing .deb package..."
|
|
91
|
+
sudo dpkg -i "$DOWNLOAD_PATH" || sudo apt-get install -f -y
|
|
92
|
+
ok "Claude Pet installed!"
|
|
93
|
+
info "Run: claude-pet"
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
rm -f "$DOWNLOAD_PATH" 2>/dev/null || true
|
|
98
|
+
ok "Installation complete!"
|