@juho0719/cckit 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/assets/hooks/agent-track.sh +53 -0
  2. package/assets/hooks/auto-commit-push.sh +123 -0
  3. package/assets/hooks/on-prompt-start.sh +6 -0
  4. package/assets/hooks/skill-track.sh +26 -0
  5. package/assets/hooks/subagent-notify.sh +20 -0
  6. package/assets/skills/plan-viewer/SKILL.md +162 -0
  7. package/assets/skills/plan-viewer/scripts/frame-template.html +266 -0
  8. package/assets/skills/plan-viewer/scripts/helper.js +84 -0
  9. package/assets/skills/plan-viewer/scripts/server.js +334 -0
  10. package/assets/skills/plan-viewer/scripts/start-server.sh +122 -0
  11. package/assets/skills/plan-viewer/scripts/stop-server.sh +27 -0
  12. package/assets/skills/plan-viewer/spec-reviewer-prompt.md +47 -0
  13. package/assets/skills/plan-viewer/visual-guide.md +234 -0
  14. package/assets/skills/scaffold/SKILL.md +119 -0
  15. package/assets/skills/scaffold/presets.md +94 -0
  16. package/assets/skills/scaffold/scripts/common.sh +73 -0
  17. package/assets/skills/scaffold/scripts/monorepo.sh +449 -0
  18. package/assets/skills/scaffold/scripts/nextjs-fullstack.sh +305 -0
  19. package/assets/statusline/statusline.sh +186 -0
  20. package/dist/{chunk-OLLOS3GG.js → chunk-E3INXQNO.js} +38 -8
  21. package/dist/{chunk-SLVASXTF.js → chunk-W7RWPDBH.js} +13 -3
  22. package/dist/{cli-6WQMAFNA.js → cli-KZYBSIXO.js} +43 -14
  23. package/dist/{hooks-S73JTX2I.js → hooks-A2WQ2LGG.js} +1 -1
  24. package/dist/index.js +10 -10
  25. package/dist/{registry-BU55RMHU.js → registry-KRLOB4TH.js} +1 -1
  26. package/dist/{uninstall-cli-7XGNDIUC.js → uninstall-cli-GLYJG5V2.js} +2 -2
  27. package/package.json +1 -1
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/common.sh"
6
+
7
+ # ── Defaults ──────────────────────────────────────────────────────────────────
8
+ DB="sqlite"
9
+ UI="shadcn"
10
+ PM="bun"
11
+
12
+ # ── Usage ─────────────────────────────────────────────────────────────────────
13
+ usage() {
14
+ error "사용법: bash nextjs-fullstack.sh <project-name> [--db sqlite|postgres] [--ui shadcn|none] [--pm bun|npm|pnpm]"
15
+ exit 1
16
+ }
17
+
18
+ if [ $# -lt 1 ]; then
19
+ usage
20
+ fi
21
+
22
+ PROJECT_ARG="$1"
23
+ shift
24
+
25
+ # ── Flag parsing ───────────────────────────────────────────────────────────────
26
+ while [ $# -gt 0 ]; do
27
+ case "$1" in
28
+ --db)
29
+ DB="$2"; shift 2 ;;
30
+ --ui)
31
+ UI="$2"; shift 2 ;;
32
+ --pm)
33
+ PM="$2"; shift 2 ;;
34
+ *)
35
+ error "알 수 없는 옵션: $1"; usage ;;
36
+ esac
37
+ done
38
+
39
+ # ── Package manager helpers ────────────────────────────────────────────────────
40
+ case "$PM" in
41
+ bun)
42
+ PKG_X="bunx"
43
+ PKG_ADD="bun add"
44
+ PKG_ADD_DEV="bun add -d"
45
+ PKG_RUN="bun run"
46
+ CREATE_FLAG="--use-bun"
47
+ ;;
48
+ npm)
49
+ PKG_X="npx"
50
+ PKG_ADD="npm install"
51
+ PKG_ADD_DEV="npm install --save-dev"
52
+ PKG_RUN="npm run"
53
+ CREATE_FLAG="--use-npm"
54
+ ;;
55
+ pnpm)
56
+ PKG_X="pnpm dlx"
57
+ PKG_ADD="pnpm add"
58
+ PKG_ADD_DEV="pnpm add -D"
59
+ PKG_RUN="pnpm run"
60
+ CREATE_FLAG="--use-pnpm"
61
+ ;;
62
+ *)
63
+ error "지원하지 않는 패키지 매니저: $PM (bun|npm|pnpm)"; exit 1 ;;
64
+ esac
65
+
66
+ # ── Prerequisite check ─────────────────────────────────────────────────────────
67
+ step "사전 조건 확인"
68
+ check_command "$PM" "https://bun.sh / https://npmjs.com / https://pnpm.io"
69
+ check_command git "https://git-scm.com"
70
+ check_command node "https://nodejs.org"
71
+
72
+ # ── Resolve target directory ───────────────────────────────────────────────────
73
+ resolve_target_dir "$PROJECT_ARG"
74
+
75
+ # ── create-next-app ────────────────────────────────────────────────────────────
76
+ step "create-next-app 실행"
77
+ if [ "$INIT_IN_PLACE" = true ]; then
78
+ $PKG_X create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" $CREATE_FLAG --yes
79
+ else
80
+ $PKG_X create-next-app@latest "$PROJECT_NAME" --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" $CREATE_FLAG --yes
81
+ fi
82
+ success "create-next-app 완료"
83
+
84
+ cd "$TARGET_DIR"
85
+
86
+ # ── Drizzle installation ───────────────────────────────────────────────────────
87
+ if [ "$DB" = "sqlite" ]; then
88
+ step "Drizzle ORM + better-sqlite3 설치"
89
+ $PKG_ADD drizzle-orm better-sqlite3
90
+ $PKG_ADD_DEV drizzle-kit @types/better-sqlite3
91
+ success "Drizzle (sqlite) 설치 완료"
92
+ elif [ "$DB" = "postgres" ]; then
93
+ step "Drizzle ORM + pg 설치"
94
+ $PKG_ADD drizzle-orm pg
95
+ $PKG_ADD_DEV drizzle-kit @types/pg
96
+ success "Drizzle (postgres) 설치 완료"
97
+ else
98
+ error "지원하지 않는 DB: $DB (sqlite|postgres)"; exit 1
99
+ fi
100
+
101
+ # ── DB schema & index ─────────────────────────────────────────────────────────
102
+ step "DB 디렉토리 및 기본 스키마 생성"
103
+ mkdir -p src/lib/db
104
+ mkdir -p src/lib/db/migrations
105
+
106
+ if [ "$DB" = "sqlite" ]; then
107
+ cat > src/lib/db/schema.ts <<'EOF'
108
+ import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'
109
+
110
+ export const users = sqliteTable('users', {
111
+ id: int('id').primaryKey({ autoIncrement: true }),
112
+ name: text('name').notNull(),
113
+ email: text('email').notNull().unique(),
114
+ createdAt: int('created_at', { mode: 'timestamp' })
115
+ .$defaultFn(() => new Date())
116
+ .notNull(),
117
+ })
118
+
119
+ export type User = typeof users.$inferSelect
120
+ export type NewUser = typeof users.$inferInsert
121
+ EOF
122
+
123
+ cat > src/lib/db/index.ts <<'EOF'
124
+ import Database from 'better-sqlite3'
125
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
126
+ import * as schema from './schema'
127
+
128
+ const sqlite = new Database(process.env.DATABASE_URL ?? 'sqlite.db')
129
+ sqlite.pragma('journal_mode = WAL')
130
+
131
+ export const db = drizzle(sqlite, { schema })
132
+ EOF
133
+
134
+ elif [ "$DB" = "postgres" ]; then
135
+ cat > src/lib/db/schema.ts <<'EOF'
136
+ import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'
137
+
138
+ export const users = pgTable('users', {
139
+ id: serial('id').primaryKey(),
140
+ name: text('name').notNull(),
141
+ email: text('email').notNull().unique(),
142
+ createdAt: timestamp('created_at').defaultNow().notNull(),
143
+ })
144
+
145
+ export type User = typeof users.$inferSelect
146
+ export type NewUser = typeof users.$inferInsert
147
+ EOF
148
+
149
+ cat > src/lib/db/index.ts <<EOF
150
+ import { Pool } from 'pg'
151
+ import { drizzle } from 'drizzle-orm/node-postgres'
152
+ import * as schema from './schema'
153
+
154
+ const pool = new Pool({
155
+ connectionString: process.env.DATABASE_URL,
156
+ })
157
+
158
+ export const db = drizzle(pool, { schema })
159
+ EOF
160
+ fi
161
+
162
+ success "DB 파일 생성 완료"
163
+
164
+ # ── drizzle.config.ts ─────────────────────────────────────────────────────────
165
+ step "drizzle.config.ts 생성"
166
+ if [ "$DB" = "sqlite" ]; then
167
+ cat > drizzle.config.ts <<'EOF'
168
+ import type { Config } from 'drizzle-kit'
169
+
170
+ export default {
171
+ schema: './src/lib/db/schema.ts',
172
+ out: './src/lib/db/migrations',
173
+ dialect: 'sqlite',
174
+ dbCredentials: {
175
+ url: process.env.DATABASE_URL ?? 'sqlite.db',
176
+ },
177
+ } satisfies Config
178
+ EOF
179
+ elif [ "$DB" = "postgres" ]; then
180
+ cat > drizzle.config.ts <<'EOF'
181
+ import type { Config } from 'drizzle-kit'
182
+
183
+ export default {
184
+ schema: './src/lib/db/schema.ts',
185
+ out: './src/lib/db/migrations',
186
+ dialect: 'postgresql',
187
+ dbCredentials: {
188
+ url: process.env.DATABASE_URL!,
189
+ },
190
+ } satisfies Config
191
+ EOF
192
+ fi
193
+ success "drizzle.config.ts 생성 완료"
194
+
195
+ # ── .env.local ────────────────────────────────────────────────────────────────
196
+ step ".env.local 생성"
197
+ if [ "$DB" = "sqlite" ]; then
198
+ ENV_DB_LINE="DATABASE_URL=sqlite.db"
199
+ elif [ "$DB" = "postgres" ]; then
200
+ ENV_DB_LINE="DATABASE_URL=postgresql://localhost:5432/${PROJECT_NAME}"
201
+ fi
202
+
203
+ if [ ! -f ".env.local" ]; then
204
+ echo "$ENV_DB_LINE" > .env.local
205
+ success ".env.local 생성 완료"
206
+ else
207
+ if ! grep -q "^DATABASE_URL=" .env.local; then
208
+ echo "" >> .env.local
209
+ echo "$ENV_DB_LINE" >> .env.local
210
+ success ".env.local에 DATABASE_URL 추가 완료"
211
+ else
212
+ info ".env.local에 이미 DATABASE_URL 존재 — 건너뜀"
213
+ fi
214
+ fi
215
+
216
+ # ── .gitignore ────────────────────────────────────────────────────────────────
217
+ step ".gitignore 업데이트"
218
+ if [ "$DB" = "sqlite" ]; then
219
+ if ! grep -q "sqlite.db" .gitignore 2>/dev/null; then
220
+ cat >> .gitignore <<'EOF'
221
+
222
+ # SQLite
223
+ *.db
224
+ *.db-shm
225
+ *.db-wal
226
+ EOF
227
+ success ".gitignore에 sqlite.db 패턴 추가"
228
+ else
229
+ info ".gitignore에 이미 sqlite.db 패턴 존재 — 건너뜀"
230
+ fi
231
+ fi
232
+
233
+ # ── package.json scripts ───────────────────────────────────────────────────────
234
+ step "package.json에 Drizzle 스크립트 추가"
235
+ node -e "
236
+ const fs = require('fs');
237
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
238
+ pkg.scripts = {
239
+ ...pkg.scripts,
240
+ 'db:generate': 'drizzle-kit generate',
241
+ 'db:migrate': 'drizzle-kit migrate',
242
+ 'db:push': 'drizzle-kit push',
243
+ 'db:studio': 'drizzle-kit studio',
244
+ };
245
+ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
246
+ "
247
+ success "package.json 스크립트 추가 완료"
248
+
249
+ # ── shadcn/ui ─────────────────────────────────────────────────────────────────
250
+ if [ "$UI" = "shadcn" ]; then
251
+ step "shadcn/ui 초기화"
252
+ printf '\n' | $PKG_X shadcn@latest init --yes
253
+ success "shadcn/ui 초기화 완료"
254
+
255
+ step "기본 shadcn/ui 컴포넌트 설치 (button, input, card)"
256
+ $PKG_X shadcn@latest add button input card --yes
257
+ success "shadcn/ui 컴포넌트 설치 완료"
258
+ elif [ "$UI" = "none" ]; then
259
+ info "UI 라이브러리 건너뜀 (--ui none)"
260
+ else
261
+ error "지원하지 않는 UI: $UI (shadcn|none)"; exit 1
262
+ fi
263
+
264
+ # ── Initial DB migration ───────────────────────────────────────────────────────
265
+ step "초기 DB 마이그레이션 생성"
266
+ $PKG_RUN db:generate
267
+ success "마이그레이션 파일 생성 완료"
268
+
269
+ # ── Git commit ────────────────────────────────────────────────────────────────
270
+ step "Git 커밋"
271
+ COMMIT_MSG="chore: add"
272
+ [ "$UI" = "shadcn" ] && COMMIT_MSG="$COMMIT_MSG shadcn/ui +"
273
+ COMMIT_MSG="$COMMIT_MSG drizzle ${DB} setup"
274
+ git_init_commit "$COMMIT_MSG"
275
+
276
+ # ── Summary ───────────────────────────────────────────────────────────────────
277
+ echo ""
278
+ echo -e "${GREEN}${BOLD}✓ 프로젝트 초기화 완료: ${PROJECT_NAME}${RESET}"
279
+ echo ""
280
+ echo -e " ${BOLD}옵션:${RESET} DB=${DB} UI=${UI} PM=${PM}"
281
+ echo ""
282
+ echo -e " ${BOLD}주요 구조:${RESET}"
283
+ echo -e " src/"
284
+ echo -e " app/ — Next.js App Router"
285
+ echo -e " ${CYAN}lib/db/${RESET} — Drizzle ORM"
286
+ echo -e " index.ts — DB 연결"
287
+ echo -e " schema.ts — 테이블 스키마"
288
+ echo -e " migrations/ — 마이그레이션 파일"
289
+ if [ "$UI" = "shadcn" ]; then
290
+ echo -e " ${CYAN}components/ui/${RESET} — shadcn/ui 컴포넌트"
291
+ fi
292
+ echo ""
293
+ echo -e " ${BOLD}DB 명령어:${RESET}"
294
+ echo -e " $PKG_RUN db:generate — 스키마 변경 후 마이그레이션 생성"
295
+ echo -e " $PKG_RUN db:migrate — 마이그레이션 적용"
296
+ echo -e " $PKG_RUN db:push — 마이그레이션 없이 스키마 직접 적용"
297
+ echo -e " $PKG_RUN db:studio — Drizzle Studio (DB GUI)"
298
+ echo ""
299
+ echo -e " ${BOLD}개발 시작:${RESET}"
300
+ if [ "$INIT_IN_PLACE" = false ]; then
301
+ echo -e " cd ${PROJECT_NAME}"
302
+ fi
303
+ echo -e " $PKG_RUN db:migrate — 첫 마이그레이션 적용"
304
+ echo -e " $PKG_RUN dev — 개발 서버 시작"
305
+ echo ""
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bash
2
+ # Docs:
3
+ # - https://code.claude.com/docs/en/statusline
4
+ # - https://code.claude.com/docs/en/settings
5
+ #
6
+ # Notes:
7
+ # - Claude Code reruns the statusline when conversation state changes, debounced at about 300ms.
8
+ # - `agent.name` is officially provided.
9
+ # - Active skills are not officially exposed in the statusline JSON, so `skills` below is a best-effort
10
+ # inference from recent `/skill-name` or `$skill-name` mentions in the transcript.
11
+
12
+ if ! command -v jq >/dev/null 2>&1; then
13
+ printf '[--] | 📁 -- | default\n'
14
+ printf '◉ CTX unavailable | 🤖 main | 🧩 --\n'
15
+ exit 0
16
+ fi
17
+
18
+ input=$(cat 2>/dev/null)
19
+ [ -n "$input" ] || exit 0
20
+
21
+ repeat_char() {
22
+ local count="$1"
23
+ local char="$2"
24
+ local out=""
25
+ while [ "$count" -gt 0 ]; do
26
+ out="${out}${char}"
27
+ count=$((count - 1))
28
+ done
29
+ printf '%s' "$out"
30
+ }
31
+
32
+ shorten() {
33
+ local text="$1"
34
+ local max="${2:-24}"
35
+ if [ "${#text}" -le "$max" ]; then
36
+ printf '%s' "$text"
37
+ else
38
+ printf '%s…' "${text:0:max-1}"
39
+ fi
40
+ }
41
+
42
+ current_dir_name() {
43
+ local path="$1"
44
+ path="${path%/}"
45
+
46
+ if [ -z "$path" ]; then
47
+ printf '/'
48
+ elif [ "$path" = "$HOME" ]; then
49
+ printf '~'
50
+ else
51
+ printf '%s' "${path##*/}"
52
+ fi
53
+ }
54
+
55
+ current_effort() {
56
+ if [ "${MAX_THINKING_TOKENS:-}" = "0" ]; then
57
+ printf 'off'
58
+ elif [ -n "${CLAUDE_CODE_EFFORT_LEVEL:-}" ]; then
59
+ printf '%s' "$CLAUDE_CODE_EFFORT_LEVEL"
60
+ elif [ "${CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING:-}" = "1" ] && [ -n "${MAX_THINKING_TOKENS:-}" ]; then
61
+ printf 'think:%s' "$MAX_THINKING_TOKENS"
62
+ elif [ "${CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING:-}" = "1" ]; then
63
+ printf 'fixed'
64
+ else
65
+ printf 'default'
66
+ fi
67
+ }
68
+
69
+ infer_skills() {
70
+ local transcript="$1"
71
+ local skills
72
+
73
+ if [ -z "$transcript" ] || [ ! -f "$transcript" ]; then
74
+ printf '%s' '--'
75
+ return
76
+ fi
77
+
78
+ skills=$(
79
+ tail -n 200 "$transcript" 2>/dev/null |
80
+ jq -r '.. | .text? // empty' 2>/dev/null |
81
+ grep -Eo '(^|[[:space:]])(\$|/)[A-Za-z][A-Za-z0-9_-]+' |
82
+ sed -E 's/^[[:space:]]+//; s#^[/$]##' |
83
+ grep -Evi '^(add-dir|agents|bug|clear|compact|config|cost|doctor|help|init|login|logout|mcp|memory|model|permissions|plugin|plugins|pr-comments|review|status|statusline|terminal-setup|vim)$' |
84
+ awk '!seen[$0]++' |
85
+ head -n 3 |
86
+ awk 'BEGIN{ORS=""} {if (NR>1) printf ", "; printf $0}'
87
+ )
88
+
89
+ printf '%s' "${skills:---}"
90
+ }
91
+
92
+ ctx_color() {
93
+ local pct="$1"
94
+ if [ "$pct" -ge 85 ]; then
95
+ printf '\033[38;2;255;107;107m'
96
+ elif [ "$pct" -ge 60 ]; then
97
+ printf '\033[38;2;255;184;77m'
98
+ else
99
+ printf '\033[38;2;92;224;163m'
100
+ fi
101
+ }
102
+
103
+ IFS=$'\t' read -r model dir agent transcript ctx_size cur_in cur_cache_create cur_cache_read used_fallback worktree_branch < <(
104
+ printf '%s' "$input" | jq -r '[
105
+ .model.display_name // .model.id // "--",
106
+ .workspace.current_dir // .cwd // "",
107
+ .agent.name // "main",
108
+ .transcript_path // "",
109
+ (.context_window.context_window_size // 0),
110
+ (.context_window.current_usage.input_tokens? // 0),
111
+ (.context_window.current_usage.cache_creation_input_tokens? // 0),
112
+ (.context_window.current_usage.cache_read_input_tokens? // 0),
113
+ (.context_window.used_percentage // 0 | floor),
114
+ .worktree.branch // ""
115
+ ] | @tsv'
116
+ )
117
+
118
+ dir="${dir:-$PWD}"
119
+ model="${model:---}"
120
+ agent="${agent:-main}"
121
+
122
+ used_tokens=$((cur_in + cur_cache_create + cur_cache_read))
123
+ if [ "$ctx_size" -gt 0 ] && [ "$used_tokens" -gt 0 ]; then
124
+ used_pct=$((used_tokens * 100 / ctx_size))
125
+ else
126
+ used_pct="${used_fallback:-0}"
127
+ fi
128
+
129
+ case "$used_pct" in
130
+ ''|*[!0-9]*)
131
+ used_pct=0
132
+ ;;
133
+ esac
134
+
135
+ [ "$used_pct" -lt 0 ] && used_pct=0
136
+ [ "$used_pct" -gt 100 ] && used_pct=100
137
+
138
+ branch="$worktree_branch"
139
+ if [ -z "$branch" ] && [ -n "$dir" ] && git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
140
+ branch="$(git -C "$dir" symbolic-ref --quiet --short HEAD 2>/dev/null || git -C "$dir" rev-parse --short HEAD 2>/dev/null)"
141
+ fi
142
+
143
+ skills="$(infer_skills "$transcript")"
144
+ effort="$(current_effort)"
145
+
146
+ dir_label="$(shorten "$(current_dir_name "$dir")" 24)"
147
+ branch_label="$(shorten "$branch" 26)"
148
+ agent_label="$(shorten "$agent" 18)"
149
+ skills_label="$(shorten "$skills" 32)"
150
+
151
+ bar_width=20
152
+ filled=$((used_pct * bar_width / 100))
153
+ empty=$((bar_width - filled))
154
+
155
+ RESET=$'\033[0m'
156
+ BOLD=$'\033[1m'
157
+ DIM=$'\033[2m'
158
+
159
+ MODEL_C=$'\033[1;38;2;110;200;255m'
160
+ DIR_C=$'\033[38;2;142;180;255m'
161
+ BRANCH_C=$'\033[38;2;120;230;160m'
162
+ EFFORT_C=$'\033[38;2;255;196;107m'
163
+ AGENT_C=$'\033[38;2;94;234;212m'
164
+ SKILL_C=$'\033[38;2;255;121;198m'
165
+ SEP_C=$'\033[38;2;108;114;134m'
166
+ FRAME_C=$'\033[38;2;96;103;122m'
167
+ TRACK_C=$'\033[38;2;62;68;84m'
168
+ LABEL_C=$'\033[38;2;145;215;255m'
169
+ PCT_C="$(ctx_color "$used_pct")"
170
+
171
+ filled_bar="$(repeat_char "$filled" '█')"
172
+ empty_bar="$(repeat_char "$empty" '░')"
173
+
174
+ line1="${MODEL_C}[${model}]${RESET} ${SEP_C}|${RESET} ${DIR_C}📁 ${dir_label}${RESET}"
175
+ if [ -n "$branch_label" ]; then
176
+ line1="${line1} ${SEP_C}|${RESET} ${BRANCH_C}⎇ ${branch_label}${RESET}"
177
+ fi
178
+ line1="${line1} ${SEP_C}|${RESET} ${EFFORT_C}${effort}${RESET}"
179
+
180
+ line2="${LABEL_C}◉ CTX ${FRAME_C}▕${PCT_C}${filled_bar}${TRACK_C}${empty_bar}${FRAME_C}▏ ${PCT_C}${used_pct}%${RESET}"
181
+ line2="${line2} ${SEP_C}|${RESET} ${AGENT_C}🤖 ${agent_label}${RESET}"
182
+ line2="${line2} ${SEP_C}|${RESET} ${SKILL_C}🧩 ${skills_label}${RESET}"
183
+
184
+ printf '%b\n' "$line1"
185
+ printf '%b\n' "$line2"
186
+
@@ -72,17 +72,47 @@ async function getCommands() {
72
72
  }
73
73
  async function getHooks() {
74
74
  const dir = join(getAssetsDir(), "hooks");
75
- const files = (await readdir(dir)).filter((f) => f.endsWith(".js"));
75
+ const allFiles = await readdir(dir);
76
+ const files = allFiles.filter((f) => f.endsWith(".js") || f.endsWith(".sh"));
76
77
  const items = [];
77
78
  for (const file of files) {
78
79
  const content = await readFile(join(dir, file), "utf-8");
79
- const descMatch = content.match(/\*\s+PostToolUse Hook:\s*(.+)/);
80
- const description = descMatch ? descMatch[1].trim() : file;
81
- const hookType = "PostToolUse";
82
- let matcher = "Edit";
83
- if (file.includes("typecheck")) matcher = "Edit|Write";
84
- if (file.includes("format")) matcher = "Edit|Write";
85
- items.push({ name: file, description, file, hookType, matcher });
80
+ if (file.endsWith(".js")) {
81
+ const descMatch = content.match(/\*\s+PostToolUse Hook:\s*(.+)/);
82
+ const description = descMatch ? descMatch[1].trim() : file;
83
+ const hookType = "PostToolUse";
84
+ let matcher = "Edit";
85
+ if (file.includes("typecheck")) matcher = "Edit|Write";
86
+ if (file.includes("format")) matcher = "Edit|Write";
87
+ items.push({ name: file, description, file, hookType, matcher });
88
+ } else {
89
+ const lines = content.split("\n");
90
+ const descLine = lines.find(
91
+ (l) => l.startsWith("#") && !l.startsWith("#!") && !l.includes("@hook-event")
92
+ );
93
+ const description = descLine ? descLine.replace(/^#\s*/, "") : file;
94
+ const eventRegex = /^# @hook-event ([^|]+)\|([^|]*)\|?(.*)$/gm;
95
+ const registrations = [];
96
+ let match;
97
+ while ((match = eventRegex.exec(content)) !== null) {
98
+ registrations.push({
99
+ hookType: match[1].trim(),
100
+ matcher: match[2].trim(),
101
+ args: match[3].trim() || void 0
102
+ });
103
+ }
104
+ if (registrations.length === 0) continue;
105
+ const [first] = registrations;
106
+ items.push({
107
+ name: file,
108
+ description,
109
+ file,
110
+ hookType: first.hookType,
111
+ matcher: first.matcher,
112
+ args: first.args,
113
+ registrations: registrations.length > 1 ? registrations : void 0
114
+ });
115
+ }
86
116
  }
87
117
  return items;
88
118
  }
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  // src/installers/hooks.ts
13
13
  import { join } from "path";
14
- import { readFile, writeFile, access } from "fs/promises";
14
+ import { readFile, writeFile, access, chmod } from "fs/promises";
15
15
  import { constants } from "fs";
16
16
  async function mergeHookSettings(settingsPath, hookType, matcher, command) {
17
17
  let settings = {};
@@ -44,8 +44,18 @@ async function installHooks(hooks, targetDir) {
44
44
  const dest = join(hooksDir, hook.file);
45
45
  await backupIfExists(dest);
46
46
  await copyFileUtil(join(srcDir, hook.file), dest);
47
- const hookCommand = `node "$CLAUDE_PROJECT_DIR"/.claude/hooks/${hook.file}`;
48
- await mergeHookSettings(settingsPath, hook.hookType, hook.matcher, hookCommand);
47
+ if (hook.file.endsWith(".sh")) {
48
+ await chmod(dest, 493);
49
+ }
50
+ const runtime = hook.file.endsWith(".sh") ? "bash" : "node";
51
+ const regs = hook.registrations ?? [
52
+ { hookType: hook.hookType, matcher: hook.matcher, args: hook.args }
53
+ ];
54
+ for (const reg of regs) {
55
+ const argsStr = reg.args ? ` ${reg.args}` : "";
56
+ const hookCommand = `${runtime} "$CLAUDE_PROJECT_DIR"/.claude/hooks/${hook.file}${argsStr}`;
57
+ await mergeHookSettings(settingsPath, reg.hookType, reg.matcher, hookCommand);
58
+ }
49
59
  }
50
60
  }
51
61