tillerhand 3.43.0 → 3.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/board-chair.md +20 -20
- package/.claude/agents/code-reviewer.md +20 -20
- package/.claude/agents/council-advisor.md +14 -14
- package/.claude/agents/security-auditor.md +30 -30
- package/.claude/agents/test-writer.md +22 -22
- package/.claude/board/board-ai.md +25 -25
- package/.claude/board/board-api.md +25 -25
- package/.claude/board/board-architect.md +28 -28
- package/.claude/board/board-devops.md +25 -25
- package/.claude/board/board-mobile.md +25 -25
- package/.claude/board/board-performance.md +24 -24
- package/.claude/board/board-privacy.md +25 -25
- package/.claude/board/board-product.md +26 -26
- package/.claude/board/board-qa.md +28 -28
- package/.claude/board/board-ux.md +26 -26
- package/.claude/board/council.md +76 -76
- package/.claude/board/rules.md +61 -61
- package/.claude/claude-security-guidance.md +10 -10
- package/.claude/commands/adopt.md +70 -69
- package/.claude/commands/ask-mode.md +9 -9
- package/.claude/commands/board.md +15 -15
- package/.claude/commands/build.md +47 -47
- package/.claude/commands/dashboard.md +17 -17
- package/.claude/commands/design.md +31 -31
- package/.claude/commands/findings-all.md +14 -14
- package/.claude/commands/findings-business.md +7 -7
- package/.claude/commands/findings-legal.md +7 -7
- package/.claude/commands/findings-perf.md +7 -7
- package/.claude/commands/findings-security.md +9 -9
- package/.claude/commands/findings-tech.md +7 -7
- package/.claude/commands/findings-ux.md +7 -7
- package/.claude/commands/fix-bug.md +15 -15
- package/.claude/commands/freeze.md +8 -8
- package/.claude/commands/handoff.md +38 -38
- package/.claude/commands/kit-update.md +17 -14
- package/.claude/commands/language.md +13 -0
- package/.claude/commands/maintain.md +19 -19
- package/.claude/commands/plan-task.md +32 -32
- package/.claude/commands/qa.md +17 -17
- package/.claude/commands/release.md +40 -40
- package/.claude/commands/research.md +31 -31
- package/.claude/commands/retro.md +32 -32
- package/.claude/commands/review-changes.md +15 -15
- package/.claude/commands/rollback.md +24 -24
- package/.claude/commands/start.md +116 -113
- package/.claude/commands/tools.md +11 -11
- package/.claude/commands/user-feedback.md +11 -11
- package/.claude/commands/wrap-up.md +18 -18
- package/.claude/findings/lenses/business.md +24 -24
- package/.claude/findings/lenses/legal.md +29 -29
- package/.claude/findings/lenses/perf.md +19 -19
- package/.claude/findings/lenses/security.md +30 -30
- package/.claude/findings/lenses/tech.md +23 -23
- package/.claude/findings/lenses/ux.md +19 -19
- package/.claude/findings/protocol.md +54 -54
- package/.claude/findings/report-template.md +30 -30
- package/.claude/hooks/budget.mjs +7 -7
- package/.claude/hooks/checkpoint.mjs +12 -12
- package/.claude/hooks/commit-gate.mjs +38 -37
- package/.claude/hooks/cost.mjs +2 -2
- package/.claude/hooks/council.mjs +3 -3
- package/.claude/hooks/dashboard-data.mjs +4 -3
- package/.claude/hooks/dashboard.mjs +164 -149
- package/.claude/hooks/destructive-guard.mjs +74 -64
- package/.claude/hooks/edit-rounds.mjs +3 -3
- package/.claude/hooks/flow.mjs +10 -10
- package/.claude/hooks/freeze-guard.mjs +27 -23
- package/.claude/hooks/gate.mjs +25 -25
- package/.claude/hooks/guard-secrets.mjs +11 -11
- package/.claude/hooks/lang.mjs +114 -0
- package/.claude/hooks/messages.mjs +495 -0
- package/.claude/hooks/models.mjs +14 -11
- package/.claude/hooks/notify.mjs +3 -2
- package/.claude/hooks/package-gate.mjs +17 -15
- package/.claude/hooks/plugins-sync.mjs +9 -9
- package/.claude/hooks/prefs.mjs +19 -19
- package/.claude/hooks/preview.mjs +11 -11
- package/.claude/hooks/progress.mjs +14 -14
- package/.claude/hooks/qa-report.mjs +11 -10
- package/.claude/hooks/reminder.mjs +6 -3
- package/.claude/hooks/session-start.mjs +23 -17
- package/.claude/hooks/shared.mjs +1 -1
- package/.claude/hooks/status-read.mjs +5 -4
- package/.claude/hooks/statusline.mjs +14 -11
- package/.claude/hooks/structure-guard.mjs +9 -9
- package/.claude/hooks/test-guard.mjs +12 -12
- package/.claude/hooks/tools.mjs +28 -29
- package/.claude/hooks/usage.mjs +18 -15
- package/.claude/output-styles/anlatici.md +28 -28
- package/.claude/security-patterns.json +3 -3
- package/.claude/settings.json +2 -0
- package/.claude/skills/logo-design/SKILL.md +42 -42
- package/.claude/skills/motion-video/SKILL.md +52 -52
- package/.claude/skills/source-research/SKILL.md +35 -35
- package/.claude/skills/ui-motion/SKILL.md +38 -38
- package/.github/workflows/kit-tests.yml +7 -7
- package/.github/workflows/npm-publish.yml +16 -16
- package/AGENTS.md +56 -56
- package/CHANGELOG.md +7 -7
- package/CLAUDE.md +5 -5
- package/README.md +199 -172
- package/README.tr.md +292 -0
- package/docs/architecture/overview.md +31 -31
- package/docs/architecture/stack.md +2 -2
- package/docs/archive/README.md +4 -4
- package/docs/decisions/adr-template.md +14 -14
- package/docs/decisions/log.md +10 -10
- package/docs/findings/README.md +13 -13
- package/docs/findings/config.md +13 -13
- package/docs/legal/privacy-kvkk.md +1 -1
- package/docs/operations/accounts.md +25 -25
- package/docs/operations/deploy-setup.md +64 -64
- package/docs/operations/feature-flags.md +17 -17
- package/docs/operations/maintenance.md +48 -48
- package/docs/operations/release-checklist.md +61 -61
- package/docs/operations/spend-alarms.md +28 -28
- package/docs/product/feedback.md +9 -8
- package/docs/product/glossary.md +16 -16
- package/docs/product/launch.md +28 -28
- package/docs/product/prd.md +43 -42
- package/docs/product/roadmap.md +31 -30
- package/docs/product/specs/_template.md +13 -12
- package/docs/product/unit-economics.md +23 -22
- package/docs/quality/metrics.md +14 -14
- package/docs/quality/retros.md +11 -11
- package/docs/quality/test-plan.md +48 -48
- package/docs/security/checklist.md +57 -57
- package/docs/workflow/board.md +7 -7
- package/docs/workflow/cost-log.md +4 -4
- package/docs/workflow/decision-protocol.md +70 -70
- package/docs/workflow/learning.md +4 -4
- package/docs/workflow/mcp-catalog.md +31 -31
- package/docs/workflow/models.md +35 -35
- package/docs/workflow/progress.md +7 -7
- package/docs/workflow/skills.md +108 -108
- package/docs/workflow/usage-limits.md +62 -62
- package/kit/CHANGELOG.md +882 -841
- package/kit/VERSION +1 -1
- package/kit/guide.html +713 -477
- package/kit/guide.tr.html +1328 -0
- package/kit/kit.mjs +56 -54
- package/kit/project/README.md +29 -29
- package/kit/project/gitignore +12 -12
- package/kit/templates/addons/ai/.env.example.append +5 -5
- package/kit/templates/addons/ai/ai/evals/README.md +11 -11
- package/kit/templates/addons/ai/ai/evals/cases.jsonl +2 -2
- package/kit/templates/addons/ai/ai/prompts/README.md +8 -8
- package/kit/templates/addons/ai/docs/ai/guide.md +56 -56
- package/kit/templates/backend/.claude/hooks/format.mjs +1 -1
- package/kit/templates/backend/.claude/structure.json +1 -1
- package/kit/templates/backend/.env.example +6 -6
- package/kit/templates/backend/.github/dependabot.yml +1 -1
- package/kit/templates/backend/.github/workflows/ci.yml +13 -13
- package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/backend/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/backend/.github/workflows/live-monitoring.yml +14 -14
- package/kit/templates/backend/.pre-commit-config.yaml +3 -3
- package/kit/templates/backend/docs/architecture/stack.md +98 -98
- package/kit/templates/backend/docs/architecture/structure.md +34 -34
- package/kit/templates/mobile/.claude/structure.json +1 -1
- package/kit/templates/mobile/.env.example +6 -6
- package/kit/templates/mobile/.github/dependabot.yml +2 -2
- package/kit/templates/mobile/.github/workflows/ci.yml +18 -18
- package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/mobile/.gitignore.append +2 -2
- package/kit/templates/mobile/docs/architecture/stack.md +113 -113
- package/kit/templates/mobile/docs/architecture/structure.md +33 -33
- package/kit/templates/mobile/docs/design/design-guide.md +37 -37
- package/kit/templates/mobile/tests/security/rls.mjs +22 -22
- package/kit/templates/web-saas/.claude/structure.json +1 -1
- package/kit/templates/web-saas/.env.example +11 -11
- package/kit/templates/web-saas/.github/dependabot.yml +3 -3
- package/kit/templates/web-saas/.github/workflows/ci.yml +20 -20
- package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +12 -12
- package/kit/templates/web-saas/docs/architecture/stack.md +116 -116
- package/kit/templates/web-saas/docs/architecture/structure.md +44 -44
- package/kit/templates/web-saas/docs/design/design-guide.md +38 -38
- package/kit/templates/web-saas/tests/live/smoke.spec.ts +4 -4
- package/kit/templates/web-saas/tests/security/rls.mjs +22 -22
- package/kit/tools/codex/.codex/hooks.json +1 -1
- package/package.json +2 -2
- package/README.en.md +0 -267
package/.claude/hooks/gate.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The result goes to .claude/state/gate.json with the same change signature as the commit gate; the commit gate stops
|
|
8
8
|
// the commit on a red or stale result. A green check's output is not written, the last 30 lines of a red one are.
|
|
9
9
|
// If no check passed (commands empty, no Semgrep) the result is EMPTY: the commit gate stays closed.
|
|
10
|
-
// "
|
|
10
|
+
// "Absolute path": an added line with this machine's project or home path is red; a generic C:\Users\<name>, /home/<name> is a note.
|
|
11
11
|
// node .claude/hooks/gate.mjs --approve saves the commands with user approval. A new or changed command does not run unapproved:
|
|
12
12
|
// an untrusted repo's AGENTS.md must not run code through the gate. The approval lives in the user folder (~/.vibe-kit/).
|
|
13
13
|
import fs from 'node:fs';
|
|
@@ -34,7 +34,7 @@ function failed(root, name, r) {
|
|
|
34
34
|
const rel = `.claude/state/gate-log/${name.toLowerCase().replace(/\s+/g, '-')}.log`;
|
|
35
35
|
fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true });
|
|
36
36
|
fs.writeFileSync(path.join(root, rel), all);
|
|
37
|
-
return { output: lines.slice(-30).join('\n'), note: `
|
|
37
|
+
return { output: lines.slice(-30).join('\n'), note: `last 30 lines; full output in ${rel}` };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// Only the "## Komutlar" section is read: a "test:" line elsewhere must not count as a command
|
|
@@ -64,7 +64,7 @@ export function approve(root) {
|
|
|
64
64
|
const file = approvalPath(root);
|
|
65
65
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
66
|
fs.writeFileSync(file, JSON.stringify({ digest: digest(list), commands: list, date: new Date().toISOString() }, null, 2));
|
|
67
|
-
return `
|
|
67
|
+
return `Gate commands approved:\n${commandList(list) || '(no commands defined)'}`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export function isApproved(root, list = approvedCommands(root)) {
|
|
@@ -85,7 +85,7 @@ function check(root, { name, command }) {
|
|
|
85
85
|
function semgrep(root, files) {
|
|
86
86
|
const name = 'Semgrep';
|
|
87
87
|
const code = files.filter((f) => isCodeFile(f) && fs.existsSync(path.join(root, f)));
|
|
88
|
-
if (!code.length) return { name, status: 'skipped', note: '
|
|
88
|
+
if (!code.length) return { name, status: 'skipped', note: 'no changed code files' };
|
|
89
89
|
const start = Date.now();
|
|
90
90
|
const cmd = process.env.SEMGREP || 'semgrep';
|
|
91
91
|
// --metrics=off: the code scan must not send usage data (docs.semgrep.dev/cli-reference, 2026-10-24)
|
|
@@ -97,8 +97,8 @@ function semgrep(root, files) {
|
|
|
97
97
|
const outside = (y) => y && path.relative(root, y).startsWith('..');
|
|
98
98
|
const found = r.error.code === 'EINVAL' ? cmd
|
|
99
99
|
: process.platform === 'win32' ? (spawnSync('where', [cmd], { encoding: 'utf8' }).stdout ?? '').split(/\r?\n/).map((y) => y.trim()).find(outside) ?? '' : '';
|
|
100
|
-
return { name, status: 'skipped', note: found ? `${found}
|
|
101
|
-
: '
|
|
100
|
+
return { name, status: 'skipped', note: found ? `${found} found but cannot run (.cmd/.bat); pass SEMGREP=<path to semgrep.exe>`
|
|
101
|
+
: 'not installed; the semgrep job in CI runs it' };
|
|
102
102
|
}
|
|
103
103
|
const sec = Math.round((Date.now() - start) / 1000);
|
|
104
104
|
if (r.status === 0) return { name, status: 'passed', sec };
|
|
@@ -115,7 +115,7 @@ function semgrep(root, files) {
|
|
|
115
115
|
// read whole. Paths are read from the git top level, so a project in a monorepo subfolder is checked too.
|
|
116
116
|
const ESCAPE = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
117
117
|
export function absPathCheck(root, files) {
|
|
118
|
-
const name = '
|
|
118
|
+
const name = 'Absolute path';
|
|
119
119
|
const git = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8', maxBuffer: BUFFER });
|
|
120
120
|
const top = git('rev-parse', '--show-toplevel').stdout?.trim() || root;
|
|
121
121
|
const variants = (p) => { const f = p.replace(/\\/g, '/').replace(/\/+$/, ''); return [f, f.replace(/\//g, '\\'), f.replace(/\//g, '\\\\')]; };
|
|
@@ -148,14 +148,14 @@ export function absPathCheck(root, files) {
|
|
|
148
148
|
if (mine?.test(l)) red.push(at); else if (generic.test(l)) notes.push(at);
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
|
-
const list = (a) => [...a.slice(0, 15), a.length > 15 ? `…
|
|
152
|
-
if (red.length) return { name, status: 'failed', note: '
|
|
151
|
+
const list = (a) => [...a.slice(0, 15), a.length > 15 ? `… and ${a.length - 15} more lines` : ''].filter(Boolean).join('\n');
|
|
152
|
+
if (red.length) return { name, status: 'failed', note: 'path specific to this machine; use import.meta.url, __dirname, path.join(root, …), os.tmpdir(). If deliberate, write kit:yol-izinli on the line or the line above',
|
|
153
153
|
output: list(red) };
|
|
154
|
-
return { name, status: 'passed', ...(notes.length && { note: `${notes.length}
|
|
154
|
+
return { name, status: 'passed', ...(notes.length && { note: `${notes.length} lines have a path that looks like a user folder; fix it if it is machine-specific (does not block)`, output: list(notes) }) };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
export const approvalNeeded = (list) => `
|
|
158
|
-
'
|
|
157
|
+
export const approvalNeeded = (list) => `command approval needed\nAGENTS.md → Komutlar is new or changed; these commands do not run unapproved:\n${commandList(list)}\n` +
|
|
158
|
+
'Show the commands to the user and ask. If they approve: node .claude/hooks/gate.mjs --approve, then run the command again.';
|
|
159
159
|
|
|
160
160
|
// Spec drift (F-23): a "must not change" behavior whose test is missing or red stops the gate; no result is only a note
|
|
161
161
|
// Only reports written during this gate run count (since = gate start): an older report says nothing about the code now.
|
|
@@ -164,17 +164,17 @@ function specCheck(root, since) {
|
|
|
164
164
|
if (!list.length && !unreadable.length) return null;
|
|
165
165
|
const bad = list.filter((k) => !k.test || k.result === 'failed');
|
|
166
166
|
const unknown = list.filter((k) => k.test && !k.result).length;
|
|
167
|
-
const notes = [`${list.length}
|
|
168
|
-
unknown && `${unknown}
|
|
169
|
-
unreadable.length && `${unreadable.length}
|
|
167
|
+
const notes = [`${list.length} behaviors that must not change`,
|
|
168
|
+
unknown && `${unknown} tests have no result in this run (the test command writes no JUnit report)`,
|
|
169
|
+
unreadable.length && `${unreadable.length} lines could not be read, write them as "- Ö-01.1: ...": ${unreadable.map((u) => `${u.spec}: ${u.line}`).join(' · ')}`];
|
|
170
170
|
return { name: 'Spec', status: bad.length ? 'failed' : 'passed', note: notes.filter(Boolean).join('; '),
|
|
171
|
-
output: bad.length ? bad.map((k) => `${k.id} (${k.spec}): ${k.test ? 'test
|
|
171
|
+
output: bad.length ? bad.map((k) => `${k.id} (${k.spec}): ${k.test ? 'test red' : 'no test'}`).join('\n') : undefined };
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// Status label and mark
|
|
175
|
-
export const STATUS_LABEL = { passed: '
|
|
174
|
+
// Status label and mark in the gate output
|
|
175
|
+
export const STATUS_LABEL = { passed: 'passed', failed: 'failed', unset: 'unset', skipped: 'skipped' };
|
|
176
176
|
const MARK = { passed: '✅', failed: '❌', unset: '⚪', skipped: '⚪' };
|
|
177
|
-
const line = (k) => `${MARK[k.status]} ${k.name}${k.sec != null ? ` (${k.sec}
|
|
177
|
+
const line = (k) => `${MARK[k.status]} ${k.name}${k.sec != null ? ` (${k.sec} s)` : ''}${k.status === 'passed' ? '' : `: ${STATUS_LABEL[k.status]}`}${k.note ? ` (${k.note})` : ''}` +
|
|
178
178
|
(k.output ? `\n\`\`\`\n${k.output}\n\`\`\`` : '');
|
|
179
179
|
|
|
180
180
|
export function runGate(root) {
|
|
@@ -182,7 +182,7 @@ export function runGate(root) {
|
|
|
182
182
|
let signature = null, files = [];
|
|
183
183
|
try { ({ signature, files } = changes()); } catch { /* not a git repo: the commands still run */ }
|
|
184
184
|
const list = approvedCommands(root);
|
|
185
|
-
if (!isApproved(root, list)) return { passed: false, text: `##
|
|
185
|
+
if (!isApproved(root, list)) return { passed: false, text: `## Gate · ${approvalNeeded(list)}` };
|
|
186
186
|
const checks = [...list.filter((k) => CHECKS.includes(k.name)).map((k) => check(root, k)), semgrep(root, files)];
|
|
187
187
|
const empty = !checks.some((k) => k.status === 'passed');
|
|
188
188
|
const spec = specCheck(root, start); // after the test command, which may have refreshed the JUnit report; it does not fill an empty gate
|
|
@@ -192,16 +192,16 @@ export function runGate(root) {
|
|
|
192
192
|
const file = path.join(root, '.claude', 'state', 'gate.json');
|
|
193
193
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
194
194
|
fs.writeFileSync(file, JSON.stringify({ signature, date: new Date().toISOString(), passed, empty, checks }, null, 2));
|
|
195
|
-
const result = checks.some((k) => k.status === 'failed') ? '
|
|
196
|
-
: empty ? '
|
|
197
|
-
: '
|
|
198
|
-
return { passed, text: [`##
|
|
195
|
+
const result = checks.some((k) => k.status === 'failed') ? 'Result: RED. The commit gate stays closed: fix it and run the gate again.'
|
|
196
|
+
: empty ? 'Result: EMPTY, no check ran. Fill in AGENTS.md → Komutlar; the commit gate stays closed (if the user wants, commit-gate.mjs --skip).'
|
|
197
|
+
: 'Result: GREEN.';
|
|
198
|
+
return { passed, text: [`## Gate · ${files.length} files changed`, ...checks.map(line), result].join('\n') };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if (isMain(import.meta.url)) {
|
|
202
202
|
const arg = process.argv[2];
|
|
203
203
|
if (arg === '--approve') { console.log(approve(findRoot())); process.exit(0); }
|
|
204
|
-
if (arg !== '--run') { console.error('
|
|
204
|
+
if (arg !== '--run') { console.error('Usage: node .claude/hooks/gate.mjs --run | --approve'); process.exit(1); }
|
|
205
205
|
const { passed, text } = runGate(findRoot());
|
|
206
206
|
console.log(text);
|
|
207
207
|
process.exit(passed ? 0 : 1);
|
|
@@ -8,14 +8,14 @@ import { execSync } from 'node:child_process';
|
|
|
8
8
|
import { readInput, decide, isMain } from './shared.mjs';
|
|
9
9
|
|
|
10
10
|
export const PATTERNS = [
|
|
11
|
-
['Anthropic/OpenAI
|
|
12
|
-
['Stripe
|
|
13
|
-
['AWS
|
|
11
|
+
['Anthropic/OpenAI key', /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{32,}/],
|
|
12
|
+
['Stripe secret key', /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{20,}/],
|
|
13
|
+
['AWS access key', /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/],
|
|
14
14
|
['GitHub token', /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}|\bgithub_pat_[A-Za-z0-9_]{50,}/],
|
|
15
15
|
['Slack token', /\bxox[abprs]-[A-Za-z0-9-]{10,}/],
|
|
16
|
-
['
|
|
17
|
-
['Resend
|
|
18
|
-
['Supabase
|
|
16
|
+
['Private key', /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/],
|
|
17
|
+
['Resend key', /\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{16,}/],
|
|
18
|
+
['Supabase secret key', /\bsb_secret_(?=[\w-]*\d)[\w-]{16,}/],
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
export function findSecrets(text) {
|
|
@@ -27,14 +27,14 @@ function hookMode() {
|
|
|
27
27
|
if (!g) process.exit(0); // unreadable input: do not block the work
|
|
28
28
|
// Codex and Antigravity also scan shell commands (heredoc, echo > file); Claude Code only file tools
|
|
29
29
|
if (g.tool !== 'claude' && findSecrets(g.command).length) {
|
|
30
|
-
decide(g.tool, 'deny', `
|
|
30
|
+
decide(g.tool, 'deny', `BLOCKED: the command contains a secret (${findSecrets(g.command).join(', ')}). Do not write the secret into a command or a file; read it from an environment variable.`);
|
|
31
31
|
}
|
|
32
32
|
for (const { path, text } of g.files) {
|
|
33
33
|
const found = findSecrets(text);
|
|
34
34
|
if (found.length) {
|
|
35
35
|
decide(g.tool, 'deny',
|
|
36
|
-
`
|
|
37
|
-
'
|
|
36
|
+
`BLOCKED: a secret is being written to ${path} (${found.join(', ')}). ` +
|
|
37
|
+
'Do not put the secret in code; read it from an environment variable and add the variable name to .env.example with an empty value.');
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -51,8 +51,8 @@ function stagedMode() {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
if (findings.length) {
|
|
54
|
-
console.error('Commit
|
|
55
|
-
console.error('
|
|
54
|
+
console.error('Commit stopped, secret found:\n ' + findings.join('\n '));
|
|
55
|
+
console.error('Remove the secret and move it to an environment variable. Revoke and replace any key that may have leaked.');
|
|
56
56
|
process.exit(1);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Conversation language of the project. /start and /adopt ask it first, /language changes it, /kit-update asks old projects once.
|
|
3
|
+
// node .claude/hooks/lang.mjs --show current code (none: not chosen yet)
|
|
4
|
+
// node .claude/hooks/lang.mjs --set <code> BCP-47 code (tr, en, de, pt-BR); .kit.json → lang, before install .claude/state/lang.json
|
|
5
|
+
// node .claude/hooks/lang.mjs --export [--missing] English texts as JSON, to translate for a language other than tr/en
|
|
6
|
+
// node .claude/hooks/lang.mjs --write < x.json saves the translation to .claude/i18n/<code>.json (merged)
|
|
7
|
+
// node .claude/hooks/lang.mjs --check exit 1 and lists the keys still missing a translation
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { findRoot, isMain } from './shared.mjs';
|
|
11
|
+
import { MESSAGES } from './messages.mjs';
|
|
12
|
+
|
|
13
|
+
// The code becomes a file name: nothing that can leave .claude/i18n/
|
|
14
|
+
export const LANG_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
15
|
+
const PLACEHOLDER = /\{(\w+)\}/g;
|
|
16
|
+
|
|
17
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
18
|
+
// Windows device names (nul.json is not a file) and over-long names cannot become a file either
|
|
19
|
+
const DEVICE = /^(con|prn|aux|nul|com\d|lpt\d)$/;
|
|
20
|
+
const validCode = (code) => typeof code === 'string' && code.length <= 35 && LANG_RE.test(code) && !DEVICE.test(code) ? code : null;
|
|
21
|
+
const kitFile = (root) => path.join(root, '.kit.json');
|
|
22
|
+
const stateFile = (root) => path.join(root, '.claude', 'state', 'lang.json');
|
|
23
|
+
const i18nFile = (root, lang) => path.join(root, '.claude', 'i18n', `${lang}.json`);
|
|
24
|
+
const placeholders = (s) => [...new Set([...s.matchAll(PLACEHOLDER)].map((m) => m[1]))].sort().join(',');
|
|
25
|
+
|
|
26
|
+
// null: not set up and not chosen yet (/start asks). Projects set up before languages existed were Turkish.
|
|
27
|
+
export function readLang(root = findRoot()) {
|
|
28
|
+
const kit = readJson(kitFile(root));
|
|
29
|
+
const chosen = validCode(kit?.lang) ?? validCode(readJson(stateFile(root))?.lang);
|
|
30
|
+
if (chosen) return chosen;
|
|
31
|
+
// same rule as status-read kitPending: a broken .kit.json counts as missing
|
|
32
|
+
const notSetUp = kit ? kit.type === null : fs.existsSync(path.join(root, 'kit', 'kit.mjs'));
|
|
33
|
+
return notSetUp ? null : 'tr';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setLang(root, code) {
|
|
37
|
+
if (!validCode(code)) throw new Error(`Invalid language code: ${code}. Use a BCP-47 code such as tr, en, de, pt-BR.`);
|
|
38
|
+
if (fs.existsSync(kitFile(root))) {
|
|
39
|
+
const kit = readJson(kitFile(root));
|
|
40
|
+
if (!kit || typeof kit !== 'object' || Array.isArray(kit)) throw new Error('.kit.json is not a valid JSON object: repair it first, then set the language.');
|
|
41
|
+
fs.writeFileSync(kitFile(root), JSON.stringify({ ...kit, lang: code }, null, 2) + '\n');
|
|
42
|
+
return kitFile(root);
|
|
43
|
+
}
|
|
44
|
+
fs.mkdirSync(path.dirname(stateFile(root)), { recursive: true });
|
|
45
|
+
fs.writeFileSync(stateFile(root), JSON.stringify({ lang: code }) + '\n');
|
|
46
|
+
return stateFile(root);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const translation = (root, lang) => {
|
|
50
|
+
const j = readJson(i18nFile(root, lang));
|
|
51
|
+
return j && typeof j === 'object' && !Array.isArray(j) ? j : {};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function t(root, key, vars = {}) {
|
|
55
|
+
if (!Object.hasOwn(MESSAGES, key)) return key;
|
|
56
|
+
const lang = readLang(root) ?? 'en';
|
|
57
|
+
let text = MESSAGES[key][lang];
|
|
58
|
+
if (text === undefined) {
|
|
59
|
+
const own = translation(root, lang);
|
|
60
|
+
text = Object.hasOwn(own, key) && typeof own[key] === 'string' ? own[key] : MESSAGES[key].en;
|
|
61
|
+
}
|
|
62
|
+
return text.replace(PLACEHOLDER, (m, name) => Object.hasOwn(vars, name) ? String(vars[name]) : m);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const needsTranslation = (lang) => lang !== null && lang !== 'tr' && lang !== 'en';
|
|
66
|
+
|
|
67
|
+
export function missingKeys(root = findRoot()) {
|
|
68
|
+
const lang = readLang(root);
|
|
69
|
+
if (!needsTranslation(lang)) return [];
|
|
70
|
+
const own = translation(root, lang);
|
|
71
|
+
return Object.keys(MESSAGES).filter((k) => !(Object.hasOwn(own, k) && typeof own[k] === 'string'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function writeTranslation(root, obj) {
|
|
75
|
+
const lang = readLang(root);
|
|
76
|
+
if (!needsTranslation(lang)) throw new Error(`No translation file for language ${lang ?? 'none'}: tr and en are built in.`);
|
|
77
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj) || Object.getPrototypeOf(obj) !== Object.prototype) throw new Error('Translation must be a JSON object { key: text }.');
|
|
78
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
79
|
+
if (!Object.hasOwn(MESSAGES, k)) throw new Error(`Unknown key: ${k}`);
|
|
80
|
+
if (typeof v !== 'string') throw new Error(`Not a text: ${k}`);
|
|
81
|
+
if (placeholders(v) !== placeholders(MESSAGES[k].en)) throw new Error(`${k}: keep the placeholders exactly ({${placeholders(MESSAGES[k].en).split(',').join('}, {')}}).`);
|
|
82
|
+
}
|
|
83
|
+
const file = i18nFile(root, lang);
|
|
84
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
85
|
+
fs.writeFileSync(file, JSON.stringify({ ...translation(root, lang), ...obj }, null, 2) + '\n');
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isMain(import.meta.url)) {
|
|
90
|
+
const root = findRoot();
|
|
91
|
+
const [cmd, arg] = process.argv.slice(2);
|
|
92
|
+
try {
|
|
93
|
+
if (cmd === '--show') console.log(readLang(root) ?? 'none');
|
|
94
|
+
else if (cmd === '--set') console.log(`Language: ${arg} → ${path.relative(root, setLang(root, arg))}`);
|
|
95
|
+
else if (cmd === '--export') {
|
|
96
|
+
const keys = arg === '--missing' ? missingKeys(root) : Object.keys(MESSAGES);
|
|
97
|
+
console.log(JSON.stringify(Object.fromEntries(keys.map((k) => [k, MESSAGES[k].en])), null, 2));
|
|
98
|
+
} else if (cmd === '--write') {
|
|
99
|
+
let obj;
|
|
100
|
+
try { obj = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { throw new Error('stdin is not valid JSON.'); }
|
|
101
|
+
console.log(`Written: ${path.relative(root, writeTranslation(root, obj))}`);
|
|
102
|
+
} else if (cmd === '--check') {
|
|
103
|
+
const missing = missingKeys(root);
|
|
104
|
+
if (missing.length) { console.log(missing.join('\n')); process.exit(1); }
|
|
105
|
+
console.log('ok');
|
|
106
|
+
} else {
|
|
107
|
+
console.error('Usage: --show | --set <code> | --export [--missing] | --write < file.json | --check');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(err.message);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|