@haposoft/cafekit 0.11.0 → 0.11.2
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/bin/lib/context.js +8 -3
- package/bin/lib/i18n.js +2 -1
- package/bin/phases/post-install.js +12 -6
- package/bin/phases/select-platform.js +24 -7
- package/package.json +1 -1
- package/src/claude/gitignore +1 -0
- package/src/claude/hooks/session.cjs +96 -4
package/bin/lib/context.js
CHANGED
|
@@ -247,6 +247,9 @@ function buildContext(argv, runId) {
|
|
|
247
247
|
const interactive = isInteractive() && !options.yes;
|
|
248
248
|
// Language: --lang wins; otherwise English (an interactive prompt may change it).
|
|
249
249
|
const lang = options.lang ? resolveLang(options.lang) : 'en';
|
|
250
|
+
// ctx.locale = the string that gets written to CLAUDE.md / runtime.json as the AI's response language.
|
|
251
|
+
// For known codes it mirrors lang; for "other", it's set later by setLang.
|
|
252
|
+
const locale = options.lang || 'en';
|
|
250
253
|
return {
|
|
251
254
|
argv,
|
|
252
255
|
runId,
|
|
@@ -254,11 +257,13 @@ function buildContext(argv, runId) {
|
|
|
254
257
|
dryRun: options.dryRun,
|
|
255
258
|
interactive,
|
|
256
259
|
ui: createUI(interactive),
|
|
257
|
-
lang,
|
|
260
|
+
lang, // UI language (one of SUPPORTED)
|
|
261
|
+
locale, // AI response language label (may be freeform, e.g. "Korean")
|
|
258
262
|
t: createTranslator(lang),
|
|
259
|
-
/** Update
|
|
260
|
-
setLang(code) {
|
|
263
|
+
/** Update UI lang + AI locale. rawLabel is the freeform label when user picks "Other". */
|
|
264
|
+
setLang(code, rawLabel) {
|
|
261
265
|
this.lang = resolveLang(code);
|
|
266
|
+
this.locale = rawLabel || LANGUAGE_LABELS[this.lang] || this.lang;
|
|
262
267
|
this.t = createTranslator(this.lang);
|
|
263
268
|
},
|
|
264
269
|
manifest: loadClaudeMigrationManifest(), // migration manifest (skills/agents/runtime lists)
|
package/bin/lib/i18n.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const SUPPORTED = ['en', 'ja', 'vi'];
|
|
12
12
|
|
|
13
13
|
const LANGUAGE_LABELS = { en: 'English', ja: '日本語', vi: 'Tiếng Việt' };
|
|
14
|
+
const OTHER_LABEL = { en: 'Other...', ja: 'その他...', vi: 'Ngôn ngữ khác...' };
|
|
14
15
|
|
|
15
16
|
const MESSAGES = {
|
|
16
17
|
// ─── English ───────────────────────────────────────────────────────────────
|
|
@@ -264,4 +265,4 @@ function createTranslator(lang) {
|
|
|
264
265
|
};
|
|
265
266
|
}
|
|
266
267
|
|
|
267
|
-
module.exports = { SUPPORTED, LANGUAGE_LABELS, MESSAGES, resolveLang, createTranslator };
|
|
268
|
+
module.exports = { SUPPORTED, LANGUAGE_LABELS, OTHER_LABEL, MESSAGES, resolveLang, createTranslator };
|
|
@@ -30,8 +30,9 @@ function patchSettingsLanguage(ctx) {
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Use locale (freeform label) so "Korean" shows correctly, not just "en".
|
|
34
|
+
const label = ctx.locale || LANGUAGE_LABELS[ctx.lang] || ctx.lang;
|
|
35
|
+
if (settings.language === label) return;
|
|
35
36
|
settings.language = label;
|
|
36
37
|
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
37
38
|
}
|
|
@@ -46,13 +47,16 @@ function patchSettingsLanguage(ctx) {
|
|
|
46
47
|
*/
|
|
47
48
|
function patchLanguageSection(ctx) {
|
|
48
49
|
if (!ctx.platforms.includes('claude')) return;
|
|
49
|
-
|
|
50
|
+
// Skip when locale is English (default — no override needed in CLAUDE.md).
|
|
51
|
+
const locale = ctx.locale || ctx.lang;
|
|
52
|
+
if (!locale || locale === 'en' || locale === 'English') return;
|
|
50
53
|
|
|
51
54
|
const claudeMdFile = path.join(process.cwd(), 'CLAUDE.md');
|
|
52
55
|
if (!fs.existsSync(claudeMdFile)) return;
|
|
53
56
|
|
|
54
57
|
const LANG_LABEL = { vi: 'Vietnamese', ja: 'Japanese', en: 'English' };
|
|
55
|
-
|
|
58
|
+
// Use locale directly when set (e.g. "Korean"), or map from lang code.
|
|
59
|
+
const label = locale in LANG_LABEL ? LANG_LABEL[locale] : locale;
|
|
56
60
|
|
|
57
61
|
const newSection = `## Language Consistency <!-- cafekit:lang -->
|
|
58
62
|
|
|
@@ -86,8 +90,10 @@ function patchRuntimeLocale(ctx) {
|
|
|
86
90
|
continue;
|
|
87
91
|
}
|
|
88
92
|
data.locale = data.locale || {};
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
// Use locale (freeform label) so custom languages propagate to the AI hook.
|
|
94
|
+
const locale = ctx.locale || ctx.lang;
|
|
95
|
+
if (data.locale.responseLanguage === locale) continue;
|
|
96
|
+
data.locale.responseLanguage = locale;
|
|
91
97
|
fs.writeFileSync(rtPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
92
98
|
if (ctx.trackers[key]) ctx.trackers[key].record(rtPath);
|
|
93
99
|
}
|
|
@@ -12,21 +12,38 @@ const {
|
|
|
12
12
|
getPlatformKeys,
|
|
13
13
|
warnLegacyClaudeFolder
|
|
14
14
|
} = require('../lib/context');
|
|
15
|
-
const { SUPPORTED, LANGUAGE_LABELS } = require('../lib/i18n');
|
|
15
|
+
const { SUPPORTED, LANGUAGE_LABELS, OTHER_LABEL } = require('../lib/i18n');
|
|
16
|
+
|
|
17
|
+
const OTHER = '__other__';
|
|
16
18
|
|
|
17
19
|
/** First step: pick the installer/UI language (interactive only). */
|
|
18
20
|
async function selectLanguage(ctx) {
|
|
19
|
-
// Honor an explicit --lang or non-interactive default (English) without prompting.
|
|
20
21
|
if (!ctx.interactive || ctx.options.lang) return ctx;
|
|
21
22
|
|
|
23
|
+
const options = [
|
|
24
|
+
...SUPPORTED.map((code) => ({ value: code, label: LANGUAGE_LABELS[code] })),
|
|
25
|
+
{ value: OTHER, label: OTHER_LABEL.en + ' / その他 / Ngôn ngữ khác' }
|
|
26
|
+
];
|
|
27
|
+
|
|
22
28
|
const r = await ctx.ui.select(
|
|
23
|
-
{
|
|
24
|
-
message: 'Select language · 言語を選択 · Chọn ngôn ngữ',
|
|
25
|
-
options: SUPPORTED.map((code) => ({ value: code, label: LANGUAGE_LABELS[code] }))
|
|
26
|
-
},
|
|
29
|
+
{ message: 'Select language · 言語を選択 · Chọn ngôn ngữ', options },
|
|
27
30
|
ctx.lang
|
|
28
31
|
);
|
|
29
|
-
if (
|
|
32
|
+
if (ctx.ui.isCancel(r)) return ctx;
|
|
33
|
+
|
|
34
|
+
if (r === OTHER) {
|
|
35
|
+
// User types their language name — UI stays English, AI responds in that language.
|
|
36
|
+
const typed = await ctx.ui.text(
|
|
37
|
+
{ message: 'Enter your language (e.g. Korean, French, Spanish…)', placeholder: 'Language name in English' },
|
|
38
|
+
''
|
|
39
|
+
);
|
|
40
|
+
if (!ctx.ui.isCancel(typed) && typed && typed.trim()) {
|
|
41
|
+
ctx.setLang('en', typed.trim()); // UI = English, locale = user's label
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
ctx.setLang(r, LANGUAGE_LABELS[r]);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
return ctx;
|
|
31
48
|
}
|
|
32
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/claude/gitignore
CHANGED
|
@@ -18,8 +18,6 @@ try {
|
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
19
|
|
|
20
20
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
/** Run a shell command safely, return stdout or fallback */
|
|
23
21
|
function run(cmd, fallback = '') {
|
|
24
22
|
try {
|
|
25
23
|
return execSync(cmd, {
|
|
@@ -88,8 +86,97 @@ try {
|
|
|
88
86
|
return '';
|
|
89
87
|
}
|
|
90
88
|
|
|
89
|
+
// ── CafeKit Update Check ──────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fetch the latest published version of @haposoft/cafekit from the npm registry.
|
|
93
|
+
* Returns a string (e.g. "0.11.2") or null on any failure. Timeout: 3 s.
|
|
94
|
+
*/
|
|
95
|
+
function fetchLatestVersion() {
|
|
96
|
+
try {
|
|
97
|
+
const https = require('https');
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const req = https.get(
|
|
100
|
+
'https://registry.npmjs.org/@haposoft/cafekit/latest',
|
|
101
|
+
{ timeout: 3000, headers: { Accept: 'application/json' } },
|
|
102
|
+
(res) => {
|
|
103
|
+
let body = '';
|
|
104
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
105
|
+
res.on('end', () => {
|
|
106
|
+
try { resolve(JSON.parse(body).version || null); }
|
|
107
|
+
catch { resolve(null); }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
req.on('error', () => resolve(null));
|
|
112
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
113
|
+
});
|
|
114
|
+
} catch { return Promise.resolve(null); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Simple semver compare: returns true when b > a. */
|
|
118
|
+
function isNewer(a, b) {
|
|
119
|
+
const n = (v) => String(v).split('.').map((x) => parseInt(x, 10) || 0);
|
|
120
|
+
const [aN, bN] = [n(a), n(b)];
|
|
121
|
+
for (let i = 0; i < 3; i++) {
|
|
122
|
+
if (bN[i] > aN[i]) return true;
|
|
123
|
+
if (bN[i] < aN[i]) return false;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read the installed CafeKit version from .claude/cafekit.json.
|
|
130
|
+
* Returns null if not found.
|
|
131
|
+
*/
|
|
132
|
+
function getInstalledVersion(cwd) {
|
|
133
|
+
try {
|
|
134
|
+
const p = path.join(cwd, '.claude', 'cafekit.json');
|
|
135
|
+
if (!fs.existsSync(p)) return null;
|
|
136
|
+
const meta = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
137
|
+
return typeof meta.version === 'string' ? meta.version : null;
|
|
138
|
+
} catch { return null; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check for a newer CafeKit version and return an update notice string,
|
|
143
|
+
* or an empty string if already up to date or check fails.
|
|
144
|
+
* Result is cached in .claude/.cafekit-update-cache.json for 12 hours.
|
|
145
|
+
*/
|
|
146
|
+
async function checkCafeKitUpdate(cwd) {
|
|
147
|
+
const installed = getInstalledVersion(cwd);
|
|
148
|
+
if (!installed) return '';
|
|
149
|
+
|
|
150
|
+
const CACHE_TTL = 12 * 60 * 60 * 1000; // 12 hours
|
|
151
|
+
const cachePath = path.join(cwd, '.claude', '.cafekit-update-cache.json');
|
|
152
|
+
|
|
153
|
+
// Return cached result if still fresh.
|
|
154
|
+
try {
|
|
155
|
+
if (fs.existsSync(cachePath)) {
|
|
156
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
157
|
+
if (Date.now() - (cache.ts || 0) < CACHE_TTL) {
|
|
158
|
+
return cache.notice || '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch { /* stale or corrupt — refetch */ }
|
|
162
|
+
|
|
163
|
+
// Fetch from npm registry.
|
|
164
|
+
const latest = await fetchLatestVersion();
|
|
165
|
+
const notice = (latest && isNewer(installed, latest))
|
|
166
|
+
? `\n⚡ CafeKit update available: ${installed} → ${latest}\n Run: npx @haposoft/cafekit\n`
|
|
167
|
+
: '';
|
|
168
|
+
|
|
169
|
+
// Persist cache (fail-open if write fails).
|
|
170
|
+
try {
|
|
171
|
+
fs.writeFileSync(cachePath, JSON.stringify({ ts: Date.now(), notice }), 'utf8');
|
|
172
|
+
} catch { /* fail-open */ }
|
|
173
|
+
|
|
174
|
+
return notice;
|
|
175
|
+
}
|
|
176
|
+
|
|
91
177
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
92
178
|
|
|
179
|
+
(async () => {
|
|
93
180
|
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
94
181
|
const payload = stdin ? JSON.parse(stdin) : {};
|
|
95
182
|
const source = payload.source || 'unknown';
|
|
@@ -97,6 +184,9 @@ try {
|
|
|
97
184
|
const cwd = process.cwd();
|
|
98
185
|
const runtime = readRuntime(cwd);
|
|
99
186
|
|
|
187
|
+
// Check CafeKit update in parallel with project detection (async, fail-open).
|
|
188
|
+
const updateCheckPromise = checkCafeKitUpdate(cwd);
|
|
189
|
+
|
|
100
190
|
// Project detection
|
|
101
191
|
const projectType = runtime.project?.type !== 'auto'
|
|
102
192
|
? (runtime.project?.type || detectProjectType())
|
|
@@ -137,14 +227,15 @@ try {
|
|
|
137
227
|
writeEnv(envFile, 'LOCALE', process.env.LANG || '');
|
|
138
228
|
}
|
|
139
229
|
|
|
140
|
-
// Session summary
|
|
230
|
+
// Session summary + update notice
|
|
231
|
+
const updateNotice = await updateCheckPromise;
|
|
141
232
|
const parts = [];
|
|
142
233
|
if (projectType) parts.push(`Type: ${projectType}`);
|
|
143
234
|
if (packageManager) parts.push(`PM: ${packageManager}`);
|
|
144
235
|
if (framework) parts.push(`Framework: ${framework}`);
|
|
145
236
|
if (gitBranch) parts.push(`Branch: ${gitBranch}`);
|
|
146
237
|
|
|
147
|
-
console.log(`Session ${source}. ${parts.length ? parts.join(' | ') : 'No project info detected.'}`);
|
|
238
|
+
console.log(`Session ${source}. ${parts.length ? parts.join(' | ') : 'No project info detected.'}${updateNotice}`);
|
|
148
239
|
|
|
149
240
|
// Compact warning — context compaction can lose pending approval state
|
|
150
241
|
if (source === 'compact') {
|
|
@@ -155,6 +246,7 @@ try {
|
|
|
155
246
|
}
|
|
156
247
|
|
|
157
248
|
process.exit(0);
|
|
249
|
+
})(); // end async IIFE
|
|
158
250
|
|
|
159
251
|
} catch (e) {
|
|
160
252
|
try {
|