tillerhand 3.27.1 → 3.28.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.
@@ -44,7 +44,7 @@ Kurulda olmayan bir üyeyi çağırma; ilgili alan kurulda yoksa durum bloğunda
44
44
  ## 4. Toplantı nasıl yapılır
45
45
 
46
46
  1. **Limit kontrolü** (`docs/workflow/usage-limits.md`): Seviye sarıysa üye sayısını yarıya indir; kırmızıysa kullanıcıya sor (kurulu hafiflet seçeneği dahil).
47
- 1b. **Deterministik kapı** (commit, görev sonu, faz sonu; plan toplantısında yok): `node .claude/hooks/gate.mjs --run`. AGENTS.md → Komutlar'daki lint, tip kontrolü ve test ile Semgrep çalışır; sonuç değişiklik imzasıyla saklanır. Kırmızıysa veya boşsa (hiçbir kontrol çalışmadı) commit kapısı açılmaz: önce düzelt ya da Komutlar'ı doldur. Kullanıcı ara commit için açıkça isterse `--approve "atlandı: kullanıcı isteği"`. Kapı "komut onayı gerekli" derse komutları kullanıcıya göster; onaylarsa `node .claude/hooks/gate.mjs --approve` (yeni veya değişen komut onaysız çalışmaz).
47
+ 1b. **Deterministik kapı** (commit, görev sonu, faz sonu; plan toplantısında yok): `node .claude/hooks/gate.mjs --run`. AGENTS.md → Komutlar'daki lint, tip kontrolü ve test ile Semgrep çalışır; sonuç değişiklik imzasıyla saklanır. Kırmızıysa veya boşsa (hiçbir kontrol çalışmadı) commit kapısı açılmaz: önce düzelt ya da Komutlar'ı doldur. Kullanıcı ara commit için açıkça isterse `node .claude/hooks/commit-gate.mjs --skip "kullanıcı isteği"` (her zaman onaya sorulur). Kapı "komut onayı gerekli" derse komutları kullanıcıya göster; onaylarsa `node .claude/hooks/gate.mjs --approve` (yeni veya değişen komut onaysız çalışmaz).
48
48
  2. **Üyeleri paralel çalıştır** (Agent aracı, her üye ayrı alt ajan). Her üyeye şunları ver: toplantı türü, kapsam (plan metni / diff aralığı / faz), görev kodu, kabul kriterleri ve kapı çıktısı (varsa). Commit toplantısında `model: "haiku"` ver.
49
49
  2b. **Konsey toplantısıysa** (§2) `.claude/board/council.md` aşama 1-4'ü uygula. Sentezi başkanın kararından kur; görüş ayrılığı satırına azınlık görüşünü yaz.
50
50
  3. **Sentezle** (ana ajan; konseyde başkanın kararına göre):
@@ -62,7 +62,7 @@ Kullanıcıya sorulacaklar: [üyelerin işaretlediği kararlar]
62
62
  5. **Kaydet:** `docs/workflow/board.md` → toplantı günlüğüne bir satır ekle. Kararları `docs/decisions/log.md` günlüğüne ekle. Açık kalan bulguları `docs/workflow/progress.md` → "Açık sorunlar"a yaz.
63
63
  6. **Commit toplantısı sonucu ONAY veya KOŞULLU ONAY ise** commit kapısını aç: `node .claude/hooks/commit-gate.mjs --approve "<katılan üyeler>"`. RET ise açma; önce kullanıcıya sor.
64
64
 
65
- Kullanıcı commit denetimini o an için atlamak isterse açıkça söylemesi gerekir. O durumda `node .claude/hooks/commit-gate.mjs --approve "atlandı: kullanıcı isteği"` çalıştır ve günlüğe yaz. Kalıcı olarak kapatmak için: `docs/workflow/board.md` → `**Commit denetimi:** kapalı`.
65
+ Kullanıcı commit denetimini o an için atlamak isterse açıkça söylemesi gerekir. O durumda `node .claude/hooks/commit-gate.mjs --skip "kullanıcı isteği"` çalıştır (onaya sorulur) ve günlüğe yaz. Kalıcı olarak kapatmak için: `docs/workflow/board.md` → `**Commit denetimi:** kapalı`.
66
66
 
67
67
  ## 5. Kurul nasıl kurulur (`/start` Kurul adımı veya `/board setup`)
68
68
 
@@ -31,7 +31,7 @@ Boşsa `docs/workflow/progress.md` → "Sıradaki adım" veya `docs/product/road
31
31
  6. Arayüz değiştiyse `impeccable:impeccable` → `critique` çalıştır; önemli bulguları düzeltip düzeltmeyeceğimi sor.
32
32
  6b. **Görev sonu kurul toplantısı** (`.claude/board/rules.md`; kurul kurulduysa): Değişikliğe ilgili en fazla 3 üye. RET varsa görev bitmez; bulguyu ve seçenekleri sor. Toplantıyı `docs/workflow/board.md` günlüğüne yaz.
33
33
  7. `AGENTS.md` → "Bitti tanımı" listesini tek tek kontrol et.
34
- 8. Kapsam kalkanını kapat: `node .claude/hooks/freeze-guard.mjs --plan-end`. `docs/product/roadmap.md`'de görevi işaretle; PRD'de sadece testi yeşil olan kriterleri işaretle. Testleri JUnit raporuyla çalıştır (`docs/architecture/stack.md` → Kabul testleri) ki pano güncellensin. Limiti tekrar oku ve `docs/workflow/cost-log.md`'ye satır ekle (başlangıç/bitiş 5 saatlik %, süre dk).
34
+ 8. Kapsam kalkanını kapat (komut kullanıcıya sorulur): `node .claude/hooks/freeze-guard.mjs --plan-end`. `docs/product/roadmap.md`'de görevi işaretle; PRD'de sadece testi yeşil olan kriterleri işaretle. Testleri JUnit raporuyla çalıştır (`docs/architecture/stack.md` → Kabul testleri) ki pano güncellensin. Limiti tekrar oku ve `docs/workflow/cost-log.md`'ye satır ekle (başlangıç/bitiş 5 saatlik %, süre dk).
35
35
  9. Rapor ver:
36
36
  - Ne yapıldı (2-3 satır)
37
37
  - Bu görevde alınan kararlar (kullanıcı / otomatik)
@@ -41,4 +41,4 @@ Bu adımda **kod yazma**, sadece plan çıkar.
41
41
 
42
42
  Planı onaylatmak için AskUserQuestion kullan: "Onayla / Şunu değiştir / Görevi böl". Onayımı bekle. Onaydan sonra `/build` ile uygulanacak.
43
43
 
44
- 8. **Onaydan sonra kapsam kalkanını aç:** `node .claude/hooks/freeze-guard.mjs --plan-save <görev kodu> <dosya...>` ("Dokunulacak dosyalar"daki yollar; bütün bir klasör için sonuna `/`). Plan aktifken listede olmayan mevcut dosyaya yazım (dosya aracıyla da, `sed -i`, `mv`, `>` gibi kabuk komutlarıyla da) kullanıcıya sorulur; yeni dosyalar ve `docs/` serbest. Uygularken listede olmayan bir dosya gerekirse kullanıcıya sor, onaylarsa `--plan-add <yol>`.
44
+ 8. **Onaydan sonra kapsam kalkanını aç:** `node .claude/hooks/freeze-guard.mjs --plan-save <görev kodu> <dosya...>` ("Dokunulacak dosyalar"daki yollar; bütün bir klasör için sonuna `/`). Komut kullanıcıya sorulur: listeyi görüp onaylar. Plan aktifken listede olmayan mevcut dosyaya yazım (dosya aracıyla da, `sed -i`, `mv`, `>` gibi kabuk komutlarıyla da) kullanıcıya sorulur; yeni dosyalar ve `docs/` serbest. Uygularken listede olmayan bir dosya gerekirse kullanıcıya sor, onaylarsa `--plan-add <yol>`.
@@ -2,6 +2,7 @@
2
2
  // Commit gate: before every AI `git commit` it requires a light board review and a green deterministic gate (gate.mjs).
3
3
  // PreToolUse (Bash) hook: without review approval it stops the commit with exit code 2 and says what to do.
4
4
  // node .claude/hooks/commit-gate.mjs --approve "<members who took part, or a note>" binds the approval to the current changes.
5
+ // node .claude/hooks/commit-gate.mjs --skip "<reason>" only when the user asks: skips the board and the gate (always asked).
5
6
  // Exempt: no board set up, "Commit denetimi: kapalı" in docs/workflow/board.md, or only docs changed.
6
7
  // Commits the user makes from their own terminal are not affected (this is a Claude Code hook).
7
8
  import fs from 'node:fs';
@@ -63,7 +64,19 @@ function reviewOff() {
63
64
  }
64
65
  }
65
66
 
67
+ // A note starting with "atlandı" skips the board and the gate. --approve refuses it whatever the spelling
68
+ // ($'atland\xc4\xb1', $(printf ...)); only --skip writes it, and --skip is always asked.
69
+ const SKIPPED = /^atlandı/i;
70
+
66
71
  function approve(note) {
72
+ if (SKIPPED.test(note)) {
73
+ console.error('"atlandı" notu --approve ile yazılmaz. Kullanıcı açıkça isterse: node .claude/hooks/commit-gate.mjs --skip "<neden>"');
74
+ process.exit(1);
75
+ }
76
+ save(note);
77
+ }
78
+
79
+ function save(note) {
67
80
  const { signature, files } = changes();
68
81
  fs.mkdirSync(path.dirname(approvalFile), { recursive: true });
69
82
  fs.writeFileSync(approvalFile, JSON.stringify({ signature, note, files, date: new Date().toISOString() }));
@@ -83,25 +96,40 @@ function readApproval() {
83
96
  return a && { signature: a.signature, note: a.note };
84
97
  }
85
98
 
99
+ // This project's own commit-gate.mjs, resolved from the shell's folder ($CLAUDE_PROJECT_DIR, absolute, relative or ./ spelling),
100
+ // is masked so it is not read as the gate approval; any other *gate.mjs (sandbox/.claude/hooks/commit-gate.mjs, a renamed
101
+ // copy of gate.mjs) still is. A cd in the command moves the folder the text cannot follow: then nothing is masked.
102
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
103
+ const samePath = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
104
+ export function maskOwnHook(command, dir) {
105
+ if (/(^|[;&|(]\s*)(cd|pushd|popd|Set-Location|sl|chdir)\b/i.test(command)) return command;
106
+ const own = real(path.join(root, '.claude', 'hooks', 'commit-gate.mjs'));
107
+ return command.replace(/"([^"\n]*commit-gate\.mjs)"|'([^'\n]*commit-gate\.mjs)'|([^\s"';&|]*commit-gate\.mjs)\b/gi, (m, a, b, c) => {
108
+ const p = (a ?? b ?? c).replace(/\$\{?CLAUDE_PROJECT_DIR\}?|%CLAUDE_PROJECT_DIR%|\$env:CLAUDE_PROJECT_DIR/gi, process.env.CLAUDE_PROJECT_DIR || root);
109
+ return samePath(real(path.resolve(dir, p)), own) ? 'KIT-COMMIT-HOOK' : m;
110
+ });
111
+ }
112
+
86
113
  function hook() {
87
- let command = '';
114
+ let command = '', dir = root;
88
115
  try {
89
- command = JSON.parse(fs.readFileSync(0, 'utf8')).tool_input?.command ?? '';
116
+ const input = JSON.parse(fs.readFileSync(0, 'utf8'));
117
+ command = input.tool_input?.command ?? '';
118
+ if (typeof input.cwd === 'string' && input.cwd) dir = input.cwd;
90
119
  } catch {
91
120
  process.exit(0);
92
121
  }
93
- // A "atlandı" approval bypasses the gate and the board: the user is asked (the AI must not write it on its own)
122
+ // --skip bypasses the gate and the board: the user is asked in every spelling (the AI must not write it on its own)
94
123
  const open = unquote(command);
95
- if (/commit-gate\.mjs\b[^;&|\n]*--approve\s+atlandı/i.test(open)) {
124
+ if ([command, open].some((c) => /commit-gate\.mjs\b[^;&|\n]*--skip\b/i.test(c))) {
96
125
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask',
97
- permissionDecisionReason: 'Commit kapısı: "atlandı" onayı kurul denetimini ve deterministik kapıyı aşar. Sadece kullanıcı açıkça isterse onayla.' } }));
126
+ permissionDecisionReason: 'Commit kapısı: --skip kurul denetimini ve deterministik kapıyı aşar. Sadece kullanıcı açıkça isterse onayla.' } }));
98
127
  process.exit(0);
99
128
  }
100
129
  // The gate command approval gives the commands in AGENTS.md permission to run: the user is asked (also covers spellings the permission list misses).
101
- // Only the real .claude/hooks/commit-gate.mjs --approve is left out (a copy of gate.mjs named commit-gate.mjs elsewhere
102
- // still matches; writing one into .claude/ is itself asked); both the raw and unquoted text are tested (unquote drops
103
- // the backslashes of .claude\hooks\gate.mjs, so the separators are optional).
104
- if ([command, open].some((c) => /(?<!\.claude[\\/]?hooks[\\/]?commit-)gate\.mjs\b[^;&|\n]*--approve\b/i.test(c))) {
130
+ // Only this project's commit-gate.mjs --approve is left out (maskOwnHook); both the raw and unquoted text are tested.
131
+ const masked = maskOwnHook(command, dir);
132
+ if ([masked, unquote(masked)].some((c) => /gate\.mjs\b[^;&|\n]*--approve\b/i.test(c))) {
105
133
  // The prompt reads AGENTS.md now: a chain, an env variable, a node flag or a redirect must not change it after the
106
134
  // prompt and get another list approved. Only the exact spelling is accepted.
107
135
  if (!/^\s*node\s+(\.\/)?\.claude[\\/]hooks[\\/]gate\.mjs\s+(['"]?)--approve\2\s*$/.test(command)) {
@@ -134,16 +162,16 @@ function hook() {
134
162
  const approval = readApproval();
135
163
  // The deterministic gate (gate.mjs) must be green for the same changes; not required if the user said "atlandı"
136
164
  const gate = readGate();
137
- const skipped = /^atlandı/i.test(String(approval?.note ?? ''));
165
+ const skipped = SKIPPED.test(String(approval?.note ?? ''));
138
166
  if (approval?.signature === d.signature && (skipped || (gate?.signature === d.signature && gate.passed === true))) process.exit(0);
139
167
  if (approval?.signature === d.signature) {
140
168
  const failed = gate?.signature === d.signature ? gate.checks.filter((k) => k.status === 'failed').map((k) => k.name) : null;
141
169
  process.stderr.write(failed && !failed.length && gate.empty
142
170
  ? 'Commit durduruldu: deterministik kapı boş (hiçbir kontrol çalışmadı). AGENTS.md → Komutlar\'ı doldur ve kapıyı tekrar çalıştır.\n' +
143
- 'Kullanıcı bu commit için açıkça isterse --approve "atlandı: kullanıcı isteği" kullan.\n'
171
+ 'Kullanıcı bu commit için açıkça isterse --skip "kullanıcı isteği" kullan.\n'
144
172
  : failed
145
173
  ? `Commit durduruldu: deterministik kapı kırmızı (${failed.join(', ') || 'kaldı'}). Düzelt, node .claude/hooks/gate.mjs --run ile tekrar çalıştır, sonra onayla.\n` +
146
- 'Kullanıcı ara commit (WIP) için açıkça isterse --approve "atlandı: kullanıcı isteği" kullan.\n'
174
+ 'Kullanıcı ara commit (WIP) için açıkça isterse --skip "kullanıcı isteği" kullan.\n'
147
175
  : 'Commit durduruldu: bu değişiklikler için deterministik kapı çalışmadı (ya da sonrasında değişiklik oldu). ' +
148
176
  'node .claude/hooks/gate.mjs --run çalıştır; kırmızı yoksa kurul onayını yenile ve commit\'i tekrar dene.\n');
149
177
  process.exit(2);
@@ -155,13 +183,14 @@ function hook() {
155
183
  '1. .claude/board/rules.md §3\'e göre değişen dosyalara ilgili en fazla 2 kurul üyesini model "haiku" ile, sadece diff üzerinde çalıştır.\n' +
156
184
  '2. RET yoksa: node .claude/hooks/commit-gate.mjs --approve "<katılan üyeler>" ve commit\'i tekrar dene.\n' +
157
185
  '3. RET varsa commit atma; bulguyu ve seçenekleri kullanıcıya sor.\n' +
158
- 'Kullanıcı bu commit için denetimi açıkça atlamak isterse --approve "atlandı: kullanıcı isteği" kullan.\n'
186
+ 'Kullanıcı bu commit için denetimi açıkça atlamak isterse --skip "kullanıcı isteği" kullan.\n'
159
187
  );
160
188
  process.exit(2);
161
189
  }
162
190
 
163
191
  if (isMain(import.meta.url)) {
164
- const i = process.argv.indexOf('--approve');
192
+ const i = process.argv.indexOf('--approve'), j = process.argv.indexOf('--skip');
165
193
  if (i > -1) approve(process.argv[i + 1] || 'belirtilmedi');
194
+ else if (j > -1) save(`atlandı: ${process.argv[j + 1] || 'kullanıcı isteği'}`);
166
195
  else hook();
167
196
  }
@@ -4,6 +4,7 @@
4
4
  // Run by /dashboard and /wrap-up. Not in the Stop hook: npx downloads the package the first time, it is slow.
5
5
  // On a subscription (Pro/Max) this is not a real bill; it is what the work would cost at API prices.
6
6
  import fs from 'node:fs';
7
+ import os from 'node:os';
7
8
  import path from 'node:path';
8
9
  import { execSync } from 'node:child_process';
9
10
  import { ccusageProjectName } from './dashboard-data.mjs';
@@ -12,10 +13,14 @@ const CCUSAGE = 'ccusage@20.0.30'; // pinned version; updating it is a technical
12
13
  const root = path.resolve(process.argv[2] ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd());
13
14
  const since = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 10).replace(/-/g, '');
14
15
 
16
+ // Run in a fresh folder of its own: in the project npx would read a repo .npmrc or node_modules/.bin/ccusage (code from the
17
+ // repo), and in a shared /tmp it would walk up to another user's /tmp/node_modules. The empty package.json stops that walk.
18
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), 'kit-cost-'));
15
19
  try {
16
- // Arguments contain only letters, digits and "-"; on Windows npx.cmd needs a shell
20
+ fs.writeFileSync(path.join(work, 'package.json'), '{}');
21
+ // Arguments contain only letters, digits and "-"; on Windows npx.cmd needs a shell.
17
22
  const output = execSync(`npx -y ${CCUSAGE} claude daily --json --instances --project ${ccusageProjectName(root)} --since ${since} --offline`,
18
- { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 120000 });
23
+ { cwd: work, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 120000 });
19
24
  const data = JSON.parse(output);
20
25
  fs.mkdirSync(path.join(root, '.claude', 'state'), { recursive: true });
21
26
  fs.writeFileSync(path.join(root, '.claude', 'state', 'cost.json'), JSON.stringify({ readAt: new Date().toISOString(), data }));
@@ -23,4 +28,6 @@ try {
23
28
  console.log(`Son 30 gün: ${Math.round((t.totalTokens ?? 0) / 1e6)}M token · API karşılığı $${(t.totalCost ?? 0).toFixed(2)}`);
24
29
  } catch (h) {
25
30
  console.error(`ccusage okunamadı (${h.code ?? h.message}). Node ve ağ erişimi gerekir; pano bu kartı boş gösterir.`);
31
+ } finally {
32
+ fs.rmSync(work, { recursive: true, force: true });
26
33
  }
@@ -239,7 +239,8 @@ export function scopeIssue(command, dir, root) {
239
239
 
240
240
  // Commands denied/asked in Claude Code via .claude/settings.json → permissions. Codex and Antigravity have no such
241
241
  // list; this is its counterpart (applied only on those tools, Claude's behavior does not change).
242
- // Only the real .claude/hooks/commit-gate.mjs --approve is kept out of the gate rule (separators optional: unquote drops backslashes).
242
+ // Only the real .claude/hooks/commit-gate.mjs --approve is kept out of the gate rule (separators optional: unquote drops backslashes;
243
+ // anchored at the start of the path, so sandbox/.claude/hooks/commit-gate.mjs, a renamed gate.mjs, is not exempt).
243
244
  const PERMISSION_EQUIVALENT = [
244
245
  ['deny', /\brm\b(?=[^;&|]*\s--?[a-z]*r)(?=[^;&|]*\s--?[a-z]*f)/i, 'rm -rf ile toplu silme'],
245
246
  ['deny', /\b(Remove-Item|rm|ri|del|erase)\b[^;&|]*\s-Recurse\b|\b(rmdir|rd|del)\s+[^;&|]*\/s\b/i, 'Klasör toplu siliniyor'],
@@ -249,7 +250,10 @@ const PERMISSION_EQUIVALENT = [
249
250
  ['ask', /freeze-guard\.mjs['"]?\s+['"]?--off\b/i, 'Düzenleme kilidi kaldırılıyor'],
250
251
  ['ask', /tools\.mjs['"]?\s+['"]?--commands\b/i, 'Paylaşılan .claude/settings.json → skillOverrides değişiyor (komut listesi)'],
251
252
  ['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-add\b/i, 'Onaylı planın dosya listesi genişliyor (kapsam kalkanı)'],
252
- ['ask', /(?<!\.claude[\\/]?hooks[\\/]?commit-)gate\.mjs['"]?\s+['"]?--approve\b/i, 'Deterministik kapının çalıştıracağı komutlar onaylanıyor'],
253
+ ['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-save\b/i, 'Kapsam kalkanının dosya listesi yazılıyor'],
254
+ ['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-end\b/i, 'Kapsam kalkanı kapatılıyor'],
255
+ ['ask', /commit-gate\.mjs['"]?\s+['"]?--skip\b/i, 'Commit kapısı ve kurul denetimi atlanıyor'],
256
+ ['ask', /(?<!(?:^|[\s'"=])(?:\.[\\/]|\$\{?CLAUDE_PROJECT_DIR\}?[\\/]?)?\.claude[\\/]?hooks[\\/]?commit-)gate\.mjs['"]?\s+['"]?--approve\b/i, 'Deterministik kapının çalıştıracağı komutlar onaylanıyor'],
253
257
  // Shell counterpart of the PROTECTED rule on file tools: a command that names a rule or guard file and writes/deletes is asked
254
258
  // (.git: the gate command approval and git hooks live there)
255
259
  ['ask', /(?=.*((^|[\s'"/\\])\.(claude|codex|agents|git)(?=$|[\s/\\'"])|\bAGENTS\.md|\bCLAUDE\.md))(?=.*(\b(rm|del|erase|rmdir|rd|mv|move|cp|copy|tee|truncate|checkout|restore|Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item)\b|\bsed\s+-i|>|\bnode\s+-e|\bpython\d?\s+-c))/i, 'Kural veya koruma dosyası kabuk komutuyla değiştiriliyor'],
@@ -290,10 +294,21 @@ function obfuscationOne(command) {
290
294
  ? { decision: 'ask', reason: 'Komut gizlenmiş görünüyor (satır sonu \\, $\'..\', $(...), `...` veya ${...}) ve korunan bir işlem içeriyor. Kullanıcı onayı gerekli.' } : null;
291
295
  }
292
296
 
297
+ // A kit guard script (.claude/hooks) run with an indirect spelling: $VAR, $(..), `..`, ${..}, $'..' or a glob * ? (commit-gate.mjs $S,
298
+ // --sk$()ip, commit-*.mjs). The text rules for --skip, --approve and --plan-* cannot see through them: asked (user decision 2026-10-25).
299
+ // Heredocs and single-quoted text are not expanded; $CLAUDE_PROJECT_DIR is the documented way to name the folder.
300
+ export function hookScriptIssue(command) {
301
+ const k = command.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n[ \t]*\2[ \t]*(?=\n|$)/g, '').replace(/'[^']*'/g, "''")
302
+ .replace(/"(?:[^"\\]|\\.)*"/g, (q) => q.replace(/[*?]/g, '')).replace(/\$\{?CLAUDE_PROJECT_DIR\}?|\$env:CLAUDE_PROJECT_DIR/gi, '');
303
+ if (!/\bnode\b/i.test(k) || !/\.claude|hooks|commit-|gate|freeze/i.test(command)) return null;
304
+ return /\$\(|`|\$\{|\$'|\$[A-Za-z_]|[*?]/.test(k)
305
+ ? { decision: 'ask', reason: 'Kitin koruma betiği dolaylı yazımla çalıştırılıyor ($DEĞİŞKEN, $(...), `...`, ${...} veya * ?); kural metni bunun içini göremez. Kullanıcı onayı gerekli.' } : null;
306
+ }
307
+
293
308
  // Separators inside quotes turned into spaces: a quoted & or | ("a & b") must not split a command so its later flags escape the rules
294
309
  const blankQuoted = (s) => s.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, (q) => q.replace(/[;&|\n]/g, ' '));
295
310
  export function evaluate(command) {
296
- return strictest(evaluateOne(command), evaluateOne(unquote(command)), evaluateOne(blankQuoted(command)), obfuscationOne(command));
311
+ return strictest(evaluateOne(command), evaluateOne(unquote(command)), evaluateOne(blankQuoted(command)), obfuscationOne(command), hookScriptIssue(command));
297
312
  }
298
313
  function evaluateOne(command) {
299
314
  const k = command.replace(/\s+/g, ' ');
@@ -11,9 +11,9 @@
11
11
  // Scope shield (F-06): after /plan-task approval the plan's file list is saved; while a plan is active a write to an
12
12
  // EXISTING file not on the list is asked. New files are the structure guard's job; docs/, CHANGELOG.md are free. No plan: silent.
13
13
  // State: .claude/state/plan.json → { "task": "G-012", "paths": ["src/a.ts", "src/features/fatura/"] } (ending in /: folder)
14
- // node .claude/hooks/freeze-guard.mjs --plan-save <task> <path...> save when no plan is active (no question)
14
+ // node .claude/hooks/freeze-guard.mjs --plan-save <task> <path...> save when no plan is active (asked: the user sees the list)
15
15
  // node .claude/hooks/freeze-guard.mjs --plan-add <path...> widen the list (with user approval)
16
- // node .claude/hooks/freeze-guard.mjs --plan-end close when the task is done
16
+ // node .claude/hooks/freeze-guard.mjs --plan-end close when the task is done (asked)
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { readInput, decide, findRoot, isMain } from './shared.mjs';
@@ -133,7 +133,7 @@ export function runGate(root) {
133
133
  fs.mkdirSync(path.dirname(file), { recursive: true });
134
134
  fs.writeFileSync(file, JSON.stringify({ signature, date: new Date().toISOString(), passed, empty, checks }, null, 2));
135
135
  const result = checks.some((k) => k.status === 'failed') ? 'Sonuç: KIRMIZI. Commit kapısı açılmaz: düzelt ve kapıyı tekrar çalıştır.'
136
- : empty ? 'Sonuç: BOŞ, hiçbir kontrol çalışmadı. AGENTS.md → Komutlar\'ı doldur; commit kapısı açılmaz (kullanıcı isterse "atlandı").'
136
+ : empty ? 'Sonuç: BOŞ, hiçbir kontrol çalışmadı. AGENTS.md → Komutlar\'ı doldur; commit kapısı açılmaz (kullanıcı isterse commit-gate.mjs --skip).'
137
137
  : 'Sonuç: YEŞİL.';
138
138
  return { passed, text: [`## Kapı · ${files.length} dosya değişti`, ...checks.map(line), result].join('\n') };
139
139
  }
@@ -6,48 +6,59 @@
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { spawn } from 'node:child_process';
9
+ import { isMain } from './shared.mjs';
9
10
 
10
- const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
11
11
  export const THRESHOLD_SEC = 90;
12
12
 
13
- let input = {};
14
- try { input = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { /* no input */ }
15
- if (process.env.KIT_NOTIFY === 'off') process.exit(0);
16
-
17
- let project = path.basename(root);
18
- for (const f of ['AGENTS.md', 'CLAUDE.md']) { // in projects before 3.9.0 the name is in CLAUDE.md
19
- try { const name = fs.readFileSync(path.join(root, f), 'utf8').match(/^# (.+)$/m)?.[1]; if (name && name !== '[PROJE ADI]') { project = name; break; } } catch { /* */ }
13
+ // Title and message go in env, never into the script text: PowerShell also treats ’ ‘ ‚ ‛ as quotes.
14
+ export function winCommand(title, message) {
15
+ const script = 'Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; ' +
16
+ '$n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; ' +
17
+ '$n.ShowBalloonTip(6000, $env:KIT_T, $env:KIT_M, [System.Windows.Forms.ToolTipIcon]::Info); Start-Sleep -Seconds 7; $n.Dispose()';
18
+ return { args: ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], env: { ...process.env, KIT_T: title, KIT_M: message } };
20
19
  }
21
20
 
22
- let message;
23
- if (process.argv.includes('--stop')) {
24
- if (input.stop_hook_active) process.exit(0);
25
- let startedAt = 0;
26
- try { const r = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'reminder.json'), 'utf8')); startedAt = r.turnAt ?? r.tur ?? 0; } catch { /* */ } // tur: key before 3.15.0
27
- const duration = (Date.now() - startedAt) / 1000;
28
- if (!startedAt || duration < THRESHOLD_SEC) process.exit(0);
29
- message = `İş bitti (${Math.round(duration / 60)} dk). Sonuca bakabilirsin.`;
30
- } else {
31
- message = input.message || 'Claude cevabını bekliyor.';
32
- }
21
+ if (isMain(import.meta.url)) main();
22
+
23
+ function main() {
24
+ const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
33
25
 
34
- const title = `Claude Code · ${project}`.slice(0, 60);
35
- message = String(message).replace(/\s+/g, ' ').slice(0, 180);
36
- const detachedOpts = { detached: true, stdio: 'ignore', windowsHide: true };
37
-
38
- try {
39
- if (process.platform === 'win32') {
40
- const ps = (s) => s.replace(/'/g, "''");
41
- const script = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; ` +
42
- `$n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; ` +
43
- `$n.ShowBalloonTip(6000, '${ps(title)}', '${ps(message)}', [System.Windows.Forms.ToolTipIcon]::Info); Start-Sleep -Seconds 7; $n.Dispose()`;
44
- spawn('powershell', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], detachedOpts).unref();
45
- } else if (process.platform === 'darwin') {
46
- const as = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
47
- spawn('osascript', ['-e', `display notification "${as(message)}" with title "${as(title)}"`], detachedOpts).unref();
26
+ let input = {};
27
+ try { input = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { /* no input */ }
28
+ if (process.env.KIT_NOTIFY === 'off') return;
29
+
30
+ let project = path.basename(root);
31
+ for (const f of ['AGENTS.md', 'CLAUDE.md']) { // in projects before 3.9.0 the name is in CLAUDE.md
32
+ try { const name = fs.readFileSync(path.join(root, f), 'utf8').match(/^# (.+)$/m)?.[1]; if (name && name !== '[PROJE ADI]') { project = name; break; } } catch { /* */ }
33
+ }
34
+
35
+ let message;
36
+ if (process.argv.includes('--stop')) {
37
+ if (input.stop_hook_active) process.exit(0);
38
+ let startedAt = 0;
39
+ try { const r = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'reminder.json'), 'utf8')); startedAt = r.turnAt ?? r.tur ?? 0; } catch { /* */ } // tur: key before 3.15.0
40
+ const duration = (Date.now() - startedAt) / 1000;
41
+ if (!startedAt || duration < THRESHOLD_SEC) process.exit(0);
42
+ message = `İş bitti (${Math.round(duration / 60)} dk). Sonuca bakabilirsin.`;
48
43
  } else {
49
- spawn('notify-send', [title, message], detachedOpts).unref();
44
+ message = input.message || 'Claude cevabını bekliyor.';
45
+ }
46
+
47
+ const title = `Claude Code · ${project}`.slice(0, 60);
48
+ message = String(message).replace(/\s+/g, ' ').slice(0, 180);
49
+ const detachedOpts = { detached: true, stdio: 'ignore', windowsHide: true };
50
+
51
+ try {
52
+ if (process.platform === 'win32') {
53
+ const { args, env } = winCommand(title, message);
54
+ spawn('powershell', args, { ...detachedOpts, env }).unref();
55
+ } else if (process.platform === 'darwin') {
56
+ const as = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
57
+ spawn('osascript', ['-e', `display notification "${as(message)}" with title "${as(title)}"`], detachedOpts).unref();
58
+ } else {
59
+ spawn('notify-send', ['--', title, message], detachedOpts).unref();
60
+ }
61
+ } catch {
62
+ // no notification tool: skip silently
50
63
  }
51
- } catch {
52
- // no notification tool: skip silently
53
64
  }
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // Package gate (PreToolUse, Bash): checks a package the AI wants to install before it is installed.
2
+ // Package gate (PreToolUse, Bash): checks a package the AI wants to install, or download and run (npx, uvx …), before it reaches the machine.
3
3
  // Why: AIs invent package names that do not exist; attackers register those names (slopsquatting).
4
4
  // Package missing -> denied (deny)
5
5
  // Very new / few downloads / very close to a popular package / unverified -> the user is asked (ask)
6
6
  // The decision goes through Claude Code's permission UI; the AI cannot skip it.
7
7
 
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
8
10
  import { readInput, decide, isMain } from './shared.mjs';
9
11
 
10
12
  export const THRESHOLD = { newDays: 30, npmWeeklyDownloads: 500 };
@@ -26,6 +28,60 @@ const COMMANDS = [
26
28
  [/^uv\s+(add|pip\s+install)\b/, 'pypi'], [/^(python3?\s+-m\s+)?pip3?\s+install\b/, 'pypi'], [/^poetry\s+add\b/, 'pypi'],
27
29
  ];
28
30
 
31
+ // Download-and-run: the first positional is the package (the rest are its arguments), or the values of the package flags.
32
+ // local: the runner uses a bin the project already has before downloading (npx, npm exec, bunx); pnpm/yarn dlx always download.
33
+ // positional: false for uv run (its positional is a script; only --with names packages).
34
+ const EXEC = [
35
+ { re: /^(npx|npm\s+(exec|x)|bunx|bun\s+x)\b/, eco: 'npm', local: true, positional: true },
36
+ { re: /^(pnpx|pnpm\s+dlx|yarn\s+dlx)\b/, eco: 'npm', local: false, positional: true },
37
+ { re: /^(uvx|uv\s+tool\s+run|pipx\s+run)\b/, eco: 'pypi', local: false, positional: true },
38
+ { re: /^uv\s+run\b/, eco: 'pypi', local: false, positional: false },
39
+ ];
40
+ const FLAGS = {
41
+ npm: { pkg: /^(-p|--package)$/, value: /^(-p|--package|-c|--call|--registry|--prefix|--cache|--userconfig|--globalconfig|-w|--workspace|--node-options|--script-shell|--shell|--loglevel|-C|--dir|--filter)$/ },
42
+ pypi: { pkg: /^(--from|--spec)$/, value: /^(--from|--spec|--with|--with-requirements|--with-editable|-p|--python|-i|--index|--index-url|--extra-index-url|--default-index|--pip-args|-c|--constraints|--overrides)$/ },
43
+ };
44
+
45
+ // Returns [{ arg, fromFlag, unsure }]. After an unknown flag its value may be the next word: that word and the one after are
46
+ // both checked, unsure (a missing one is asked, not denied: it may be a file name).
47
+ function execArgs(args, eco, positional) {
48
+ const named = [], extra = [], out = [];
49
+ let unknown = false;
50
+ for (let i = 0; i < args.length; i++) {
51
+ const [flag, inline] = args[i].split(/=(.*)/s);
52
+ if (args[i] === '--') { if (positional && args[i + 1]) out.push({ arg: args[i + 1] }); break; }
53
+ if (FLAGS[eco].value.test(flag)) {
54
+ const v = inline ?? args[++i] ?? '';
55
+ if (FLAGS[eco].pkg.test(flag)) named.push({ arg: v, fromFlag: true });
56
+ else if (flag === '--with') extra.push(...v.split(',').map((arg) => ({ arg, fromFlag: true })));
57
+ continue;
58
+ }
59
+ if (args[i].startsWith('-')) { unknown ||= inline === undefined && /^--?\w/.test(flag) && !/^(-y|--yes|--no|-q|--quiet|-s|--silent|--no-install|--ignore-existing|--offline|-v|--verbose)$/.test(flag); continue; }
60
+ if (!positional || named.length) break;
61
+ out.push({ arg: args[i], unsure: unknown });
62
+ if (unknown && args[i + 1] && !args[i + 1].startsWith('-')) out.push({ arg: args[i + 1], unsure: true });
63
+ break;
64
+ }
65
+ return [...(named.length ? named : out), ...extra];
66
+ }
67
+
68
+ // What runs a segment: wrappers, VAR=x, a path or .cmd/.exe on the tool, and the manager's own flags before its subcommand are
69
+ // dropped (FOO=1 npx x, cmd /c npx x, bash -c "npx x", /usr/bin/npx x, npx.cmd x, pnpm -C . dlx x, npm --silent exec x).
70
+ const WRAPPER = /^(?:\(|[A-Za-z_]\w*=\S*\s+|(?:sudo|env|time|nohup|exec|command|xargs|corepack|call|start)\s+|(?:bash|sh|zsh|dash|cmd|pwsh|powershell)(?:\.exe)?\s+(?:-c|\/c|\/k|-Command)\s+["']?)/i;
71
+ const SUBCOMMAND = /^(install|i|add|exec|x|dlx|tool|pip|run|global)$/;
72
+ export function normalize(part) {
73
+ let s = part.trim(), prev;
74
+ do { prev = s; s = s.replace(WRAPPER, '').trim(); } while (s !== prev);
75
+ s = s.replace(/^["']?(?:[^\s"']*[\/])?([\w.-]+?)(?:\.cmd|\.exe|\.ps1)?["']?(?=\s|$)/i, '$1');
76
+ const t = s.split(/\s+/);
77
+ if (/^(npm|pnpm|yarn|bun|uv)$/.test(t[0])) {
78
+ let i = 1;
79
+ while (i < t.length && t[i].startsWith('-')) i += !t[i].includes('=') && t[i + 1] && !t[i + 1].startsWith('-') && !SUBCOMMAND.test(t[i + 1]) ? 2 : 1;
80
+ s = [t[0], ...t.slice(i)].join(' ');
81
+ }
82
+ return s;
83
+ }
84
+
29
85
  function npmName(arg) {
30
86
  if (/^(\.|\/|file:|link:|git\+|git:|https?:|github:|workspace:)/.test(arg)) return null;
31
87
  if (arg.startsWith('@')) { const i = arg.indexOf('@', 1); return i > 0 ? arg.slice(0, i) : arg; }
@@ -40,12 +96,20 @@ function pypiName(arg) {
40
96
  export function findPackages(command) {
41
97
  const result = [];
42
98
  for (const part of command.split(/&&|\|\||;|\|/)) {
43
- const s = part.trim().replace(/^(sudo|env\s+\S+=\S+)\s+/, '');
99
+ const s = normalize(part);
44
100
  const match = COMMANDS.find(([re]) => re.test(s));
45
- if (!match) continue;
46
- const [re, eco] = match;
101
+ const exec = match ? null : EXEC.find((e) => e.re.test(s));
102
+ if (!match && !exec) continue;
103
+ const re = match?.[0] ?? exec.re, eco = match?.[1] ?? exec.eco;
47
104
  // Redirects (2>&1, > log, 2>/dev/null, &> x) are not arguments; a quoted "pkg>=1" has no space before ">" and stays
48
- const args = s.replace(re, '').replace(/\s+(\d*|&)(>>?|<)(&\d+|\s*[^\s'"]+)/g, '').trim().split(/\s+/).map((x) => x.replace(/^['"]|['"]$/g, '')).filter(Boolean);
105
+ const args = s.replace(re, '').replace(/\s+(\d*|&)(>>?|<)(&\d+|\s*[^\s'"]+)/g, '').trim().split(/\s+/).map((x) => x.replace(/^['"(]+|['")]+$/g, '')).filter(Boolean);
106
+ if (exec) {
107
+ for (const { arg, fromFlag, unsure } of execArgs(args, eco, exec.positional)) {
108
+ const name = eco === 'npm' ? npmName(arg) : pypiName(arg);
109
+ if (name) result.push({ eco, name, exec: true, ...(exec.local && !fromFlag && { local: true }), ...(unsure && { unsure: true }) });
110
+ }
111
+ continue;
112
+ }
49
113
  for (let i = 0; i < args.length; i++) {
50
114
  const a = args[i];
51
115
  if (a.startsWith('-')) {
@@ -114,10 +178,13 @@ export async function check({ eco, name }, get = fetch, now = Date.now()) {
114
178
  async function hook() {
115
179
  const g = readInput();
116
180
  if (!g) process.exit(0);
117
- const packages = findPackages(g.command);
181
+ // npx/bunx run a bin the project already installed without downloading: a local bin (tsc, eas) is not a package name
182
+ const bin = (name) => /^\w[\w.-]*$/.test(name) && [g.dir, g.root].some((d) => fs.existsSync(path.join(d, 'node_modules', '.bin', name)));
183
+ // The kit's own package (/start and /adopt run npx -y tillerhand@<version> install): the kit already trusts itself
184
+ const packages = findPackages(g.command).filter((p) => !(p.local && bin(p.name)) && !(p.eco === 'npm' && p.name === 'tillerhand'));
118
185
  if (!packages.length) process.exit(0);
119
186
  const results = await Promise.all(packages.map(async (p) => ({ ...p, ...(await check(p)) })));
120
- const missing = results.filter((s) => s.status === 'missing');
187
+ const missing = results.filter((s) => s.status === 'missing' && !s.unsure);
121
188
  if (missing.length) {
122
189
  decide(g.tool, 'deny', `Paket kapısı: ${missing.map((s) => `${s.name} (${s.detail})`).join(', ')}. Bu ad uydurulmuş olabilir. Doğru paket adını resmi dokümandan bul; kullanıcıya bildir.`);
123
190
  }
@@ -11,8 +11,6 @@
11
11
  "Bash(node .claude/hooks/cost.mjs)",
12
12
  "Bash(node .claude/hooks/freeze-guard.mjs --set:*)",
13
13
  "Bash(node .claude/hooks/freeze-guard.mjs --status)",
14
- "Bash(node .claude/hooks/freeze-guard.mjs --plan-save:*)",
15
- "Bash(node .claude/hooks/freeze-guard.mjs --plan-end)",
16
14
  "Bash(node .claude/hooks/gate.mjs --run)",
17
15
  "Bash(node .claude/hooks/preview.mjs:*)",
18
16
  "Bash(node .claude/hooks/checkpoint.mjs --list:*)",
@@ -31,6 +29,9 @@
31
29
  "Bash(node .claude/hooks/tools.mjs --enable:*)",
32
30
  "Bash(node .claude/hooks/tools.mjs --commands:*)",
33
31
  "Bash(node .claude/hooks/freeze-guard.mjs --plan-add:*)",
32
+ "Bash(node .claude/hooks/freeze-guard.mjs --plan-save:*)",
33
+ "Bash(node .claude/hooks/freeze-guard.mjs --plan-end)",
34
+ "Bash(node .claude/hooks/commit-gate.mjs --skip:*)",
34
35
  "Bash(node .claude/hooks/gate.mjs --approve)",
35
36
  "Edit(./CLAUDE.md)",
36
37
  "Edit(./AGENTS.md)",
package/README.en.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <a href="kit/CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-3.26.0-2a78d6?style=flat-square"></a>
9
+ <a href="kit/CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-3.28.0-2a78d6?style=flat-square"></a>
10
10
  <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-2a78d6?style=flat-square"></a>
11
11
  <img alt="Claude Code" src="https://img.shields.io/badge/Claude%20Code-ready-d97757?style=flat-square">
12
12
  <img alt="Also Codex and Antigravity" src="https://img.shields.io/badge/also-Codex%20%C2%B7%20Antigravity-5b6577?style=flat-square">
@@ -56,7 +56,7 @@ You don't need to know how to code. Claude handles git, the terminal and install
56
56
 
57
57
  **2. Create your project**
58
58
  1. Make a folder for your project and select it in the **Code** tab of the Claude desktop app.
59
- 2. Type: `clone github.com/burakdogrul/TillerHand into this folder as vibe-kit, then run node vibe-kit/kit/kit.mjs init`.
59
+ 2. Type: `run npx tillerhand init in this folder`.
60
60
  3. **Start a new session** in the same folder. The kit's commands and guardrails load in the new session.
61
61
 
62
62
  **3. Start**
@@ -87,14 +87,20 @@ Claude asks its questions one by one. **The first option is always the recommend
87
87
 
88
88
  ```bash
89
89
  mkdir my-app && cd my-app
90
+ npx tillerhand init
91
+ claude
92
+ ```
93
+
94
+ With git instead of npm (the kit stays in a `vibe-kit/` folder inside the project and goes into `.gitignore`):
95
+
96
+ ```bash
90
97
  git clone https://github.com/burakdogrul/TillerHand.git vibe-kit
91
98
  node vibe-kit/kit/kit.mjs init
92
- claude
93
99
  ```
94
100
 
95
101
  - On first launch Claude Code offers to install the kit's skill plugins; accept.
96
102
  - Kit in another folder: `node <kit>/kit/kit.mjs init --project <project>`.
97
- - For Codex or Antigravity: `node vibe-kit/kit/kit.mjs init --tool codex` (several: `--tool claude,codex`).
103
+ - For Codex or Antigravity: `npx tillerhand init --tool codex` (several: `--tool claude,codex`).
98
104
  - Recommended: a code map ([codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)). The AI queries the code instead of reading it file by file, which saves tokens. `/start` offers to install it.
99
105
 
100
106
  ## How it works
@@ -192,7 +198,7 @@ A new file outside the map is asked about, clutter like `*.bak` and `*-old.*` is
192
198
  |---|---|
193
199
  | Secret guard | Blocks API keys written into code or docs |
194
200
  | Destructive-command shield | Blocks `DROP`, unconditional `DELETE`, `db reset`; asks before prod deploys and rollbacks |
195
- | Package gate | Blocks packages that don't exist; asks about very new ones or lookalikes of popular ones |
201
+ | Package gate | Blocks packages that don't exist (on install and on `npx`, `uvx` runs); asks about very new ones or lookalikes of popular ones |
196
202
  | Structure guard | Asks about files outside the map, blocks clutter files |
197
203
  | Test guard | Asks before a test is deleted, skipped or weakened |
198
204
  | Deterministic gate | No commit until lint, type check, tests and Semgrep have run |
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <a href="kit/CHANGELOG.md"><img alt="Sürüm" src="https://img.shields.io/badge/s%C3%BCr%C3%BCm-3.26.0-2a78d6?style=flat-square"></a>
9
+ <a href="kit/CHANGELOG.md"><img alt="Sürüm" src="https://img.shields.io/badge/s%C3%BCr%C3%BCm-3.28.0-2a78d6?style=flat-square"></a>
10
10
  <a href="LICENSE"><img alt="Lisans: MIT" src="https://img.shields.io/badge/lisans-MIT-2a78d6?style=flat-square"></a>
11
11
  <img alt="Claude Code" src="https://img.shields.io/badge/Claude%20Code-haz%C4%B1r-d97757?style=flat-square">
12
12
  <img alt="Codex ve Antigravity" src="https://img.shields.io/badge/ayr%C4%B1ca-Codex%20%C2%B7%20Antigravity-5b6577?style=flat-square">
@@ -54,7 +54,7 @@ Kod yazmayı bilmen gerekmez. Git, terminal ve kurulum işlerini Claude yapar; s
54
54
 
55
55
  **2. Projeni oluştur**
56
56
  1. Projen için bir klasör aç ve Claude masaüstü uygulamasında **Code** sekmesinde seç.
57
- 2. Şunu yaz: `github.com/burakdogrul/TillerHand reposunu bu klasörün içine vibe-kit adıyla klonla, sonra node vibe-kit/kit/kit.mjs init komutunu çalıştır`.
57
+ 2. Şunu yaz: `bu klasörde npx tillerhand init komutunu çalıştır`.
58
58
  3. Aynı klasörde **yeni oturum aç**. Kitin komutları ve korumaları yeni oturumda yüklenir.
59
59
 
60
60
  **3. Başlat**
@@ -85,14 +85,20 @@ Claude sırayla soru sorar. **Her sorunun ilk seçeneği önerilendir**; emin de
85
85
 
86
86
  ```bash
87
87
  mkdir yeni-proje && cd yeni-proje
88
+ npx tillerhand init
89
+ claude
90
+ ```
91
+
92
+ npm yerine git ile (kit, proje içinde `vibe-kit/` klasöründe durur ve `.gitignore`'a girer):
93
+
94
+ ```bash
88
95
  git clone https://github.com/burakdogrul/TillerHand.git vibe-kit
89
96
  node vibe-kit/kit/kit.mjs init
90
- claude
91
97
  ```
92
98
 
93
99
  - Açılışta Claude Code, kitin skill eklentilerini kurmayı önerir; kabul et.
94
100
  - Kit başka bir klasördeyse: `node <kit>/kit/kit.mjs init --project <proje>`.
95
- - Codex veya Antigravity için: `node vibe-kit/kit/kit.mjs init --tool codex` (birden fazlası: `--tool claude,codex`).
101
+ - Codex veya Antigravity için: `npx tillerhand init --tool codex` (birden fazlası: `--tool claude,codex`).
96
102
  - Önerilen: kod haritası ([codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)). AI kodu dosya dosya okumak yerine sorgular, token tasarrufu sağlar. `/start` kurmayı sorar.
97
103
 
98
104
  ## Nasıl çalışır
@@ -190,7 +196,7 @@ Harita dışı yeni dosya sorulur, `*.bak` ve `*-old.*` gibi dağınıklık dosy
190
196
  |---|---|
191
197
  | Sır engeli | Koda veya dokümana yazılan API anahtarını engeller |
192
198
  | Yıkıcı komut kalkanı | `DROP`, koşulsuz `DELETE`, `db reset` engellenir; prod deploy ve geri alma sorulur |
193
- | Paket kapısı | Var olmayan paketi engeller; çok yeni veya popüler bir pakete benzeyeni sorar |
199
+ | Paket kapısı | Var olmayan paketi engeller (kurulumda ve `npx`, `uvx` ile çalıştırmada); çok yeni veya popüler bir pakete benzeyeni sorar |
194
200
  | Yapı koruyucu | Harita dışı dosyayı sorar, dağınıklık dosyalarını engeller |
195
201
  | Test koruması | Silinen, atlanan veya gevşetilen testi sorar |
196
202
  | Deterministik kapı | Lint, tip, test ve Semgrep çalışmadan commit açılmaz |
package/kit/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
3
3
  Kit sürümü `kit/VERSION` dosyasında. Bir projeyi güncellemek: projede `/kit-update`.
4
4
  "Elle uyarla" notu olan maddeler projeye ait dosyalardadır; güncelleme bunlara dokunmaz.
5
5
 
6
+ ## [3.28.0] - 2026-10-25
7
+
8
+ ### Değişen
9
+ - **Commit denetimini atlamak artık ayrı bir komut: `commit-gate.mjs --skip "<neden>"`.** Bu komut her seferinde kullanıcıya sorulur. `--approve` "atlandı" ile başlayan notu artık kabul etmiyor. Önceden hook komut metninde "atlandı" kelimesini arıyordu; kelime `$'atland\xc4\xb1'` gibi kaçışlı yazılınca soru çıkmadan kapı ve kurul atlanabiliyordu.
10
+ - **Kapsam kalkanını açmak ve kapatmak (`freeze-guard.mjs --plan-save`, `--plan-end`) artık sorulur.** İkisi de serbestti. AI planı kapatıp listede olmayan dosyalara yazabiliyor ya da planı daha geniş bir listeyle yeniden kaydedebiliyordu. `/kit-update` eski izin satırlarını kaldırıp yenilerini ekler.
11
+ - **Koruma betiği dolaylı yazımla çalıştırılırsa sorulur.** `.claude/hooks` altındaki bir betik `$DEĞİŞKEN`, `$(...)`, ters tırnak, `${...}` ya da `*` `?` ile çağrılırsa (`commit-gate.mjs $S`, `commit-*.mjs`) kurallar komutun içini göremez; bu yüzden kullanıcı onayı istenir. `$CLAUDE_PROJECT_DIR` ve heredoc içindeki commit mesajları bu kurala takılmaz.
12
+ - **Paket kapısı indirip çalıştıran komutlara da bakıyor:** `npx`, `bunx`, `bun x`, `npm exec`, `pnpm dlx`, `yarn dlx`, `uvx`, `uv tool run`, `uv run --with`, `pipx run`. Paket adı uydurmaysa komut engellenir. Çok yeni, az indirilmiş ya da popüler bir pakete benzeyen paket kullanıcıya sorulur. Projede zaten kurulu bir komut (`node_modules/.bin`) `npx`, `npm exec` ve `bunx` ile çalıştırılırken denetlenmez; `dlx` her zaman indirdiği için denetlenir. Kitin kendi paketi (`tillerhand`) denetlenmez.
13
+ - **Paket kapısı önekleri ve sarmalayıcıları da görüyor** (kurulum komutlarında da): `FOO=1 npx`, `cmd /c`, `bash -c`, `time`, `/usr/bin/npx`, `npx.cmd`, `pnpm -C . dlx`, `npm --prefix web install`. Bilinmeyen bir bayraktan sonraki ad kesin paket sayılmaz; kayıt defterinde yoksa engellenmez, sorulur.
14
+
15
+ ### Düzeltilen
16
+ - **Windows'ta masaüstü bildirimi proje adından komut çalıştırabiliyordu.** Proje adı PowerShell betiğinin metnine gömülüyordu ve yalnız düz `'` kaçırılıyordu; PowerShell `’ ‘ ‚ ‛` işaretlerini de tırnak sayar. Başlık ve mesaj artık betiğe ortam değişkeniyle geçiyor.
17
+ - **Biçimlendirme hook'u (web, mobil) Windows'ta boşluklu klasörde çalışmıyordu** ve dosya yolunu kabuğa kaçışsız veriyordu. Prettier artık kabuk olmadan, doğrudan Node ile çalışıyor (Prettier 3).
18
+ - **Maliyet okuma (`cost.mjs`) `npx`'i proje klasöründe çalıştırıyordu.** Bu yüzden depodaki bir `.npmrc` ya da `node_modules/.bin/ccusage` devreye girebiliyordu. Artık her çalışmada açılıp silinen kendi geçici klasöründe çalışıyor.
19
+ - **Commit kapısındaki muafiyet sadece projenin kendi `commit-gate.mjs` dosyası için geçerli.** Yol çözülerek karşılaştırılır: göreli, mutlak ya da `$CLAUDE_PROJECT_DIR` ile yazılan yol muaf kalır. Başka bir klasörde (`sandbox/.claude/hooks/commit-gate.mjs`) bu adla duran bir `gate.mjs` kopyası ise kapı komut onayı sayılır.
20
+ - Linux bildiriminde `-` ile başlayan mesaj seçenek sanılmaz.
21
+ - Elle uyarla: yok; `/kit-update` hook'ları, kuralları ve izin satırlarını yeniler.
22
+
23
+ ## [3.27.2] - 2026-10-25
24
+
25
+ ### Belgeler
26
+ - **README ve rehberde npm ile kurulum:** `npx tillerhand init` önerilen yol; git klonu seçenek olarak duruyor. Codex/Antigravity için `npx tillerhand init --tool codex`.
27
+ - **Rehber:** Üstteki "Hemen başla" ve "Nasıl çalışır?" düğmeleri kaldırıldı. Kurulum komutları ve terminal blokları (durum bloğu, alt durum çubuğu) dar ekranda yana kaymadan alt satıra iner.
28
+ - Paket Node.js 18 ve üstünü kabul eder (belgedeki gereksinimle aynı).
29
+
6
30
  ## [3.27.1] - 2026-10-25
7
31
 
8
32
  ### Düzeltilen
package/kit/VERSION CHANGED
@@ -1 +1 @@
1
- 3.27.1
1
+ 3.28.0
package/kit/guide.html CHANGED
@@ -90,9 +90,9 @@ p { margin: 0 0 1em; }
90
90
  .hero h1 { font-size: clamp(40px, 5.6vw, 64px); font-weight: 800; letter-spacing: -.035em; line-height: 1.04; margin-bottom: 22px; }
91
91
  .hero h1 em { font-style: normal; color: var(--accent); }
92
92
  .hero .sub { font-size: 19px; color: var(--fg-2); max-width: 34em; margin-bottom: 30px; }
93
- .cta { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 30px; }
94
93
  .install { display: flex; align-items: center; gap: 10px; max-width: 520px; background: var(--term-bg); color: var(--term-fg); border-radius: 12px; padding: 12px 12px 12px 18px; font: 14px var(--mono); }
95
- .install span { flex: 1; overflow-x: auto; white-space: nowrap; }
94
+ .install + .install { margin-top: 10px; }
95
+ .install span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
96
96
  .install span::before { content: "$ "; color: var(--term-dim); }
97
97
  .copy { flex: none; background: rgba(255,255,255,.08); color: var(--term-fg); border: 1px solid rgba(255,255,255,.14); border-radius: 8px; font: 500 12.5px var(--font); padding: 6px 10px; cursor: pointer; }
98
98
  .copy:hover { background: rgba(255,255,255,.16); }
@@ -158,7 +158,7 @@ p { margin: 0 0 1em; }
158
158
  .moments { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
159
159
  .moment h3 { font-size: 17px; margin: 18px 0 6px; }
160
160
  .moment > p { color: var(--muted); font-size: 15px; margin: 0; }
161
- .term { background: var(--term-bg); color: var(--term-fg); font: 13px/1.7 var(--mono); padding: 18px 20px; border-radius: 12px; overflow-x: auto; white-space: pre; margin: 0 0 1.2em; }
161
+ .term { background: var(--term-bg); color: var(--term-fg); font: 13px/1.7 var(--mono); padding: 18px 20px; border-radius: 12px; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0 0 1.2em; }
162
162
  .term .c { color: var(--term-dim); }
163
163
  .term .r { color: #ff8a80; font-weight: 600; }
164
164
  .term .g { color: #7ee2a8; }
@@ -287,14 +287,11 @@ footer .container { display: flex; flex-wrap: wrap; gap: 16px; justify-content:
287
287
  <section class="hero">
288
288
  <div class="container">
289
289
  <div>
290
- <span class="pill"><b>v3.26.0</b> Claude Code için başlangıç kiti · MIT</span>
290
+ <span class="pill"><b>v3.28.0</b> Claude Code için başlangıç kiti · MIT</span>
291
291
  <h1>Kodu AI yazar.<br><em>Dümen sende.</em></h1>
292
292
  <p class="sub">TillerHand, Claude Code ile bir ürünü fikirden canlıya ve bakıma kadar götürür. Her önemli kararda AI sana seçenekleri, önerisini ve her seçeneğin bedelini sunar. Bir uzman kurulu işi denetler, korumalar hatayı daha olmadan durdurur.</p>
293
- <div class="cta">
294
- <a class="btn primary" href="#baslangic">Hemen başla</a>
295
- <a class="btn ghost" href="#nasil">Nasıl çalışır?</a>
296
- </div>
297
- <div class="install"><span id="cmd">git clone https://github.com/burakdogrul/TillerHand.git vibe-kit</span><button class="copy" type="button" data-copy="cmd">Kopyala</button></div>
293
+ <div class="install"><span id="cmd">npx tillerhand init</span><button class="copy" type="button" data-copy="cmd">Kopyala</button></div>
294
+ <div class="install"><span id="cmd-git">git clone https://github.com/burakdogrul/TillerHand.git vibe-kit</span><button class="copy" type="button" data-copy="cmd-git">Kopyala</button></div>
298
295
  <p class="hero-note">Claude Code için yazıldı · Codex ve Antigravity'de de çalışır · Kod bilmen gerekmez</p>
299
296
  </div>
300
297
  <div class="window" aria-label="Örnek: AI'nın sana sorduğu bir karar sorusu">
@@ -527,11 +524,13 @@ Bekleyen karar: e-posta servisi</pre>
527
524
  <h2>Kurulum</h2>
528
525
  <p class="lead">TillerHand ve projen ayrı klasörlerde durur. <code>init</code> gereken dosyaları (<code>.claude/</code>, <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>docs/</code>) proje klasörüne kopyalar, git deposu açar ve kit klasörüne dokunmaz. Proje tipi kurulumda değil, <code>/start</code> sırasında soruyla seçilir.</p>
529
526
  <p><b>Gereksinimler:</b> <a href="https://claude.com/claude-code">Claude Code</a>, Git, Node.js 18+.</p>
530
- <pre class="term"><span class="c"># 1. Proje klasörü ve içinde kit</span>
527
+ <pre class="term"><span class="c"># 1. Proje klasöründe kiti kur (npm ile, önerilen)</span>
531
528
  mkdir yeni-proje &amp;&amp; cd yeni-proje
532
- git clone https://github.com/burakdogrul/TillerHand.git vibe-kit
533
- node vibe-kit/kit/kit.mjs init <span class="c"># vibe-kit/ .gitignore'a girer</span>
529
+ npx tillerhand init
534
530
  claude
531
+ <span class="c"># ya da git ile: kit proje içinde vibe-kit/ klasöründe durur (.gitignore'a girer)</span>
532
+ git clone https://github.com/burakdogrul/TillerHand.git vibe-kit
533
+ node vibe-kit/kit/kit.mjs init
535
534
  <span class="c"># 2. Claude Code içinde (yeni oturum)</span>
536
535
  /start Serbest çalışanlar için fatura ve tahsilat takip uygulaması
537
536
  <span class="c"># var olan projede /start yerine:</span>
@@ -549,7 +548,7 @@ claude
549
548
  <ol>
550
549
  <li><b>Hesaplar (bir kez):</b> <a href="https://github.com">GitHub</a> hesabı (ücretsiz) ve Claude <b>Pro</b> veya <b>Max</b> aboneliği.</li>
551
550
  <li><b>Programlar (bir kez):</b> <a href="https://claude.com/download">Claude masaüstü uygulaması</a>, <a href="https://nodejs.org">Node.js</a> (LTS sürümü), <a href="https://git-scm.com/downloads">Git</a>. İstersen <a href="https://desktop.github.com">GitHub Desktop</a> da kurabilirsin.</li>
552
- <li><b>Proje:</b> Projen için bir klasör aç ve Claude masaüstü uygulamasında <b>Code</b> sekmesinde seç. Claude'a yaz: "github.com/burakdogrul/TillerHand reposunu bu klasörün içine vibe-kit adıyla klonla, sonra <code>node vibe-kit/kit/kit.mjs init</code> çalıştır".</li>
551
+ <li><b>Proje:</b> Projen için bir klasör aç ve Claude masaüstü uygulamasında <b>Code</b> sekmesinde seç. Claude'a yaz: "bu klasörde <code>npx tillerhand init</code> komutunu çalıştır".</li>
553
552
  <li><b>Aç:</b> Aynı klasörde <b>yeni oturum</b> aç; kitin komutları ve korumaları yeni oturumda yüklenir. Eklentileri kurmayı önerirse kabul et.</li>
554
553
  <li><b>Başlat:</b> <code>/start fikrin</code> yaz. <b>Her sorunun ilk seçeneği önerilendir</b>; emin değilsen onu seç. Pro'daysan ilk sorular için öneri: profil <b>Hafif</b>, planlama derinliği <b>Hızlı</b> (5 saatlik limitin ~%25-35'i), pano <b>Evet</b>, anlatıcı mod <b>Evet</b>.</li>
555
554
  </ol>
@@ -761,14 +760,14 @@ claude
761
760
  <tbody>
762
761
  <tr><td>Sır engeli</td><td>Koda veya dokümana yazılan API anahtarını (Anthropic, OpenAI, Stripe, AWS, GitHub, Supabase…) engeller; commit öncesi de tarar</td></tr>
763
762
  <tr><td>Yıkıcı komut kalkanı</td><td><code>DROP</code>, <code>TRUNCATE</code>, koşulsuz <code>DELETE</code>, <code>db reset</code> ve prod ortam değişkenlerini makineye çekmek engellenir; prod deploy, geri alma, mağazaya gönderim ve migration sorulur</td></tr>
764
- <tr><td>Paket kapısı</td><td>Paket kayıt defterinden doğrulanır: yoksa engellenir; çok yeni, az indirilmiş veya popüler bir pakete benziyorsa sorulur</td></tr>
763
+ <tr><td>Paket kapısı</td><td>Kurulan ya da <code>npx</code>, <code>uvx</code> ile indirilip çalıştırılan paket kayıt defterinden doğrulanır: yoksa engellenir; çok yeni, az indirilmiş veya popüler bir pakete benziyorsa sorulur</td></tr>
765
764
  <tr><td>Yapı koruyucu</td><td>Harita dışı yeni dosya sorulur; <code>*.bak</code>, <code>*-old.*</code>, kopya ve <code>temp/</code> gibi dağınıklıklar engellenir</td></tr>
766
765
  <tr><td>Yapılandırma onayı</td><td><code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.claude/</code>, <code>.codex/</code>, <code>.mcp.json</code> ve iş akışı dosyaları onayın olmadan değişmez</td></tr>
767
766
  <tr><td>Test koruması</td><td>Beklenti silmek veya yoruma almak, <code>.skip</code>/<code>.only</code>, gevşek beklenti, test dosyasını silmek veya taşımak ve test ayarında kapsamı daraltmak sorulur; kabukla test dosyasına yazmak da</td></tr>
768
767
  <tr><td>Deterministik kapı</td><td><code>gate.mjs --run</code>: lint, tip kontrolü, test ve Semgrep çalışır; çıktı kurula girdi olur, kırmızı kontrol RET sayılır. Spec'teki "değişmemesi gereken" davranışların testi yoksa veya kırmızıysa kapı kırmızıdır. Yeni veya değişen komut önce sana gösterilip onaylatılır</td></tr>
769
- <tr><td>Commit kapısı</td><td>Kurul denetimi ve aynı değişiklikler için yeşil kapı olmadan commit atılmaz</td></tr>
768
+ <tr><td>Commit kapısı</td><td>Kurul denetimi ve aynı değişiklikler için yeşil kapı olmadan commit atılmaz; denetimi atlamak (<code>--skip</code>) her seferinde sorulur</td></tr>
770
769
  <tr><td>Düzenleme kilidi</td><td><code>/freeze &lt;klasör&gt;</code> açıkken AI sadece o klasöre yazar; kabukla dışarı yazmak sorulur, sembolik bağdan kaçılamaz</td></tr>
771
- <tr><td>Kapsam kalkanı</td><td><code>/plan-task</code> onayından sonra planın dosya listesi kaydedilir; listede olmayan mevcut dosyaya yazım sorulur</td></tr>
770
+ <tr><td>Kapsam kalkanı</td><td><code>/plan-task</code> onayından sonra planın dosya listesi kaydedilir; listede olmayan mevcut dosyaya yazım sorulur. Kalkanı açmak ve kapatmak da sorulur</td></tr>
772
771
  <tr><td>Düzenleme turu sayacı</td><td>Aynı kod dosyası bir oturumda 5., 10. kez düzenlenince Semgrep ve güvenlik incelemesi hatırlatılır</td></tr>
773
772
  <tr><td>Dev/prod ayrımı</td><td>Yerel ortam ve AI (salt-okunur MCP dahil) sadece dev projesine bağlanır; prod sırları barındırma panelinde kalır</td></tr>
774
773
  <tr><td>Güvenlik incelemesi</td><td><code>security-guidance</code>: yazarken riskli kalıp uyarısı (istemcide service_role, kişisel veri logu, innerHTML), commit'te ayrı modelle inceleme</td></tr>
@@ -953,7 +952,7 @@ Sıradaki: G-013 şifre sıfırlama · bekleyen karar: e-posta servisi</pre>
953
952
  <p class="path">Başka araçla</p>
954
953
  <h2>Codex ve Antigravity</h2>
955
954
  <p class="lead">TillerHand Claude Code için yazıldı. Kurallar, komutlar ve korumalar OpenAI Codex ve Google Antigravity'de de çalışır. Kurul tek modelle yürür; soru arayüzü ve durum çubuğu bu araçlarda yok.</p>
956
- <pre class="term">node vibe-kit/kit/kit.mjs init --tool codex <span class="c"># ya da antigravity, ya da claude,codex</span>
955
+ <pre class="term">npx tillerhand init --tool codex <span class="c"># ya da antigravity, ya da claude,codex</span>
957
956
  <span class="c"># var olan projeye araç eklemek</span>
958
957
  /kit-update --tool claude,codex</pre>
959
958
  <div class="table-wrap"><table>
package/kit/kit.mjs CHANGED
@@ -117,7 +117,9 @@ const OLD_RULES = {
117
117
  allow: ['Bash(node .claude/hooks/freeze-guard.mjs --plan-kaydet:*)', 'Bash(node .claude/hooks/freeze-guard.mjs --plan-bitir)',
118
118
  'Bash(node .claude/hooks/tools.mjs --gerekli:*)', 'Bash(node .claude/hooks/tools.mjs --olc)', 'Bash(node .claude/hooks/tools.mjs --kapat:*)',
119
119
  // 3.19.1: in the project folder the CLI loads a repo's .playwright/cli.config.json (it can run a program); asked, not allowed
120
- ...['open', 'snapshot', 'console', 'requests', 'screenshot'].map((c) => `Bash(npx -y @playwright/cli@0.1.22 ${c}:*)`)],
120
+ ...['open', 'snapshot', 'console', 'requests', 'screenshot'].map((c) => `Bash(npx -y @playwright/cli@0.1.22 ${c}:*)`),
121
+ // 3.28.0: --plan-end + --plan-save widened or dropped the scope shield unasked; both are asked now
122
+ 'Bash(node .claude/hooks/freeze-guard.mjs --plan-save:*)', 'Bash(node .claude/hooks/freeze-guard.mjs --plan-end)'],
121
123
  ask: ['Bash(node .claude/hooks/freeze-guard.mjs --plan-ekle:*)', 'Bash(node .claude/hooks/gate.mjs --onayla)', 'Bash(node .claude/hooks/tools.mjs --ac:*)'],
122
124
  };
123
125
 
@@ -3,7 +3,7 @@
3
3
  // If Prettier is not installed in the project it passes silently; it never blocks the work.
4
4
  import { readFileSync, existsSync } from 'node:fs';
5
5
  import { spawnSync } from 'node:child_process';
6
- import { join, extname } from 'node:path';
6
+ import { join, extname, resolve } from 'node:path';
7
7
 
8
8
  const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md']);
9
9
 
@@ -11,9 +11,11 @@ try {
11
11
  const input = JSON.parse(readFileSync(0, 'utf8'));
12
12
  const file = input.tool_input?.file_path;
13
13
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
14
- const bin = join(root, 'node_modules', '.bin', process.platform === 'win32' ? 'prettier.cmd' : 'prettier');
14
+ // Prettier 3's own entry run with node: no shell, so a spaced or special-character path is passed as is
15
+ const bin = join(root, 'node_modules', 'prettier', 'bin', 'prettier.cjs');
15
16
  if (file && EXTENSIONS.has(extname(file)) && existsSync(bin)) {
16
- spawnSync(bin, ['--write', '--log-level', 'warn', file], { cwd: root, stdio: 'ignore', shell: process.platform === 'win32' });
17
+ // shortcut: Prettier 3 only (v2 has bin-prettier.js and --loglevel); the kit's stack installs the latest. Absolute path: never read as a flag
18
+ spawnSync(process.execPath, [bin, '--write', '--log-level', 'warn', resolve(root, file)], { cwd: root, stdio: 'ignore' });
17
19
  }
18
20
  } catch {
19
21
  // a formatting error must not break the edit
@@ -3,7 +3,7 @@
3
3
  // If Prettier is not installed in the project it passes silently; it never blocks the work.
4
4
  import { readFileSync, existsSync } from 'node:fs';
5
5
  import { spawnSync } from 'node:child_process';
6
- import { join, extname } from 'node:path';
6
+ import { join, extname, resolve } from 'node:path';
7
7
 
8
8
  const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md']);
9
9
 
@@ -11,9 +11,11 @@ try {
11
11
  const input = JSON.parse(readFileSync(0, 'utf8'));
12
12
  const file = input.tool_input?.file_path;
13
13
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
14
- const bin = join(root, 'node_modules', '.bin', process.platform === 'win32' ? 'prettier.cmd' : 'prettier');
14
+ // Prettier 3's own entry run with node: no shell, so a spaced or special-character path is passed as is
15
+ const bin = join(root, 'node_modules', 'prettier', 'bin', 'prettier.cjs');
15
16
  if (file && EXTENSIONS.has(extname(file)) && existsSync(bin)) {
16
- spawnSync(bin, ['--write', '--log-level', 'warn', file], { cwd: root, stdio: 'ignore', shell: process.platform === 'win32' });
17
+ // shortcut: Prettier 3 only (v2 has bin-prettier.js and --loglevel); the kit's stack installs the latest. Absolute path: never read as a flag
18
+ spawnSync(process.execPath, [bin, '--write', '--log-level', 'warn', resolve(root, file)], { cwd: root, stdio: 'ignore' });
17
19
  }
18
20
  } catch {
19
21
  // a formatting error must not break the edit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tillerhand",
3
- "version": "3.27.1",
3
+ "version": "3.28.0",
4
4
  "description": "Claude Code, Codex ve Antigravity için Türkçe vibe coding kiti: kurallar, komutlar, korumalar ve proje şablonları.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  "CLAUDE.md"
23
23
  ],
24
24
  "engines": {
25
- "node": ">=22"
25
+ "node": ">=18"
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",