tink-harness 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +8 -0
- package/CHANGELOG.md +109 -0
- package/LICENSE +21 -0
- package/README.ko.md +224 -0
- package/README.md +166 -0
- package/VERSIONING.md +73 -0
- package/bin/install.js +520 -0
- package/commands/cast.md +484 -0
- package/commands/frog.md +77 -0
- package/commands/list.md +104 -0
- package/commands/setup.md +185 -0
- package/commands/update.md +90 -0
- package/commands/weave.md +81 -0
- package/hooks/hooks.json +15 -0
- package/package.json +52 -0
- package/skills/tink/SKILL.md +66 -0
- package/templates/claude/commands/tink/cast.md +484 -0
- package/templates/claude/commands/tink/frog.md +77 -0
- package/templates/claude/commands/tink/list.md +104 -0
- package/templates/claude/commands/tink/setup.md +185 -0
- package/templates/claude/commands/tink/update.md +90 -0
- package/templates/claude/commands/tink/weave.md +81 -0
- package/templates/claude/skills/tink/SKILL.md +66 -0
- package/templates/tink/config.json +20 -0
- package/templates/tink/harnesses/HARNESS.md +28 -0
- package/templates/tink/harnesses/bug-fix.md +31 -0
- package/templates/tink/harnesses/code-change.md +30 -0
- package/templates/tink/harnesses/docs.md +30 -0
- package/templates/tink/harnesses/harness-curation.md +78 -0
- package/templates/tink/harnesses/harness-synthesis.md +52 -0
- package/templates/tink/harnesses/index.json +157 -0
- package/templates/tink/harnesses/pre-publish-multi-agent-verify.md +44 -0
- package/templates/tink/harnesses/research.md +31 -0
- package/templates/tink/harnesses/review.md +31 -0
- package/templates/tink/harnesses/ship.md +33 -0
- package/templates/tink/harnesses/tink-feedback-apply.md +37 -0
- package/templates/tink/hooks/user-prompt-submit.json +7 -0
- package/templates/tink/hooks/user-prompt-submit.mjs +49 -0
- package/templates/tink/maintenance/ledger.jsonl +0 -0
- package/templates/tink/maintenance/weave-queue.json +3 -0
- package/templates/tink/memory/lessons.md +17 -0
- package/templates/tink/memory/mistakes.md +16 -0
- package/templates/tink/memory/preferences.md +16 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { cancel, intro, isCancel, log, multiselect, note, outro, select, spinner } from '@clack/prompts';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const root = path.resolve(__dirname, '..');
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const command = args[0] || 'install';
|
|
14
|
+
const isUpdate = command === 'update';
|
|
15
|
+
const dryRun = args.includes('--dry-run');
|
|
16
|
+
const force = args.includes('--force');
|
|
17
|
+
const yes = args.includes('--yes') || args.includes('-y');
|
|
18
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !yes && !dryRun;
|
|
19
|
+
const source = 'https://github.com/dotoricode/tink-harness.git';
|
|
20
|
+
|
|
21
|
+
const COPY = {
|
|
22
|
+
en: {
|
|
23
|
+
intro: ' tink ',
|
|
24
|
+
source: 'Source',
|
|
25
|
+
discovering: 'Discovering Tink templates...',
|
|
26
|
+
found: 'harnesses',
|
|
27
|
+
language: 'Language / 언어 / 语言',
|
|
28
|
+
components: 'Select components to install (space to toggle)',
|
|
29
|
+
scope: 'Installation scope',
|
|
30
|
+
gitNoteTitle: 'Git tracking',
|
|
31
|
+
gitNote: '`.tink/harnesses/` contains reusable work templates. `.tink/current/`, `.tink/runs/`, and `.tink/cache/` are runtime state.',
|
|
32
|
+
gitPolicy: 'Project .tink tracking',
|
|
33
|
+
hookScope: 'Hook scope',
|
|
34
|
+
installed: 'Installed',
|
|
35
|
+
done: 'Done'
|
|
36
|
+
},
|
|
37
|
+
ko: {
|
|
38
|
+
intro: ' tink ',
|
|
39
|
+
source: 'Source',
|
|
40
|
+
discovering: 'Tink 템플릿 확인 중...',
|
|
41
|
+
found: '개 harness 발견',
|
|
42
|
+
language: 'Language / 언어 / 语言',
|
|
43
|
+
components: '설치할 항목을 선택하세요 (space로 토글)',
|
|
44
|
+
scope: '설치 범위',
|
|
45
|
+
gitNoteTitle: 'Git 추적 정책',
|
|
46
|
+
gitNote: '`.tink/harnesses/`는 재사용 작업 템플릿입니다. `.tink/current/`, `.tink/runs/`, `.tink/cache/`는 실행 중 임시 상태입니다.',
|
|
47
|
+
gitPolicy: '프로젝트 .tink 추적 방식',
|
|
48
|
+
hookScope: 'Hook 범위',
|
|
49
|
+
installed: '설치 완료',
|
|
50
|
+
done: '완료'
|
|
51
|
+
},
|
|
52
|
+
zh: {
|
|
53
|
+
intro: ' tink ',
|
|
54
|
+
source: 'Source',
|
|
55
|
+
discovering: '正在检查 Tink 模板...',
|
|
56
|
+
found: '个 harness',
|
|
57
|
+
language: 'Language / 언어 / 语言',
|
|
58
|
+
components: '选择要安装的项目(空格切换)',
|
|
59
|
+
scope: '安装范围',
|
|
60
|
+
gitNoteTitle: 'Git 跟踪策略',
|
|
61
|
+
gitNote: '`.tink/harnesses/` 是可复用工作模板。`.tink/current/`, `.tink/runs/`, `.tink/cache/` 是运行时临时状态。',
|
|
62
|
+
gitPolicy: '项目 .tink 跟踪方式',
|
|
63
|
+
hookScope: 'Hook 范围',
|
|
64
|
+
installed: '安装完成',
|
|
65
|
+
done: '完成'
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const COMPONENTS = {
|
|
70
|
+
en: [
|
|
71
|
+
{ value: 'commands', label: 'Claude Code commands', hint: '/tink:setup, /tink:cast, /tink:list, /tink:frog, /tink:weave, /tink:update' },
|
|
72
|
+
{ value: 'skill', label: 'Tink skill', hint: 'Tink operating rules for Claude Code' },
|
|
73
|
+
{ value: 'harnesses', label: 'Built-in harnesses', hint: 'Reusable task templates' },
|
|
74
|
+
{ value: 'memory', label: 'Memory templates', hint: 'Approved mistakes/preferences/lessons files' },
|
|
75
|
+
{ value: 'hook', label: 'Hook recommendation (optional)', hint: 'Registers a safe UserPromptSubmit hook when selected. Off by default.' }
|
|
76
|
+
],
|
|
77
|
+
ko: [
|
|
78
|
+
{ value: 'commands', label: 'Claude Code 명령', hint: '/tink:setup, /tink:cast, /tink:list, /tink:frog, /tink:weave, /tink:update' },
|
|
79
|
+
{ value: 'skill', label: 'Tink skill', hint: 'Claude Code가 읽는 Tink 작업 원칙' },
|
|
80
|
+
{ value: 'harnesses', label: '기본 harness', hint: '재사용 작업 템플릿' },
|
|
81
|
+
{ value: 'memory', label: 'Memory 템플릿', hint: '승인된 실수/선호/교훈 파일' },
|
|
82
|
+
{ value: 'hook', label: 'Hook 추천 (선택)', hint: '선택하면 안전한 UserPromptSubmit hook으로 등록합니다. 기본 off.' }
|
|
83
|
+
],
|
|
84
|
+
zh: [
|
|
85
|
+
{ value: 'commands', label: 'Claude Code 命令', hint: '/tink:setup, /tink:cast, /tink:list, /tink:frog, /tink:weave, /tink:update' },
|
|
86
|
+
{ value: 'skill', label: 'Tink skill', hint: 'Claude Code 读取的 Tink 工作规则' },
|
|
87
|
+
{ value: 'harnesses', label: '内置 harness', hint: '可复用任务模板' },
|
|
88
|
+
{ value: 'memory', label: 'Memory 模板', hint: '经批准的错误/偏好/经验文件' },
|
|
89
|
+
{ value: 'hook', label: 'Hook 推荐(可选)', hint: '选择后注册安全的 UserPromptSubmit hook。默认关闭。' }
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function argValue(name) {
|
|
94
|
+
const prefix = `${name}=`;
|
|
95
|
+
const found = args.find((arg) => arg.startsWith(prefix));
|
|
96
|
+
return found ? found.slice(prefix.length) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function usage() {
|
|
100
|
+
console.log(`Tink installer for Claude Code\n\nUsage:\n npx tink-harness@latest [install] [--scope=repo|global] [--global] [--lang=en|ko|zh] [--yes] [--with-hook] [--dry-run] [--force]\n npx tink-harness@latest update [--scope=repo|global] [--global] [--lang=en|ko|zh] [--yes] [--dry-run] [--force]\n\nCommands:\n install Install Tink.\n update Update Tink to the latest templates. Keeps user-modified files.\n\nDefault interactive flow:\n 1. Select language\n 2. Show TINK wizard\n 3. Select components\n 4. Select repo/global installation scope\n 5. Select git tracking policy for project state\n\nScopes:\n repo Install into the current project.\n global Install into your home Claude Code config.\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function colorLine(line, color) {
|
|
104
|
+
if (!process.stdout.isTTY && !interactive) return line;
|
|
105
|
+
const [r, g, b] = color;
|
|
106
|
+
return `\x1b[38;2;${r};${g};${b}m${line}\x1b[0m`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printBanner() {
|
|
110
|
+
const lines = [
|
|
111
|
+
'████████╗██╗███╗ ██╗██╗ ██╗',
|
|
112
|
+
'╚══██╔══╝██║████╗ ██║██║ ██╔╝',
|
|
113
|
+
' ██║ ██║██╔██╗ ██║█████╔╝',
|
|
114
|
+
' ██║ ██║██║╚██╗██║██╔═██╗',
|
|
115
|
+
' ██║ ██║██║ ╚████║██║ ██╗',
|
|
116
|
+
' ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝'
|
|
117
|
+
];
|
|
118
|
+
// Keep the banner readable on dark terminal themes. The previous deep navy
|
|
119
|
+
// top of the gradient blended into black backgrounds.
|
|
120
|
+
const top = [96, 165, 250];
|
|
121
|
+
const bottom = [34, 211, 238];
|
|
122
|
+
console.log('');
|
|
123
|
+
lines.forEach((line, i) => {
|
|
124
|
+
const t = i / Math.max(lines.length - 1, 1);
|
|
125
|
+
const color = [
|
|
126
|
+
Math.round(top[0] + (bottom[0] - top[0]) * t),
|
|
127
|
+
Math.round(top[1] + (bottom[1] - top[1]) * t),
|
|
128
|
+
Math.round(top[2] + (bottom[2] - top[2]) * t)
|
|
129
|
+
];
|
|
130
|
+
console.log(colorLine(line, color));
|
|
131
|
+
});
|
|
132
|
+
console.log('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleCancel(value) {
|
|
136
|
+
if (isCancel(value)) {
|
|
137
|
+
cancel('Installation cancelled');
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readHarnessCount() {
|
|
144
|
+
const dir = path.join(root, 'templates/tink/harnesses');
|
|
145
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith('.md')).length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function displayPath(base, filePath) {
|
|
149
|
+
return path.relative(base, filePath) || '.';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isAlwaysUpdatePath(src) {
|
|
153
|
+
const rel = path.relative(root, src).replace(/\\/g, '/');
|
|
154
|
+
return rel.startsWith('templates/claude/commands/') ||
|
|
155
|
+
rel.startsWith('templates/claude/skills/') ||
|
|
156
|
+
rel.startsWith('templates/tink/maintenance/');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeFileFromTemplate(src, dest, base) {
|
|
160
|
+
const exists = fs.existsSync(dest);
|
|
161
|
+
if (exists && !force) {
|
|
162
|
+
if (isUpdate) {
|
|
163
|
+
const srcContent = fs.readFileSync(src);
|
|
164
|
+
const destContent = fs.readFileSync(dest);
|
|
165
|
+
if (Buffer.compare(srcContent, destContent) === 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!isAlwaysUpdatePath(src)) {
|
|
169
|
+
log.message(`keep user-modified ${displayPath(base, dest)}`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// commands/skills/maintenance: always update to new version
|
|
173
|
+
} else {
|
|
174
|
+
log.message(`skip existing ${displayPath(base, dest)}`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
log.message(`${dryRun ? 'would write' : (exists ? 'update' : 'write')} ${displayPath(base, dest)}`);
|
|
179
|
+
if (!dryRun) {
|
|
180
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
181
|
+
fs.copyFileSync(src, dest);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function copyDir(src, dest, base) {
|
|
186
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
187
|
+
const s = path.join(src, entry.name);
|
|
188
|
+
const d = path.join(dest, entry.name);
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
if (!dryRun) fs.mkdirSync(d, { recursive: true });
|
|
191
|
+
copyDir(s, d, base);
|
|
192
|
+
} else {
|
|
193
|
+
writeFileFromTemplate(s, d, base);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function copyTinkCommands(templateRoot, target) {
|
|
199
|
+
const commandSrc = path.join(templateRoot, 'claude/commands/tink');
|
|
200
|
+
const commandDest = path.join(target, '.claude/commands/tink');
|
|
201
|
+
const flatCommandDest = path.join(target, '.claude/commands');
|
|
202
|
+
const legacyFlatCommands = ['tink-setup.md', 'tink-forge.md', 'tink-list.md', 'tink-purge.md', 'tink-hone.md'];
|
|
203
|
+
const legacyNamespaceCommands = ['forge.md', 'purge.md', 'hone.md'];
|
|
204
|
+
const legacyTinyCommands = ['tiny-setup.md', 'tiny-use.md', 'tiny-list.md', 'tiny-save.md'];
|
|
205
|
+
const legacyDirs = [path.join(flatCommandDest, 'tiny'), path.join(target, '.claude/skills/tiny')];
|
|
206
|
+
for (const name of legacyFlatCommands) {
|
|
207
|
+
const legacy = path.join(flatCommandDest, name);
|
|
208
|
+
if (fs.existsSync(legacy)) {
|
|
209
|
+
log.message(`${dryRun ? 'would remove legacy' : 'remove legacy'} ${displayPath(target, legacy)}`);
|
|
210
|
+
if (!dryRun) fs.rmSync(legacy, { force: true });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const name of legacyNamespaceCommands) {
|
|
214
|
+
const legacy = path.join(commandDest, name);
|
|
215
|
+
if (fs.existsSync(legacy)) {
|
|
216
|
+
log.message(`${dryRun ? 'would remove legacy' : 'remove legacy'} ${displayPath(target, legacy)}`);
|
|
217
|
+
if (!dryRun) fs.rmSync(legacy, { force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const name of legacyTinyCommands) {
|
|
221
|
+
const legacy = path.join(flatCommandDest, name);
|
|
222
|
+
if (fs.existsSync(legacy)) {
|
|
223
|
+
log.message(`${dryRun ? 'would remove legacy Tiny' : 'remove legacy Tiny'} ${displayPath(target, legacy)}`);
|
|
224
|
+
if (!dryRun) fs.rmSync(legacy, { force: true });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const legacyDir of legacyDirs) {
|
|
228
|
+
if (fs.existsSync(legacyDir)) {
|
|
229
|
+
log.message(`${dryRun ? 'would remove legacy Tiny' : 'remove legacy Tiny'} ${displayPath(target, legacyDir)}`);
|
|
230
|
+
if (!dryRun) fs.rmSync(legacyDir, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const entry of fs.readdirSync(commandSrc, { withFileTypes: true })) {
|
|
234
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
235
|
+
writeFileFromTemplate(path.join(commandSrc, entry.name), path.join(commandDest, entry.name), target);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
function readJsonFile(filePath, fallback) {
|
|
241
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
242
|
+
try {
|
|
243
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
244
|
+
} catch {
|
|
245
|
+
return fallback;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function writeJsonFile(filePath, value, base) {
|
|
250
|
+
log.message(`${dryRun ? 'would write' : 'write'} ${displayPath(base, filePath)}`);
|
|
251
|
+
if (!dryRun) {
|
|
252
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
253
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function hookCommandFor(scope, target) {
|
|
258
|
+
const script = path.join(target, '.tink/hooks/user-prompt-submit.mjs');
|
|
259
|
+
const display = scope === 'repo' ? '.tink/hooks/user-prompt-submit.mjs' : script;
|
|
260
|
+
return `node ${JSON.stringify(display)}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function registerClaudeHook(target, scope, base) {
|
|
264
|
+
const settingsPath = path.join(target, '.claude/settings.json');
|
|
265
|
+
const settings = readJsonFile(settingsPath, {});
|
|
266
|
+
const command = hookCommandFor(scope, target);
|
|
267
|
+
settings.hooks ||= {};
|
|
268
|
+
const entries = Array.isArray(settings.hooks.UserPromptSubmit) ? settings.hooks.UserPromptSubmit : [];
|
|
269
|
+
const filtered = entries.filter((entry) => !JSON.stringify(entry).includes('user-prompt-submit.mjs'));
|
|
270
|
+
filtered.push({
|
|
271
|
+
matcher: '',
|
|
272
|
+
hooks: [{ type: 'command', command }]
|
|
273
|
+
});
|
|
274
|
+
settings.hooks.UserPromptSubmit = filtered;
|
|
275
|
+
writeJsonFile(settingsPath, settings, base);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function copySelected(scope, components) {
|
|
279
|
+
const repoTarget = process.cwd();
|
|
280
|
+
const globalTarget = os.homedir();
|
|
281
|
+
const target = scope === 'global' ? globalTarget : repoTarget;
|
|
282
|
+
const templateRoot = path.join(root, 'templates');
|
|
283
|
+
|
|
284
|
+
if (components.includes('commands')) {
|
|
285
|
+
copyTinkCommands(templateRoot, target);
|
|
286
|
+
}
|
|
287
|
+
if (components.includes('skill')) {
|
|
288
|
+
copyDir(path.join(templateRoot, 'claude/skills'), path.join(target, '.claude/skills'), target);
|
|
289
|
+
}
|
|
290
|
+
if (components.includes('harnesses')) {
|
|
291
|
+
copyDir(path.join(templateRoot, 'tink/harnesses'), path.join(target, '.tink/harnesses'), target);
|
|
292
|
+
copyDir(path.join(templateRoot, 'tink/maintenance'), path.join(target, '.tink/maintenance'), target);
|
|
293
|
+
writeFileFromTemplate(path.join(templateRoot, 'tink/config.json'), path.join(target, '.tink/config.json'), target);
|
|
294
|
+
}
|
|
295
|
+
if (components.includes('memory')) {
|
|
296
|
+
copyDir(path.join(templateRoot, 'tink/memory'), path.join(target, '.tink/memory'), target);
|
|
297
|
+
}
|
|
298
|
+
if (components.includes('hook')) {
|
|
299
|
+
copyDir(path.join(templateRoot, 'tink/hooks'), path.join(target, '.tink/hooks'), target);
|
|
300
|
+
registerClaudeHook(target, scope, target);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { repoTarget, globalTarget, installTarget: target };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function updateGitignore(target, policy) {
|
|
307
|
+
if (policy === 'all') {
|
|
308
|
+
log.message('skip .gitignore update: tracking all .tink files');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const gitignorePath = path.join(target, '.gitignore');
|
|
312
|
+
const ignoreBlock = policy === 'none'
|
|
313
|
+
? ['.tink/']
|
|
314
|
+
: ['.tink/current/', '.tink/runs/', '.tink/cache/'];
|
|
315
|
+
|
|
316
|
+
if (fs.existsSync(gitignorePath)) {
|
|
317
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
318
|
+
const missing = ignoreBlock.filter((line) => !existing.includes(line));
|
|
319
|
+
if (missing.length) {
|
|
320
|
+
log.message(`${dryRun ? 'would update' : 'update'} .gitignore`);
|
|
321
|
+
if (!dryRun) fs.appendFileSync(gitignorePath, `\n# Tink runtime state\n${missing.join('\n')}\n`);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
log.message(`${dryRun ? 'would write' : 'write'} .gitignore`);
|
|
325
|
+
if (!dryRun) fs.writeFileSync(gitignorePath, `# Tink runtime state\n${ignoreBlock.join('\n')}\n`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function patchConfig(target, scope, hookScope, language) {
|
|
330
|
+
const configPath = path.join(target, '.tink/config.json');
|
|
331
|
+
if (!fs.existsSync(configPath) || dryRun) return;
|
|
332
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
333
|
+
config.install_scope = scope;
|
|
334
|
+
config.hook_scope = hookScope;
|
|
335
|
+
config.language = language;
|
|
336
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function detectLanguage() {
|
|
340
|
+
const envLang = (process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || '').toLowerCase();
|
|
341
|
+
if (envLang.startsWith('ko') || envLang.includes('.ko') || envLang.includes('_kr')) return 'ko';
|
|
342
|
+
if (envLang.startsWith('zh') || envLang.includes('_cn') || envLang.includes('_tw') || envLang.includes('_hk')) return 'zh';
|
|
343
|
+
return 'en';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function resolveChoices() {
|
|
347
|
+
let scope = args.includes('--global') ? 'global' : (argValue('--scope') || undefined);
|
|
348
|
+
let language = argValue('--lang') || argValue('--language') || detectLanguage();
|
|
349
|
+
if (scope && !['repo', 'global'].includes(scope)) {
|
|
350
|
+
console.error(`Invalid scope: ${scope}`);
|
|
351
|
+
usage();
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
if (!['en', 'ko', 'zh'].includes(language)) language = 'en';
|
|
355
|
+
|
|
356
|
+
let components = COMPONENTS[language].map((item) => item.value).filter((value) => value !== 'hook');
|
|
357
|
+
if (args.includes('--with-hook')) components.push('hook');
|
|
358
|
+
let gitPolicy = 'harnesses';
|
|
359
|
+
let hookScope = 'off';
|
|
360
|
+
|
|
361
|
+
if (!interactive) {
|
|
362
|
+
scope = scope || 'repo';
|
|
363
|
+
if (components.includes('hook')) hookScope = scope;
|
|
364
|
+
return { scope, components, gitPolicy, hookScope, language };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
language = handleCancel(await select({
|
|
368
|
+
message: COPY[language].language,
|
|
369
|
+
options: [
|
|
370
|
+
{ value: 'ko', label: '한국어', hint: '설치 안내와 Tink 기본 문구를 한국어로 설정' },
|
|
371
|
+
{ value: 'en', label: 'English', hint: 'Use English setup copy' },
|
|
372
|
+
{ value: 'zh', label: '中文', hint: '使用中文安装说明' }
|
|
373
|
+
],
|
|
374
|
+
initialValue: language
|
|
375
|
+
}));
|
|
376
|
+
|
|
377
|
+
const copy = COPY[language];
|
|
378
|
+
printBanner();
|
|
379
|
+
intro(pc.bgBlue(pc.white(copy.intro)));
|
|
380
|
+
log.message(`${copy.source}: ${source}`);
|
|
381
|
+
|
|
382
|
+
const s = spinner();
|
|
383
|
+
s.start(copy.discovering);
|
|
384
|
+
const harnessCount = readHarnessCount();
|
|
385
|
+
s.stop(language === 'ko' ? `Found ${pc.green(harnessCount)} ${copy.found}` : `Found ${pc.green(harnessCount)} ${copy.found}`);
|
|
386
|
+
|
|
387
|
+
note(
|
|
388
|
+
language === 'ko'
|
|
389
|
+
? '기본 항목은 대부분 그대로 설치하면 됩니다. Hook은 선택 사항이며 기본으로 꺼져 있습니다.'
|
|
390
|
+
: language === 'zh'
|
|
391
|
+
? '默认项目通常保持选中即可。Hook 是可选项,默认关闭。'
|
|
392
|
+
: 'The defaults are usually enough. Hook is optional and off by default.',
|
|
393
|
+
language === 'ko' ? '항목 설명' : language === 'zh' ? '项目说明' : 'Component notes'
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
components = handleCancel(await multiselect({
|
|
397
|
+
message: copy.components,
|
|
398
|
+
options: COMPONENTS[language],
|
|
399
|
+
initialValues: components,
|
|
400
|
+
required: true
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
scope = handleCancel(await select({
|
|
404
|
+
message: copy.scope,
|
|
405
|
+
options: [
|
|
406
|
+
{
|
|
407
|
+
value: 'repo',
|
|
408
|
+
label: language === 'ko' ? 'Repo' : language === 'zh' ? 'Repo' : 'Repo',
|
|
409
|
+
hint: language === 'ko'
|
|
410
|
+
? '현재 프로젝트에 설치. 프로젝트 harness에 권장.'
|
|
411
|
+
: language === 'zh'
|
|
412
|
+
? '安装到当前项目。推荐用于项目 harness。'
|
|
413
|
+
: 'Install into current project. Recommended for project harnesses.'
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
value: 'global',
|
|
417
|
+
label: language === 'ko' ? 'Global' : language === 'zh' ? 'Global' : 'Global',
|
|
418
|
+
hint: language === 'ko'
|
|
419
|
+
? '사용자 홈에 설치. 여러 프로젝트에서 같은 명령 사용.'
|
|
420
|
+
: language === 'zh'
|
|
421
|
+
? '安装到用户目录。多个项目共用命令。'
|
|
422
|
+
: 'Install into home config. Available across projects.'
|
|
423
|
+
}
|
|
424
|
+
],
|
|
425
|
+
initialValue: scope || 'repo'
|
|
426
|
+
}));
|
|
427
|
+
|
|
428
|
+
if (scope === 'repo' && components.some((item) => ['harnesses', 'memory', 'hook'].includes(item))) {
|
|
429
|
+
note(copy.gitNote, copy.gitNoteTitle);
|
|
430
|
+
gitPolicy = handleCancel(await select({
|
|
431
|
+
message: copy.gitPolicy,
|
|
432
|
+
options: [
|
|
433
|
+
{
|
|
434
|
+
value: 'harnesses',
|
|
435
|
+
label: language === 'ko' ? 'Harnesses만 커밋' : language === 'zh' ? '只提交 harnesses' : 'Commit harnesses only',
|
|
436
|
+
hint: language === 'ko' ? '권장. current/runs/cache 제외.' : language === 'zh' ? '推荐。忽略 current/runs/cache。' : 'Recommended. Ignore current/runs/cache.'
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
value: 'all',
|
|
440
|
+
label: language === 'ko' ? '전부 커밋' : language === 'zh' ? '全部提交' : 'Commit all .tink files',
|
|
441
|
+
hint: language === 'ko' ? '대부분 비권장.' : language === 'zh' ? '通常不推荐。' : 'Usually not recommended.'
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
value: 'none',
|
|
445
|
+
label: language === 'ko' ? '커밋 안 함' : language === 'zh' ? '不提交 .tink' : 'Commit no .tink files',
|
|
446
|
+
hint: language === 'ko' ? '이 머신에만 유지.' : language === 'zh' ? '仅保留在本机。' : 'Keep Tink local to this machine.'
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
initialValue: 'harnesses'
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (components.includes('hook')) {
|
|
454
|
+
hookScope = scope;
|
|
455
|
+
note(
|
|
456
|
+
language === 'ko'
|
|
457
|
+
? `Hook 추천을 ${scope} 범위의 Claude Code UserPromptSubmit에 등록합니다. 추가 범위 질문은 하지 않습니다. 작업 실행/저장 없이 일반 프롬프트에서만 Tink 사용을 추천합니다.`
|
|
458
|
+
: language === 'zh'
|
|
459
|
+
? `Hook 推荐模板将安装到 ${scope} 范围。不再询问额外 hook 范围。它不会执行或保存内容,只在普通提示中建议使用 Tink。`
|
|
460
|
+
: `Hook recommendation will be registered as a Claude Code UserPromptSubmit hook in ${scope} scope. No extra hook scope question. It does not execute or save anything; it only suggests Tink on normal prompts.`,
|
|
461
|
+
language === 'ko' ? 'Hook 안전성' : language === 'zh' ? 'Hook 安全性' : 'Hook safety'
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return { scope, components, gitPolicy, hookScope, language };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function main() {
|
|
469
|
+
if (command === 'help' || args.includes('--help')) {
|
|
470
|
+
usage();
|
|
471
|
+
process.exit(0);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (command !== 'install' && command !== 'update') {
|
|
475
|
+
console.error(`Unknown command: ${command}`);
|
|
476
|
+
usage();
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const { scope, components, gitPolicy, hookScope, language } = await resolveChoices();
|
|
481
|
+
|
|
482
|
+
if (!interactive) {
|
|
483
|
+
console.log('Installing Tink for Claude Code');
|
|
484
|
+
console.log(`Source: ${source}`);
|
|
485
|
+
console.log(`language ${language}`);
|
|
486
|
+
console.log(`scope ${scope}`);
|
|
487
|
+
console.log(`components ${components.join(', ')}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const targets = copySelected(scope, components);
|
|
491
|
+
|
|
492
|
+
if (scope === 'repo' && components.some((item) => ['harnesses', 'memory', 'hook'].includes(item))) {
|
|
493
|
+
updateGitignore(targets.repoTarget, gitPolicy);
|
|
494
|
+
} else if (scope === 'global') {
|
|
495
|
+
log.message('skip .gitignore for global install');
|
|
496
|
+
}
|
|
497
|
+
patchConfig(targets.installTarget, scope, hookScope, language);
|
|
498
|
+
|
|
499
|
+
const summary = [
|
|
500
|
+
`Language: ${language}`,
|
|
501
|
+
`Scope: ${scope}`,
|
|
502
|
+
`Install target: ${targets.installTarget}`,
|
|
503
|
+
`Components: ${components.join(', ')}`,
|
|
504
|
+
`Hook scope: ${hookScope}`,
|
|
505
|
+
'Next: open Claude Code and run /tink:cast <task> to start. Run /tink:setup only to review or change settings.'
|
|
506
|
+
].join('\n');
|
|
507
|
+
|
|
508
|
+
if (interactive) {
|
|
509
|
+
note(summary, COPY[language].installed);
|
|
510
|
+
outro(COPY[language].done);
|
|
511
|
+
} else {
|
|
512
|
+
console.log(`\n${summary}`);
|
|
513
|
+
console.log('\nDone. Open Claude Code and run /tink:cast <task> to start.');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
main().catch((error) => {
|
|
518
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
519
|
+
process.exit(1);
|
|
520
|
+
});
|