@haposoft/cafekit 0.10.3 → 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/install.js +51 -13
- package/bin/lib/context.js +19 -0
- package/bin/lib/i18n.js +268 -0
- package/bin/lib/manifest.js +14 -1
- package/bin/lib/skill-deps.js +14 -12
- package/bin/lib/version-check.js +91 -0
- package/bin/phases/claude-runtime.js +7 -1
- package/bin/phases/post-install.js +103 -7
- package/bin/phases/select-platform.js +48 -19
- package/bin/phases/skills-setup.js +38 -34
- package/bin/phases/summary.js +37 -21
- package/package.json +1 -1
- package/src/claude/gitignore +1 -0
- package/src/claude/hooks/session.cjs +96 -4
package/bin/install.js
CHANGED
|
@@ -20,7 +20,7 @@ const lock = require('./lib/lock');
|
|
|
20
20
|
const backup = require('./lib/backup');
|
|
21
21
|
const manifestLib = require('./lib/manifest');
|
|
22
22
|
|
|
23
|
-
const { resolvePlatforms } = require('./phases/select-platform');
|
|
23
|
+
const { selectLanguage, resolvePlatforms } = require('./phases/select-platform');
|
|
24
24
|
const { copyPlatformFiles } = require('./phases/copy-payload');
|
|
25
25
|
const {
|
|
26
26
|
copyRoutingFile,
|
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
const { mergeClaudeSettings } = require('./phases/claude-settings');
|
|
33
33
|
const { installOpenCodeRuntime } = require('./phases/opencode-runtime');
|
|
34
34
|
const { writePlatformVersionMetadata } = require('./phases/write-metadata');
|
|
35
|
+
const { checkVersions } = require('./lib/version-check');
|
|
35
36
|
const { ensureGitignore } = require('./phases/root-config');
|
|
36
37
|
const { runPostInstall } = require('./phases/post-install');
|
|
37
38
|
const { setupSkillDeps } = require('./phases/skills-setup');
|
|
@@ -46,7 +47,7 @@ function installPlatform(ctx, platformKey) {
|
|
|
46
47
|
ctx.trackers[platformKey] = manifestLib.createTracker(platform.folder, packageJson.version);
|
|
47
48
|
|
|
48
49
|
const before = { copied: ctx.results.copied, updated: ctx.results.updated, skills: ctx.results.installedSkills };
|
|
49
|
-
ctx.ui.startSpinner(
|
|
50
|
+
ctx.ui.startSpinner(ctx.t('installingPlatform', { name: platform.name }));
|
|
50
51
|
|
|
51
52
|
copyPlatformFiles(ctx, platformKey);
|
|
52
53
|
|
|
@@ -66,13 +67,48 @@ function installPlatform(ctx, platformKey) {
|
|
|
66
67
|
writePlatformVersionMetadata(ctx, platformKey);
|
|
67
68
|
|
|
68
69
|
const wrote = (ctx.results.copied - before.copied) + (ctx.results.updated - before.updated);
|
|
69
|
-
const
|
|
70
|
-
ctx.ui.stopSpinner(
|
|
70
|
+
const skills = ctx.results.installedSkills - before.skills;
|
|
71
|
+
ctx.ui.stopSpinner(ctx.t(ctx.dryRun ? 'platformDryInstalled' : 'platformInstalled', {
|
|
72
|
+
name: platform.name, files: wrote, skills
|
|
73
|
+
}));
|
|
71
74
|
|
|
72
75
|
ctx.results.targets.push(platform.commandsDir);
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
function printHelp() {
|
|
79
|
+
console.log(`CafeKit installer (cafekit) v${packageJson.version}
|
|
80
|
+
|
|
81
|
+
Usage: npx @haposoft/cafekit [options]
|
|
82
|
+
|
|
83
|
+
Installs CafeKit skills, agents, rules and runtime into the current project
|
|
84
|
+
(.claude/ and/or .opencode/). Re-runs are selective: managed files are updated,
|
|
85
|
+
your edits are preserved.
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--dry-run Preview changes; write nothing
|
|
89
|
+
--force-overwrite Overwrite user-modified managed files (backup kept)
|
|
90
|
+
-u, --upgrade, -f, --force Alias of --force-overwrite
|
|
91
|
+
--with-skills-deps Install skill dependencies (Python venv + pip, npm,
|
|
92
|
+
Chromium/Playwright). Otherwise prompted interactively.
|
|
93
|
+
-y, --yes Non-interactive: skip prompts, use defaults (CI)
|
|
94
|
+
-h, --help Show this help
|
|
95
|
+
-v, --version Print version
|
|
96
|
+
|
|
97
|
+
Docs: https://github.com/haposoft/cafekit`);
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
async function main() {
|
|
101
|
+
// Info flags short-circuit before any side effects.
|
|
102
|
+
const argv = process.argv.slice(2);
|
|
103
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
104
|
+
printHelp();
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
108
|
+
console.log(packageJson.version);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
76
112
|
// ── Concurrency guard (before context; plain output) ────
|
|
77
113
|
const got = lock.acquire();
|
|
78
114
|
if (!got.acquired) {
|
|
@@ -87,13 +123,15 @@ async function main() {
|
|
|
87
123
|
try {
|
|
88
124
|
ctx = buildContext(process.argv, `${Date.now()}`);
|
|
89
125
|
ctx.ui.intro(`${ctx.ui.pc.bgCyan(ctx.ui.pc.black(' CafeKit '))}Installer v${packageJson.version} · Multi-platform SDD`);
|
|
90
|
-
if (got.reclaimed) ctx.ui.info(
|
|
126
|
+
if (got.reclaimed) ctx.ui.info(ctx.t('reclaimed'));
|
|
91
127
|
|
|
128
|
+
await selectLanguage(ctx);
|
|
92
129
|
await resolvePlatforms(ctx);
|
|
93
|
-
if (ctx.cancelled) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
if (ctx.cancelled) { lock.release(); process.exit(0); }
|
|
131
|
+
|
|
132
|
+
// Version check: same → exit, downgrade → confirm
|
|
133
|
+
await checkVersions(ctx);
|
|
134
|
+
if (ctx.cancelled) { lock.release(); process.exit(0); }
|
|
97
135
|
|
|
98
136
|
// ── Pre-run snapshot for rollback ─────────────────────
|
|
99
137
|
// Capture platform folders AND the root files the pipeline mutates
|
|
@@ -117,14 +155,14 @@ async function main() {
|
|
|
117
155
|
exitCode = ctx.results.errors > 0 ? 1 : 0;
|
|
118
156
|
} catch (error) {
|
|
119
157
|
const log = ctx && ctx.ui ? ctx.ui : { error: (m) => console.error(m) };
|
|
120
|
-
|
|
158
|
+
const t = ctx ? ctx.t : (k) => k;
|
|
159
|
+
log.error(t('installFailed', { reason: error.message }));
|
|
121
160
|
if (ctx && ctx.backupDir && !ctx.dryRun) {
|
|
122
161
|
try {
|
|
123
162
|
backup.restore(ctx.backupDir);
|
|
124
|
-
log.error('
|
|
163
|
+
log.error(t('rolledBack'));
|
|
125
164
|
} catch (restoreError) {
|
|
126
|
-
log.error(
|
|
127
|
-
log.error(`Manual restore available at: ${ctx.backupDir}`);
|
|
165
|
+
log.error(t('rollbackFailed', { reason: restoreError.message, dir: ctx.backupDir }));
|
|
128
166
|
}
|
|
129
167
|
}
|
|
130
168
|
exitCode = 1;
|
package/bin/lib/context.js
CHANGED
|
@@ -15,6 +15,7 @@ const path = require('path');
|
|
|
15
15
|
const packageJson = require('../../package.json');
|
|
16
16
|
const { getOpenCodeCopyOptions } = require('./opencode-install');
|
|
17
17
|
const { isInteractive, createUI } = require('./ui');
|
|
18
|
+
const { resolveLang, createTranslator } = require('./i18n');
|
|
18
19
|
|
|
19
20
|
const INSTALL_COMMAND = `npx ${packageJson.name}@${packageJson.version}`;
|
|
20
21
|
|
|
@@ -198,6 +199,10 @@ function parseInstallerArgs(argv) {
|
|
|
198
199
|
args.yes = true;
|
|
199
200
|
} else if (arg === '--with-skills-deps') {
|
|
200
201
|
args.withSkillsDeps = true;
|
|
202
|
+
} else if (arg === '--lang') {
|
|
203
|
+
args.lang = argv[++i];
|
|
204
|
+
} else if (arg.startsWith('--lang=')) {
|
|
205
|
+
args.lang = arg.slice('--lang='.length);
|
|
201
206
|
}
|
|
202
207
|
}
|
|
203
208
|
|
|
@@ -240,6 +245,11 @@ function buildContext(argv, runId) {
|
|
|
240
245
|
const options = parseInstallerArgs(argv);
|
|
241
246
|
// Interactive only on a real TTY and not when --yes/CI forces non-interactive.
|
|
242
247
|
const interactive = isInteractive() && !options.yes;
|
|
248
|
+
// Language: --lang wins; otherwise English (an interactive prompt may change it).
|
|
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';
|
|
243
253
|
return {
|
|
244
254
|
argv,
|
|
245
255
|
runId,
|
|
@@ -247,6 +257,15 @@ function buildContext(argv, runId) {
|
|
|
247
257
|
dryRun: options.dryRun,
|
|
248
258
|
interactive,
|
|
249
259
|
ui: createUI(interactive),
|
|
260
|
+
lang, // UI language (one of SUPPORTED)
|
|
261
|
+
locale, // AI response language label (may be freeform, e.g. "Korean")
|
|
262
|
+
t: createTranslator(lang),
|
|
263
|
+
/** Update UI lang + AI locale. rawLabel is the freeform label when user picks "Other". */
|
|
264
|
+
setLang(code, rawLabel) {
|
|
265
|
+
this.lang = resolveLang(code);
|
|
266
|
+
this.locale = rawLabel || LANGUAGE_LABELS[this.lang] || this.lang;
|
|
267
|
+
this.t = createTranslator(this.lang);
|
|
268
|
+
},
|
|
250
269
|
manifest: loadClaudeMigrationManifest(), // migration manifest (skills/agents/runtime lists)
|
|
251
270
|
platforms: [],
|
|
252
271
|
results: createResults(),
|
package/bin/lib/i18n.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer i18n — English / 日本語 / Tiếng Việt.
|
|
3
|
+
*
|
|
4
|
+
* Covers every user-visible string: prompts, spinners, warnings, summary and
|
|
5
|
+
* next steps. High-frequency internal debug logs stay English.
|
|
6
|
+
*
|
|
7
|
+
* Tone: concise, friendly, action-oriented. No ellipsis abuse (…); imperative
|
|
8
|
+
* verbs; consistent capitalisation per language norms.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SUPPORTED = ['en', 'ja', 'vi'];
|
|
12
|
+
|
|
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...' };
|
|
15
|
+
|
|
16
|
+
const MESSAGES = {
|
|
17
|
+
// ─── English ───────────────────────────────────────────────────────────────
|
|
18
|
+
en: {
|
|
19
|
+
// Banner / lock / rollback
|
|
20
|
+
reclaimed: 'Stale install lock reclaimed (previous process was gone).',
|
|
21
|
+
lockHeld: 'Another install is already running (pid {pid}, started {since}).\n If that process has ended, delete .cafekit.lock and try again.',
|
|
22
|
+
installFailed: 'Installation failed: {reason}',
|
|
23
|
+
rolledBack: 'Changes rolled back to the pre-install state.',
|
|
24
|
+
rollbackFailed: 'Rollback failed: {reason}\n Manual restore available at: {dir}',
|
|
25
|
+
dryRunBanner: 'Dry-run — no files will be written.',
|
|
26
|
+
|
|
27
|
+
// Language / platform selection
|
|
28
|
+
selectLanguage: 'Select language',
|
|
29
|
+
selectPlatform: 'Select platform(s) to install',
|
|
30
|
+
allPlatforms: 'All platforms',
|
|
31
|
+
confirmAllDetected: 'Found existing configs: {names}. Install for all?',
|
|
32
|
+
defaultedToClaude: 'No existing config detected — defaulting to Claude Code.',
|
|
33
|
+
cancelled: 'Installation cancelled.',
|
|
34
|
+
|
|
35
|
+
// Mode banner
|
|
36
|
+
modeForce: 'Mode: force-overwrite • user-modified files will be replaced (backup kept)',
|
|
37
|
+
modeDryRun: 'Mode: dry-run • no changes will be written',
|
|
38
|
+
modeInstall: 'Mode: install / update • your customisations are preserved',
|
|
39
|
+
|
|
40
|
+
// Platform install spinner
|
|
41
|
+
installingPlatform: 'Installing {name}',
|
|
42
|
+
platformInstalled: '{name} — {files} file(s), {skills} skill(s)',
|
|
43
|
+
platformDryInstalled: '{name} — {files} file(s), {skills} skill(s) [dry-run]',
|
|
44
|
+
|
|
45
|
+
// Addressing
|
|
46
|
+
addressingQuestion: 'What should the AI call you?',
|
|
47
|
+
addressingPlaceholder: 'e.g. boss, friend — Enter to skip',
|
|
48
|
+
addressingSet: '{name} will address you as "{addr}"',
|
|
49
|
+
addressingInvalid: 'Letters only please — addressing skipped.',
|
|
50
|
+
|
|
51
|
+
// Skills / deps setup
|
|
52
|
+
skillDepsConfirm: 'Install skill dependencies? (Python venv, pip, npm, Chromium — takes a few minutes)',
|
|
53
|
+
skillsSkipped: 'Skill dependencies skipped.\n Run later: npx @haposoft/cafekit --with-skills-deps',
|
|
54
|
+
venvCreating: 'Setting up Python environment',
|
|
55
|
+
venvReady: 'Python environment ready',
|
|
56
|
+
venvFailed: 'Could not create Python environment — check your Python 3 installation.',
|
|
57
|
+
venvNoHost: 'Python 3 not found. Install it, then re-run with --with-skills-deps.',
|
|
58
|
+
pipInstalling: 'Installing Python packages: {skill}',
|
|
59
|
+
pipInstalled: '{skill} — Python packages ready',
|
|
60
|
+
pipFailed: '{skill} — some packages failed. Retry:\n {cmd}',
|
|
61
|
+
npmInstalling: 'Installing Node packages: {skill}',
|
|
62
|
+
npmInstalled: '{skill} — Node packages ready',
|
|
63
|
+
npmFailed: '{skill} — npm install failed. Check the log and retry in: {dir}',
|
|
64
|
+
playwrightInstalling: 'Downloading Playwright browser for {skill}',
|
|
65
|
+
playwrightReady: '{skill} — Playwright browser ready',
|
|
66
|
+
playwrightSkipped: '{skill} — Playwright browser download skipped',
|
|
67
|
+
chromiumInstalling: 'Downloading Chromium for chrome-devtools',
|
|
68
|
+
chromiumReady: 'Chromium ready',
|
|
69
|
+
chromiumSkipped: 'Chromium download skipped — chrome-devtools will prompt on first use.',
|
|
70
|
+
optionalToolsTitle: 'Optional tools to install manually',
|
|
71
|
+
|
|
72
|
+
// Summary stats
|
|
73
|
+
summaryTitle: 'Installation complete',
|
|
74
|
+
summaryDryTitle: 'Dry-run preview',
|
|
75
|
+
labelCopied: 'Installed',
|
|
76
|
+
labelUpdated: 'Updated',
|
|
77
|
+
labelUnchanged: 'Unchanged',
|
|
78
|
+
labelPreserved: 'Preserved',
|
|
79
|
+
labelSkills: 'Skills',
|
|
80
|
+
labelVersion: 'CafeKit Version',
|
|
81
|
+
labelMissing: 'Missing',
|
|
82
|
+
labelErrors: 'Errors',
|
|
83
|
+
yes: 'yes',
|
|
84
|
+
no: 'no',
|
|
85
|
+
preservedNote: '{n} file(s) you edited were kept as-is.\n Re-run with --force-overwrite to reset them (a backup is saved to .cafekit-backup/).',
|
|
86
|
+
|
|
87
|
+
// Next steps
|
|
88
|
+
nextStepsTitle: 'Next steps',
|
|
89
|
+
nsClaude: 'Start coding: /hapo:specs <feature>',
|
|
90
|
+
nsOpencode: 'Start coding: ask the agent to begin a new feature or brainstorm',
|
|
91
|
+
nsKeys: 'Some skills need API keys: {skills}\n Copy <skill>/.env.example → .env and fill in the values',
|
|
92
|
+
nsForce: 'To force-refresh managed files: npx @haposoft/cafekit --force-overwrite',
|
|
93
|
+
dryRunOnly: 'Dry-run complete — no files were changed.\n Re-run without --dry-run to apply.',
|
|
94
|
+
outroDone: 'Done! docs: https://github.com/haposoft/cafekit',
|
|
95
|
+
|
|
96
|
+
// Version check
|
|
97
|
+
versionUpToDate: 'Already up to date ({v}). Use --force-overwrite to reinstall.',
|
|
98
|
+
versionDowngrade: 'Downgrading {from} → {to}. Features may be removed. Continue?'
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// ─── 日本語 ─────────────────────────────────────────────────────────────────
|
|
102
|
+
ja: {
|
|
103
|
+
reclaimed: '前のプロセスが終了したため、ロックを解除しました。',
|
|
104
|
+
lockHeld: '別のインストールが実行中です(pid {pid}、開始: {since})。\n そのプロセスが終了している場合は .cafekit.lock を削除して再試行してください。',
|
|
105
|
+
installFailed: 'インストールに失敗しました: {reason}',
|
|
106
|
+
rolledBack: 'インストール前の状態に戻しました。',
|
|
107
|
+
rollbackFailed: 'ロールバックに失敗しました: {reason}\n 手動復元可能: {dir}',
|
|
108
|
+
dryRunBanner: 'ドライラン — ファイルへの書き込みは行いません。',
|
|
109
|
+
|
|
110
|
+
selectLanguage: '言語を選択してください',
|
|
111
|
+
selectPlatform: 'インストールするプラットフォームを選択',
|
|
112
|
+
allPlatforms: 'すべてのプラットフォーム',
|
|
113
|
+
confirmAllDetected: '既存の設定が見つかりました: {names}。すべてにインストールしますか?',
|
|
114
|
+
defaultedToClaude: '既存の設定が見つかりません — Claude Code をデフォルトで使用します。',
|
|
115
|
+
cancelled: 'インストールをキャンセルしました。',
|
|
116
|
+
|
|
117
|
+
modeForce: 'モード: 強制上書き • 編集済みファイルを置換します(バックアップあり)',
|
|
118
|
+
modeDryRun: 'モード: ドライラン • ファイルへの変更はありません',
|
|
119
|
+
modeInstall: 'モード: インストール / 更新 • カスタマイズは保持されます',
|
|
120
|
+
|
|
121
|
+
installingPlatform: '{name} をインストール中',
|
|
122
|
+
platformInstalled: '{name} — {files} ファイル、{skills} スキル',
|
|
123
|
+
platformDryInstalled: '{name} — {files} ファイル、{skills} スキル [ドライラン]',
|
|
124
|
+
|
|
125
|
+
addressingQuestion: 'AI にどう呼ばれたいですか?',
|
|
126
|
+
addressingPlaceholder: '例: さん、くん — Enter でスキップ',
|
|
127
|
+
addressingSet: '{name} はあなたを「{addr}」とお呼びします',
|
|
128
|
+
addressingInvalid: '文字のみ入力できます — 呼称の設定をスキップしました。',
|
|
129
|
+
|
|
130
|
+
skillDepsConfirm: 'スキルの依存関係をインストールしますか?(Python venv、pip、npm、Chromium — 数分かかります)',
|
|
131
|
+
skillsSkipped: 'スキルの依存関係をスキップしました。\n 後で実行: npx @haposoft/cafekit --with-skills-deps',
|
|
132
|
+
venvCreating: 'Python 環境を準備しています',
|
|
133
|
+
venvReady: 'Python 環境の準備ができました',
|
|
134
|
+
venvFailed: 'Python 環境を作成できませんでした — Python 3 のインストールを確認してください。',
|
|
135
|
+
venvNoHost: 'Python 3 が見つかりません。インストール後、--with-skills-deps で再実行してください。',
|
|
136
|
+
pipInstalling: 'Python パッケージをインストール中: {skill}',
|
|
137
|
+
pipInstalled: '{skill} — Python パッケージの準備ができました',
|
|
138
|
+
pipFailed: '{skill} — 一部のパッケージが失敗しました。再試行:\n {cmd}',
|
|
139
|
+
npmInstalling: 'Node パッケージをインストール中: {skill}',
|
|
140
|
+
npmInstalled: '{skill} — Node パッケージの準備ができました',
|
|
141
|
+
npmFailed: '{skill} — npm install に失敗しました。{dir} で手動再試行してください。',
|
|
142
|
+
playwrightInstalling: '{skill} 用の Playwright ブラウザをダウンロード中',
|
|
143
|
+
playwrightReady: '{skill} — Playwright ブラウザの準備ができました',
|
|
144
|
+
playwrightSkipped: '{skill} — Playwright ブラウザのダウンロードをスキップしました',
|
|
145
|
+
chromiumInstalling: 'chrome-devtools 用 Chromium をダウンロード中',
|
|
146
|
+
chromiumReady: 'Chromium の準備ができました',
|
|
147
|
+
chromiumSkipped: 'Chromium のダウンロードをスキップ — 初回使用時に chrome-devtools が自動取得します。',
|
|
148
|
+
optionalToolsTitle: '手動でインストールが必要なオプションツール',
|
|
149
|
+
|
|
150
|
+
summaryTitle: 'インストール完了',
|
|
151
|
+
summaryDryTitle: 'ドライラン プレビュー',
|
|
152
|
+
labelCopied: 'インストール',
|
|
153
|
+
labelUpdated: '更新',
|
|
154
|
+
labelUnchanged: '変更なし',
|
|
155
|
+
labelPreserved: '保持',
|
|
156
|
+
labelSkills: 'スキル',
|
|
157
|
+
labelVersion: 'CafeKit Version',
|
|
158
|
+
labelMissing: '不足',
|
|
159
|
+
labelErrors: 'エラー',
|
|
160
|
+
yes: 'はい',
|
|
161
|
+
no: 'いいえ',
|
|
162
|
+
preservedNote: '編集済みファイル {n} 件はそのままにしました。\n --force-overwrite で再実行するとリセットできます(.cafekit-backup/ にバックアップ保存)。',
|
|
163
|
+
|
|
164
|
+
nextStepsTitle: '次のステップ',
|
|
165
|
+
nsClaude: 'コーディング開始: /hapo:specs <機能名>',
|
|
166
|
+
nsOpencode: 'コーディング開始: エージェントに新機能の開始またはブレストを依頼',
|
|
167
|
+
nsKeys: '一部のスキルに API キーが必要です: {skills}\n <skill>/.env.example を .env にコピーし、値を入力してください',
|
|
168
|
+
nsForce: '管理ファイルを強制更新: npx @haposoft/cafekit --force-overwrite',
|
|
169
|
+
dryRunOnly: 'ドライラン完了 — ファイルへの変更はありませんでした。\n 変更を適用するには --dry-run なしで再実行してください。',
|
|
170
|
+
outroDone: '完了! ドキュメント: https://github.com/haposoft/cafekit',
|
|
171
|
+
|
|
172
|
+
versionUpToDate: '最新バージョンです({v})。再インストールするには --force-overwrite を使用してください。',
|
|
173
|
+
versionDowngrade: '{from} → {to} へのダウングレードです。機能が削除される可能性があります。続けますか?'
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// ─── Tiếng Việt ─────────────────────────────────────────────────────────────
|
|
177
|
+
vi: {
|
|
178
|
+
reclaimed: 'Khóa cài đặt cũ đã được thu hồi (tiến trình trước đã kết thúc).',
|
|
179
|
+
lockHeld: 'Có một phiên cài đặt khác đang chạy (pid {pid}, bắt đầu: {since}).\n Nếu tiến trình đó đã kết thúc, hãy xóa .cafekit.lock rồi thử lại.',
|
|
180
|
+
installFailed: 'Cài đặt thất bại: {reason}',
|
|
181
|
+
rolledBack: 'Đã hoàn tác về trạng thái trước khi cài đặt.',
|
|
182
|
+
rollbackFailed: 'Hoàn tác thất bại: {reason}\n Bản sao lưu thủ công: {dir}',
|
|
183
|
+
dryRunBanner: 'Dry-run — sẽ không có tệp nào được ghi.',
|
|
184
|
+
|
|
185
|
+
selectLanguage: 'Chọn ngôn ngữ',
|
|
186
|
+
selectPlatform: 'Chọn (các) nền tảng cần cài',
|
|
187
|
+
allPlatforms: 'Tất cả nền tảng',
|
|
188
|
+
confirmAllDetected: 'Phát hiện cấu hình sẵn: {names}. Cài cho tất cả?',
|
|
189
|
+
defaultedToClaude: 'Không tìm thấy cấu hình sẵn — mặc định dùng Claude Code.',
|
|
190
|
+
cancelled: 'Đã hủy cài đặt.',
|
|
191
|
+
|
|
192
|
+
modeForce: 'Chế độ: ghi đè • file bạn đã sửa sẽ bị thay (có bản sao lưu)',
|
|
193
|
+
modeDryRun: 'Chế độ: dry-run • sẽ không thay đổi bất kỳ tệp nào',
|
|
194
|
+
modeInstall: 'Chế độ: cài / cập nhật • nội dung bạn tuỳ chỉnh được giữ nguyên',
|
|
195
|
+
|
|
196
|
+
installingPlatform: 'Đang cài {name}',
|
|
197
|
+
platformInstalled: '{name} — {files} tệp, {skills} skill',
|
|
198
|
+
platformDryInstalled: '{name} — {files} tệp, {skills} skill [dry-run]',
|
|
199
|
+
|
|
200
|
+
addressingQuestion: 'Bạn muốn AI gọi bạn là gì?',
|
|
201
|
+
addressingPlaceholder: 'vd: sếp, đại ca — Enter để bỏ qua',
|
|
202
|
+
addressingSet: '{name} sẽ gọi bạn là "{addr}"',
|
|
203
|
+
addressingInvalid: 'Chỉ nhập chữ cái — bỏ qua thiết lập xưng hô.',
|
|
204
|
+
|
|
205
|
+
skillDepsConfirm: 'Cài dependencies cho skill ngay? (Python venv, pip, npm, Chromium — mất vài phút)',
|
|
206
|
+
skillsSkipped: 'Đã bỏ qua dependencies skill.\n Cài sau bằng lệnh: npx @haposoft/cafekit --with-skills-deps',
|
|
207
|
+
venvCreating: 'Đang thiết lập môi trường Python',
|
|
208
|
+
venvReady: 'Môi trường Python đã sẵn sàng',
|
|
209
|
+
venvFailed: 'Không thể tạo môi trường Python — hãy kiểm tra cài đặt Python 3.',
|
|
210
|
+
venvNoHost: 'Không tìm thấy Python 3. Hãy cài Python 3 rồi chạy lại với --with-skills-deps.',
|
|
211
|
+
pipInstalling: 'Đang cài gói Python: {skill}',
|
|
212
|
+
pipInstalled: '{skill} — gói Python đã sẵn sàng',
|
|
213
|
+
pipFailed: '{skill} — một số gói cài thất bại. Chạy lại thủ công:\n {cmd}',
|
|
214
|
+
npmInstalling: 'Đang cài gói Node: {skill}',
|
|
215
|
+
npmInstalled: '{skill} — gói Node đã sẵn sàng',
|
|
216
|
+
npmFailed: '{skill} — npm install thất bại. Hãy thử lại trong: {dir}',
|
|
217
|
+
playwrightInstalling: 'Đang tải Playwright browser cho {skill}',
|
|
218
|
+
playwrightReady: '{skill} — Playwright browser đã sẵn sàng',
|
|
219
|
+
playwrightSkipped: '{skill} — đã bỏ qua tải Playwright browser',
|
|
220
|
+
chromiumInstalling: 'Đang tải Chromium cho chrome-devtools',
|
|
221
|
+
chromiumReady: 'Chromium đã sẵn sàng',
|
|
222
|
+
chromiumSkipped: 'Bỏ qua tải Chromium — chrome-devtools sẽ tự tải khi dùng lần đầu.',
|
|
223
|
+
optionalToolsTitle: 'Công cụ tuỳ chọn cần cài thủ công',
|
|
224
|
+
|
|
225
|
+
summaryTitle: 'Cài đặt hoàn tất',
|
|
226
|
+
summaryDryTitle: 'Xem trước dry-run',
|
|
227
|
+
labelCopied: 'Đã cài',
|
|
228
|
+
labelUpdated: 'Đã cập nhật',
|
|
229
|
+
labelUnchanged: 'Không đổi',
|
|
230
|
+
labelPreserved: 'Đã giữ',
|
|
231
|
+
labelSkills: 'Skill',
|
|
232
|
+
labelVersion: 'CafeKit Version',
|
|
233
|
+
labelMissing: 'Thiếu',
|
|
234
|
+
labelErrors: 'Lỗi',
|
|
235
|
+
yes: 'có',
|
|
236
|
+
no: 'không',
|
|
237
|
+
preservedNote: '{n} tệp bạn đã chỉnh sửa được giữ nguyên.\n Chạy lại với --force-overwrite để cài đè (bản sao lưu ở .cafekit-backup/).',
|
|
238
|
+
|
|
239
|
+
nextStepsTitle: 'Bước tiếp theo',
|
|
240
|
+
nsClaude: 'Bắt đầu code: /hapo:specs <mô-tả-tính-năng>',
|
|
241
|
+
nsOpencode: 'Bắt đầu code: yêu cầu agent khởi động tính năng mới hoặc brainstorm',
|
|
242
|
+
nsKeys: 'Một số skill cần API key: {skills}\n Sao chép <skill>/.env.example → .env rồi điền giá trị',
|
|
243
|
+
nsForce: 'Làm mới file được quản lý: npx @haposoft/cafekit --force-overwrite',
|
|
244
|
+
dryRunOnly: 'Dry-run hoàn tất — không có tệp nào thay đổi.\n Chạy lại không có --dry-run để áp dụng.',
|
|
245
|
+
outroDone: 'Xong! Tài liệu: https://github.com/haposoft/cafekit',
|
|
246
|
+
|
|
247
|
+
versionUpToDate: 'Đã là phiên bản mới nhất ({v}). Dùng --force-overwrite để cài lại.',
|
|
248
|
+
versionDowngrade: 'Đang hạ cấp {from} → {to}. Một số tính năng có thể bị xoá. Tiếp tục?'
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** Normalize a language code to a supported one; unknown → 'ja'. */
|
|
253
|
+
function resolveLang(code) {
|
|
254
|
+
if (!code) return 'en';
|
|
255
|
+
const lc = String(code).toLowerCase().slice(0, 2);
|
|
256
|
+
return SUPPORTED.includes(lc) ? lc : 'ja';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Build a translator. Falls back to English per-key, then the key itself. */
|
|
260
|
+
function createTranslator(lang) {
|
|
261
|
+
const resolved = resolveLang(lang);
|
|
262
|
+
return function t(key, vars = {}) {
|
|
263
|
+
const template = (MESSAGES[resolved] && MESSAGES[resolved][key]) || MESSAGES.en[key] || key;
|
|
264
|
+
return template.replace(/\{(\w+)\}/g, (_, name) => (vars[name] !== undefined ? String(vars[name]) : `{${name}}`));
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { SUPPORTED, LANGUAGE_LABELS, OTHER_LABEL, MESSAGES, resolveLang, createTranslator };
|
package/bin/lib/manifest.js
CHANGED
|
@@ -95,6 +95,16 @@ function createTracker(platformFolder, version) {
|
|
|
95
95
|
return Object.keys(files).length;
|
|
96
96
|
},
|
|
97
97
|
|
|
98
|
+
/** Keys explicitly pruned this run — must not survive the merge into the prior manifest. */
|
|
99
|
+
_pruned: new Set(),
|
|
100
|
+
|
|
101
|
+
/** Remove a relPath from this run's tracked files (e.g. after obsolete removal). */
|
|
102
|
+
prune(rel) {
|
|
103
|
+
const key = rel.replace(/\\/g, '/');
|
|
104
|
+
delete files[key];
|
|
105
|
+
this._pruned.add(key);
|
|
106
|
+
},
|
|
107
|
+
|
|
98
108
|
/** Recorded hash for a relPath written earlier this run, or null. */
|
|
99
109
|
recorded(rel) {
|
|
100
110
|
const key = rel.replace(/\\/g, '/');
|
|
@@ -107,7 +117,10 @@ function createTracker(platformFolder, version) {
|
|
|
107
117
|
*/
|
|
108
118
|
write() {
|
|
109
119
|
const prior = read(platformFolder);
|
|
110
|
-
|
|
120
|
+
// Start from prior, delete pruned entries, then overlay this run's files.
|
|
121
|
+
const merged = { ...prior.files };
|
|
122
|
+
for (const key of this._pruned) delete merged[key];
|
|
123
|
+
Object.assign(merged, files);
|
|
111
124
|
const out = {
|
|
112
125
|
schemaVersion: SCHEMA_VERSION,
|
|
113
126
|
version,
|
package/bin/lib/skill-deps.js
CHANGED
|
@@ -69,16 +69,17 @@ function pipUpgrade(skillsDir) {
|
|
|
69
69
|
return run(venvPython(skillsDir), ['-m', 'pip', 'install', '--upgrade', 'pip', '--prefer-binary']).ok;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** Install
|
|
73
|
-
function pipInstall(skillsDir, requirementsPath) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
/** Install or upgrade pip requirements in the venv. */
|
|
73
|
+
function pipInstall(skillsDir, requirementsPath, upgrade = false) {
|
|
74
|
+
const args = ['-m', 'pip', 'install', '-r', requirementsPath, '--prefer-binary'];
|
|
75
|
+
if (upgrade) args.push('--upgrade');
|
|
76
|
+
return run(venvPython(skillsDir), args).ok;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** `npm install` in a skill scripts dir.
|
|
80
|
-
function npmInstall(dir) {
|
|
81
|
-
|
|
79
|
+
/** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. */
|
|
80
|
+
function npmInstall(dir, upgrade = false) {
|
|
81
|
+
const args = upgrade ? ['update', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund'];
|
|
82
|
+
return run('npm', args, { cwd: dir }).ok;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/**
|
|
@@ -115,6 +116,7 @@ function installPlaywrightBrowser(scriptsDir) {
|
|
|
115
116
|
/**
|
|
116
117
|
* List installed skills that ship a runnable scripts/requirements.txt
|
|
117
118
|
* (excludes the tests/ requirements). Returns [{ skill, file }].
|
|
119
|
+
* Always returned regardless of whether venv already exists (caller decides upgrade vs fresh).
|
|
118
120
|
*/
|
|
119
121
|
function collectRequirements(skillsDir) {
|
|
120
122
|
const out = [];
|
|
@@ -127,16 +129,16 @@ function collectRequirements(skillsDir) {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
/**
|
|
130
|
-
* List installed skills with a scripts/package.json
|
|
131
|
-
*
|
|
132
|
+
* List installed skills with a scripts/package.json.
|
|
133
|
+
* Returns [{ skill, dir, hasNodeModules }] — always, so caller can upgrade existing installs.
|
|
132
134
|
*/
|
|
133
135
|
function collectSkillPackages(skillsDir) {
|
|
134
136
|
const out = [];
|
|
135
137
|
if (!fs.existsSync(skillsDir)) return out;
|
|
136
138
|
for (const skill of fs.readdirSync(skillsDir)) {
|
|
137
139
|
const dir = path.join(skillsDir, skill, 'scripts');
|
|
138
|
-
if (fs.existsSync(path.join(dir, 'package.json'))
|
|
139
|
-
out.push({ skill, dir });
|
|
140
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
141
|
+
out.push({ skill, dir, hasNodeModules: fs.existsSync(path.join(dir, 'node_modules')) });
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
return out;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version check before install.
|
|
3
|
+
*
|
|
4
|
+
* Reads each platform's cafekit.json to compare the installed version with the
|
|
5
|
+
* incoming one and decides whether to proceed:
|
|
6
|
+
*
|
|
7
|
+
* same → info + exit 0 (unless --force-overwrite)
|
|
8
|
+
* newer → normal update
|
|
9
|
+
* older → downgrade warning + confirm (interactive) or abort (non-interactive)
|
|
10
|
+
* missing → fresh install, no check needed
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { PLATFORMS, packageJson } = require('./context');
|
|
16
|
+
|
|
17
|
+
/** Simple semver-ish comparison. Returns -1 | 0 | 1. */
|
|
18
|
+
function cmpVersion(a, b) {
|
|
19
|
+
const toNum = (v) => String(v).split('.').map((n) => parseInt(n, 10) || 0);
|
|
20
|
+
const [aN, bN] = [toNum(a), toNum(b)];
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
const diff = (aN[i] || 0) - (bN[i] || 0);
|
|
23
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the currently installed version for a platform.
|
|
30
|
+
* Returns null if cafekit.json doesn't exist yet (fresh install).
|
|
31
|
+
*/
|
|
32
|
+
function getInstalledVersion(platformKey) {
|
|
33
|
+
const p = path.join(PLATFORMS[platformKey].folder, 'cafekit.json');
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(p)) return null;
|
|
36
|
+
const meta = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
37
|
+
return typeof meta.version === 'string' ? meta.version : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check versions for all selected platforms and decide whether to proceed.
|
|
45
|
+
* Sets ctx.cancelled on same-version (without --force) or unconfirmed downgrade.
|
|
46
|
+
*/
|
|
47
|
+
async function checkVersions(ctx) {
|
|
48
|
+
if (ctx.dryRun) return ctx;
|
|
49
|
+
|
|
50
|
+
const incoming = packageJson.version;
|
|
51
|
+
|
|
52
|
+
for (const key of ctx.platforms) {
|
|
53
|
+
const installed = getInstalledVersion(key);
|
|
54
|
+
if (!installed) continue; // fresh install — no check needed
|
|
55
|
+
|
|
56
|
+
const cmp = cmpVersion(installed, incoming);
|
|
57
|
+
|
|
58
|
+
// ── Same version ─────────────────────────────────────────────────────────
|
|
59
|
+
if (cmp === 0) {
|
|
60
|
+
if (ctx.options.forceOverwrite) continue; // --force-overwrite overrides
|
|
61
|
+
ctx.ui.info(ctx.t ? ctx.t('versionUpToDate', { v: installed }) : `Already up to date (${installed}). Use --force-overwrite to reinstall.`);
|
|
62
|
+
ctx.cancelled = true;
|
|
63
|
+
return ctx;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Downgrade ─────────────────────────────────────────────────────────────
|
|
67
|
+
if (cmp > 0) {
|
|
68
|
+
const msg = ctx.t
|
|
69
|
+
? ctx.t('versionDowngrade', { from: installed, to: incoming })
|
|
70
|
+
: `Downgrading ${installed} → ${incoming}. This may remove features. Continue?`;
|
|
71
|
+
|
|
72
|
+
if (!ctx.interactive) {
|
|
73
|
+
ctx.ui.warn ? ctx.ui.warn(`Downgrade detected (${installed} → ${incoming}). Aborted in non-interactive mode. Use --force-overwrite to proceed.`) : console.warn(msg);
|
|
74
|
+
ctx.cancelled = true;
|
|
75
|
+
return ctx;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const yes = await ctx.ui.confirm({ message: msg, initialValue: false }, false);
|
|
79
|
+
if (!yes || ctx.ui.isCancel(yes)) {
|
|
80
|
+
ctx.cancelled = true;
|
|
81
|
+
return ctx;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// cmp < 0 → upgrade, proceed normally
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ctx;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { getInstalledVersion, cmpVersion, checkVersions };
|
|
@@ -78,7 +78,13 @@ function removeObsoleteClaudeRuntimeFiles(ctx, platformKey) {
|
|
|
78
78
|
const targetPath = path.join(targetBase, relPath);
|
|
79
79
|
if (!fs.existsSync(targetPath)) return;
|
|
80
80
|
|
|
81
|
-
if (!ctx.dryRun)
|
|
81
|
+
if (!ctx.dryRun) {
|
|
82
|
+
fs.rmSync(targetPath, { force: true });
|
|
83
|
+
// Remove the entry from the manifest so it doesn't linger as a zombie.
|
|
84
|
+
if (ctx.trackers && ctx.trackers.claude) {
|
|
85
|
+
ctx.trackers.claude.prune(relPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
82
88
|
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete runtime: ${relPath}`);
|
|
83
89
|
ctx.results.updated++;
|
|
84
90
|
});
|
|
@@ -13,8 +13,92 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { PLATFORMS } = require('../lib/context');
|
|
17
|
+
const { LANGUAGE_LABELS } = require('../lib/i18n');
|
|
16
18
|
const { setupOpenCodeModel } = require('../lib/opencode-install');
|
|
17
19
|
|
|
20
|
+
/** Write `"language"` field into .claude/settings.json so it's visible at a glance. */
|
|
21
|
+
function patchSettingsLanguage(ctx) {
|
|
22
|
+
if (!ctx.platforms.includes('claude')) return;
|
|
23
|
+
const settingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
|
24
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
25
|
+
|
|
26
|
+
let settings;
|
|
27
|
+
try {
|
|
28
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
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;
|
|
36
|
+
settings.language = label;
|
|
37
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Patch the "## Language Consistency" section in CLAUDE.md with the chosen
|
|
42
|
+
* language so every AI session knows which language to respond in — without
|
|
43
|
+
* relying on hooks reading runtime.json.
|
|
44
|
+
*
|
|
45
|
+
* The section is identified by a stable marker comment so subsequent installs
|
|
46
|
+
* can update it idempotently.
|
|
47
|
+
*/
|
|
48
|
+
function patchLanguageSection(ctx) {
|
|
49
|
+
if (!ctx.platforms.includes('claude')) return;
|
|
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;
|
|
53
|
+
|
|
54
|
+
const claudeMdFile = path.join(process.cwd(), 'CLAUDE.md');
|
|
55
|
+
if (!fs.existsSync(claudeMdFile)) return;
|
|
56
|
+
|
|
57
|
+
const LANG_LABEL = { vi: 'Vietnamese', ja: 'Japanese', en: 'English' };
|
|
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;
|
|
60
|
+
|
|
61
|
+
const newSection = `## Language Consistency <!-- cafekit:lang -->
|
|
62
|
+
|
|
63
|
+
Always respond in **${label}**. Technical terms, code identifiers, and file paths may remain in English, but all explanations, comments directed at the user, and structured output (specs, docs, reports) must be in ${label}.
|
|
64
|
+
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
let content = fs.readFileSync(claudeMdFile, 'utf8');
|
|
68
|
+
// Replace if marker present, else replace the generic section.
|
|
69
|
+
const markerRe = /## Language Consistency <!-- cafekit:lang -->[\s\S]*?(?=\n##|\n*$)/;
|
|
70
|
+
const genericRe = /## Language Consistency\n[\s\S]*?(?=\n##|\n*$)/;
|
|
71
|
+
|
|
72
|
+
if (markerRe.test(content)) {
|
|
73
|
+
content = content.replace(markerRe, newSection);
|
|
74
|
+
} else if (genericRe.test(content)) {
|
|
75
|
+
content = content.replace(genericRe, newSection);
|
|
76
|
+
} else {
|
|
77
|
+
content += `\n${newSection}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(claudeMdFile, content, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
function patchRuntimeLocale(ctx) {
|
|
83
|
+
for (const key of ctx.platforms) {
|
|
84
|
+
const rtPath = path.join(PLATFORMS[key].folder, 'runtime.json');
|
|
85
|
+
if (!fs.existsSync(rtPath)) continue;
|
|
86
|
+
let data;
|
|
87
|
+
try {
|
|
88
|
+
data = JSON.parse(fs.readFileSync(rtPath, 'utf8'));
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
data.locale = data.locale || {};
|
|
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;
|
|
97
|
+
fs.writeFileSync(rtPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
98
|
+
if (ctx.trackers[key]) ctx.trackers[key].record(rtPath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
18
102
|
/** Human assistant name for the active platform(s). */
|
|
19
103
|
function assistantName(ctx) {
|
|
20
104
|
return ctx.platforms.includes('claude') ? 'Claude Code' : 'OpenCode';
|
|
@@ -40,22 +124,23 @@ ${name} always addresses the user as "${userAddress}" throughout the conversatio
|
|
|
40
124
|
: `${content.endsWith('\n') ? content : `${content}\n`}\n${addressingSection}\n`;
|
|
41
125
|
|
|
42
126
|
fs.writeFileSync(claudeMdFile, content, 'utf8');
|
|
43
|
-
ctx.ui.success(
|
|
127
|
+
ctx.ui.success(ctx.t('addressingSet', { name, addr: userAddress }));
|
|
44
128
|
}
|
|
45
129
|
|
|
46
130
|
async function setupAddressing(ctx) {
|
|
47
131
|
if (!ctx.platforms.includes('claude')) return;
|
|
48
132
|
|
|
49
133
|
const answer = await ctx.ui.text(
|
|
50
|
-
{ message: '
|
|
134
|
+
{ message: ctx.t('addressingQuestion'), placeholder: ctx.t('addressingPlaceholder') },
|
|
51
135
|
''
|
|
52
136
|
);
|
|
53
137
|
if (ctx.ui.isCancel(answer)) return;
|
|
54
138
|
const userAddress = (answer || '').trim();
|
|
55
139
|
if (!userAddress) return;
|
|
56
140
|
|
|
57
|
-
|
|
58
|
-
|
|
141
|
+
// Allow letters from any script (incl. Japanese) + spaces; reject digits/symbols.
|
|
142
|
+
if (/[^\p{L}\s]/u.test(userAddress)) {
|
|
143
|
+
ctx.ui.warn(ctx.t('addressingInvalid'));
|
|
59
144
|
return;
|
|
60
145
|
}
|
|
61
146
|
configureAddressing(ctx, userAddress);
|
|
@@ -77,11 +162,22 @@ async function runPostInstall(ctx) {
|
|
|
77
162
|
|
|
78
163
|
await setupAddressing(ctx);
|
|
79
164
|
|
|
80
|
-
//
|
|
81
|
-
|
|
165
|
+
// Patch Language Consistency section in CLAUDE.md so AI responds in the chosen language.
|
|
166
|
+
patchLanguageSection(ctx);
|
|
167
|
+
|
|
168
|
+
// Persist chosen language into each platform's runtime.json (records in tracker).
|
|
169
|
+
patchRuntimeLocale(ctx);
|
|
170
|
+
|
|
171
|
+
// Write "language" field into .claude/settings.json for visibility.
|
|
172
|
+
patchSettingsLanguage(ctx);
|
|
173
|
+
|
|
174
|
+
// Re-record post-write baselines so installer-managed files stay "pristine",
|
|
175
|
+
// then flush each touched platform tracker.
|
|
82
176
|
if (ctx.platforms.includes('claude') && ctx.trackers.claude && fs.existsSync('CLAUDE.md')) {
|
|
83
177
|
ctx.trackers.claude.record('CLAUDE.md');
|
|
84
|
-
|
|
178
|
+
}
|
|
179
|
+
for (const key of ctx.platforms) {
|
|
180
|
+
if (ctx.trackers[key]) ctx.trackers[key].write();
|
|
85
181
|
}
|
|
86
182
|
return ctx;
|
|
87
183
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phase:
|
|
2
|
+
* Phase: language + platform selection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Sets ctx.platforms, or ctx.cancelled if the user backs out.
|
|
4
|
+
* Language is chosen first (interactive) so the rest of the installer renders in
|
|
5
|
+
* it; non-interactive uses --lang or English. Then platforms are auto-detected
|
|
6
|
+
* or prompted. Sets ctx.platforms, or ctx.cancelled if the user backs out.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
const {
|
|
@@ -13,6 +12,40 @@ const {
|
|
|
13
12
|
getPlatformKeys,
|
|
14
13
|
warnLegacyClaudeFolder
|
|
15
14
|
} = require('../lib/context');
|
|
15
|
+
const { SUPPORTED, LANGUAGE_LABELS, OTHER_LABEL } = require('../lib/i18n');
|
|
16
|
+
|
|
17
|
+
const OTHER = '__other__';
|
|
18
|
+
|
|
19
|
+
/** First step: pick the installer/UI language (interactive only). */
|
|
20
|
+
async function selectLanguage(ctx) {
|
|
21
|
+
if (!ctx.interactive || ctx.options.lang) return ctx;
|
|
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
|
+
|
|
28
|
+
const r = await ctx.ui.select(
|
|
29
|
+
{ message: 'Select language · 言語を選択 · Chọn ngôn ngữ', options },
|
|
30
|
+
ctx.lang
|
|
31
|
+
);
|
|
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
|
+
|
|
47
|
+
return ctx;
|
|
48
|
+
}
|
|
16
49
|
|
|
17
50
|
/** clack select: which platform(s) to install for. */
|
|
18
51
|
async function promptPlatformSelection(ctx) {
|
|
@@ -22,12 +55,9 @@ async function promptPlatformSelection(ctx) {
|
|
|
22
55
|
label: PLATFORMS[key].name,
|
|
23
56
|
hint: `${PLATFORMS[key].folder}/`
|
|
24
57
|
})),
|
|
25
|
-
{ value: getPlatformKeys(), label: '
|
|
58
|
+
{ value: getPlatformKeys(), label: ctx.t('allPlatforms') }
|
|
26
59
|
];
|
|
27
|
-
const r = await ctx.ui.select(
|
|
28
|
-
{ message: 'Select which platform(s) to install for', options },
|
|
29
|
-
[]
|
|
30
|
-
);
|
|
60
|
+
const r = await ctx.ui.select({ message: ctx.t('selectPlatform'), options }, []);
|
|
31
61
|
if (ctx.ui.isCancel(r)) return [];
|
|
32
62
|
return r;
|
|
33
63
|
}
|
|
@@ -36,7 +66,7 @@ async function promptPlatformSelection(ctx) {
|
|
|
36
66
|
async function confirmAllDetected(ctx, detected) {
|
|
37
67
|
const names = detected.map((key) => PLATFORMS[key].name).join(', ');
|
|
38
68
|
const r = await ctx.ui.confirm(
|
|
39
|
-
{ message:
|
|
69
|
+
{ message: ctx.t('confirmAllDetected', { names }), initialValue: true },
|
|
40
70
|
true
|
|
41
71
|
);
|
|
42
72
|
if (ctx.ui.isCancel(r)) return false;
|
|
@@ -53,12 +83,11 @@ async function resolvePlatforms(ctx) {
|
|
|
53
83
|
if (ctx.interactive) {
|
|
54
84
|
platforms = await promptPlatformSelection(ctx);
|
|
55
85
|
if (platforms.length === 0) {
|
|
56
|
-
ctx.ui.outro('
|
|
86
|
+
ctx.ui.outro(ctx.t('cancelled'));
|
|
57
87
|
ctx.cancelled = true;
|
|
58
88
|
return ctx;
|
|
59
89
|
}
|
|
60
90
|
} else {
|
|
61
|
-
// Non-interactive with nothing detected → sensible default, never block.
|
|
62
91
|
platforms = ['claude'];
|
|
63
92
|
ctx.ui.info('No platform detected; defaulting to Claude Code (.claude/).');
|
|
64
93
|
}
|
|
@@ -67,7 +96,7 @@ async function resolvePlatforms(ctx) {
|
|
|
67
96
|
if (!proceed) {
|
|
68
97
|
platforms = await promptPlatformSelection(ctx);
|
|
69
98
|
if (platforms.length === 0) {
|
|
70
|
-
ctx.ui.outro('
|
|
99
|
+
ctx.ui.outro(ctx.t('cancelled'));
|
|
71
100
|
ctx.cancelled = true;
|
|
72
101
|
return ctx;
|
|
73
102
|
}
|
|
@@ -77,18 +106,18 @@ async function resolvePlatforms(ctx) {
|
|
|
77
106
|
ctx.platforms = platforms;
|
|
78
107
|
|
|
79
108
|
const platformNames = platforms.map((key) => PLATFORMS[key].name).join(', ');
|
|
80
|
-
ctx.ui.info(
|
|
109
|
+
ctx.ui.info(ctx.t('installingFor', { names: platformNames }));
|
|
81
110
|
warnLegacyClaudeFolder(platforms);
|
|
82
111
|
|
|
83
112
|
if (ctx.options.forceOverwrite) {
|
|
84
|
-
ctx.ui.info(
|
|
113
|
+
ctx.ui.info(ctx.t('modeForce'));
|
|
85
114
|
} else if (ctx.dryRun) {
|
|
86
|
-
ctx.ui.info(
|
|
115
|
+
ctx.ui.info(ctx.t('modeDryRun'));
|
|
87
116
|
} else {
|
|
88
|
-
ctx.ui.info(
|
|
117
|
+
ctx.ui.info(ctx.t('modeInstall'));
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
return ctx;
|
|
92
121
|
}
|
|
93
122
|
|
|
94
|
-
module.exports = { promptPlatformSelection, resolvePlatforms };
|
|
123
|
+
module.exports = { selectLanguage, promptPlatformSelection, resolvePlatforms };
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - Python venv at <skillsDir>/.venv + pip install each skill's requirements
|
|
6
6
|
* - skill-local `npm install` (scripts/package.json without node_modules)
|
|
7
7
|
* - Puppeteer Chromium binary (chrome-devtools)
|
|
8
|
+
* - Playwright browser binary (pptx html2pptx)
|
|
8
9
|
*
|
|
9
10
|
* System binaries (ffmpeg, poppler, librsvg, tesseract) and global npm CLIs are
|
|
10
11
|
* NOT auto-installed — they are detected and printed as guidance. All steps are
|
|
@@ -23,93 +24,96 @@ async function shouldRun(ctx) {
|
|
|
23
24
|
if (ctx.options.withSkillsDeps) return true;
|
|
24
25
|
if (!ctx.interactive) return false;
|
|
25
26
|
const yes = await ctx.ui.confirm(
|
|
26
|
-
{ message: '
|
|
27
|
+
{ message: ctx.t('skillDepsConfirm'), initialValue: false },
|
|
27
28
|
false
|
|
28
29
|
);
|
|
29
30
|
return yes === true;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/** Provision Python venv + pip requirements for a platform's skills. */
|
|
33
|
+
/** Provision or upgrade Python venv + pip requirements for a platform's skills. */
|
|
33
34
|
function setupPython(ctx, skillsDir) {
|
|
34
35
|
const reqs = dep.collectRequirements(skillsDir);
|
|
35
36
|
if (reqs.length === 0) return;
|
|
36
37
|
|
|
37
38
|
const py = dep.findPython();
|
|
38
39
|
if (!py) {
|
|
39
|
-
ctx.ui.warn(
|
|
40
|
+
ctx.ui.warn(ctx.t('venvNoHost'));
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
const alreadyExists = dep.venvValid(skillsDir);
|
|
45
|
+
ctx.ui.startSpinner(ctx.t('venvCreating'));
|
|
44
46
|
if (!dep.createVenv(skillsDir, py)) {
|
|
45
|
-
ctx.ui.stopSpinner('
|
|
46
|
-
ctx.ui.warn(
|
|
47
|
+
ctx.ui.stopSpinner(ctx.t('venvFailed'));
|
|
48
|
+
ctx.ui.warn(ctx.t('venvFailed'));
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
dep.pipUpgrade(skillsDir);
|
|
50
|
-
ctx.ui.stopSpinner(
|
|
52
|
+
ctx.ui.stopSpinner(`${ctx.t('venvReady')} (${dep.venvDir(skillsDir)})`);
|
|
51
53
|
|
|
54
|
+
// Upgrade packages if venv already existed (update run), install fresh otherwise.
|
|
55
|
+
const upgrade = alreadyExists;
|
|
52
56
|
for (const { skill, file } of reqs) {
|
|
53
|
-
ctx.ui.startSpinner(
|
|
54
|
-
const ok = dep.pipInstall(skillsDir, file);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
ctx.ui.startSpinner(ctx.t(upgrade ? 'pipInstalling' : 'pipInstalling', { skill }));
|
|
58
|
+
const ok = dep.pipInstall(skillsDir, file, upgrade);
|
|
59
|
+
if (ok) {
|
|
60
|
+
ctx.ui.stopSpinner(ctx.t('pipInstalled', { skill }));
|
|
61
|
+
} else {
|
|
62
|
+
ctx.ui.stopSpinner(ctx.t('pipFailed', { skill, cmd: `"${dep.venvPython(skillsDir)}" -m pip install -r "${file}"` }));
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
/** Run skill-local npm installs +
|
|
67
|
+
/** Run skill-local npm installs/upgrades + browser binaries for a platform's skills. */
|
|
63
68
|
function setupNode(ctx, skillsDir) {
|
|
64
|
-
for (const { skill, dir } of dep.collectSkillPackages(skillsDir)) {
|
|
65
|
-
ctx.ui.startSpinner(
|
|
66
|
-
const ok = dep.npmInstall(dir);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
for (const { skill, dir, hasNodeModules } of dep.collectSkillPackages(skillsDir)) {
|
|
70
|
+
ctx.ui.startSpinner(ctx.t('npmInstalling', { skill }));
|
|
71
|
+
const ok = dep.npmInstall(dir, hasNodeModules); // upgrade if node_modules exists
|
|
72
|
+
if (ok) {
|
|
73
|
+
ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
|
|
74
|
+
} else {
|
|
75
|
+
ctx.ui.stopSpinner(ctx.t('npmFailed', { skill, dir }));
|
|
70
76
|
continue;
|
|
71
77
|
}
|
|
72
|
-
|
|
78
|
+
|
|
79
|
+
// Skills using Playwright (e.g. pptx) also need a browser binary.
|
|
73
80
|
if (dep.pkgHasDep(dir, 'playwright')) {
|
|
74
|
-
ctx.ui.startSpinner(
|
|
81
|
+
ctx.ui.startSpinner(ctx.t('playwrightInstalling', { skill }));
|
|
75
82
|
const pw = dep.installPlaywrightBrowser(dir);
|
|
76
|
-
ctx.ui.stopSpinner(pw ?
|
|
83
|
+
ctx.ui.stopSpinner(pw ? ctx.t('playwrightReady', { skill }) : ctx.t('playwrightSkipped', { skill }));
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
// Chromium for chrome-devtools (puppeteer-based).
|
|
80
88
|
const chromeScripts = path.join(skillsDir, 'chrome-devtools', 'scripts');
|
|
81
89
|
if (fs.existsSync(path.join(chromeScripts, 'package.json'))) {
|
|
82
|
-
ctx.ui.startSpinner('
|
|
90
|
+
ctx.ui.startSpinner(ctx.t('chromiumInstalling'));
|
|
83
91
|
const ok = dep.installChromium(chromeScripts);
|
|
84
|
-
ctx.ui.stopSpinner(ok ? '
|
|
85
|
-
if (!ok) ctx.ui.warn('Chromium not downloaded — chrome-devtools may prompt on first use.');
|
|
92
|
+
ctx.ui.stopSpinner(ok ? ctx.t('chromiumReady') : ctx.t('chromiumSkipped'));
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
/** Detect (don't install) system binaries + global npm CLIs and print guidance. */
|
|
90
97
|
function guideManual(ctx) {
|
|
91
98
|
const missingTools = dep.SYSTEM_TOOLS.filter((t) => !dep.hasCmd(t.cmd));
|
|
92
|
-
const missingNpm
|
|
99
|
+
const missingNpm = dep.GLOBAL_NPM.filter((g) => !dep.hasCmd(g.cmd));
|
|
93
100
|
if (missingTools.length === 0 && missingNpm.length === 0) return;
|
|
94
101
|
|
|
95
102
|
const lines = [];
|
|
96
103
|
for (const t of missingTools) lines.push(`${t.cmd} — ${t.why}\n ${dep.systemHint(t)}`);
|
|
97
|
-
for (const g of missingNpm)
|
|
98
|
-
ctx.ui.note(lines.join('\n'), '
|
|
104
|
+
for (const g of missingNpm) lines.push(`${g.cmd} — ${g.why}\n npm install -g ${g.pkg}`);
|
|
105
|
+
ctx.ui.note(lines.join('\n'), ctx.t('optionalToolsTitle'));
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
async function setupSkillDeps(ctx) {
|
|
102
109
|
if (!(await shouldRun(ctx))) {
|
|
103
|
-
if (!ctx.dryRun)
|
|
104
|
-
ctx.ui.info('Skill dependencies skipped — run later with: npx @haposoft/cafekit --with-skills-deps');
|
|
105
|
-
}
|
|
110
|
+
if (!ctx.dryRun) ctx.ui.info(ctx.t('skillsSkipped'));
|
|
106
111
|
return ctx;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
for (const key of ctx.platforms) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
setupNode(ctx, skillsDir);
|
|
115
|
+
setupPython(ctx, PLATFORMS[key].skillsDir);
|
|
116
|
+
setupNode(ctx, PLATFORMS[key].skillsDir);
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
guideManual(ctx);
|
package/bin/phases/summary.js
CHANGED
|
@@ -6,49 +6,65 @@
|
|
|
6
6
|
* plain text otherwise.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
9
11
|
const { PLATFORMS, packageJson } = require('../lib/context');
|
|
10
12
|
|
|
13
|
+
/** Installed skills that ship a .env.example (i.e. need API keys configured). */
|
|
14
|
+
function skillsNeedingKeys(platforms) {
|
|
15
|
+
const found = new Set();
|
|
16
|
+
for (const key of platforms) {
|
|
17
|
+
const skillsDir = PLATFORMS[key].skillsDir;
|
|
18
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
19
|
+
for (const skill of fs.readdirSync(skillsDir)) {
|
|
20
|
+
if (fs.existsSync(path.join(skillsDir, skill, '.env.example'))) found.add(skill);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...found].sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
function printSummary(ctx) {
|
|
12
|
-
const { results, platforms, options, ui } = ctx;
|
|
27
|
+
const { results, platforms, options, ui, t } = ctx;
|
|
28
|
+
const pad = (s) => `${s}:`.padEnd(16, ' ');
|
|
13
29
|
|
|
14
30
|
const stats = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
results.missingDependencies > 0 ?
|
|
22
|
-
results.errors > 0 ?
|
|
31
|
+
`${pad(t('labelCopied'))}${results.copied}`,
|
|
32
|
+
`${pad(t('labelUpdated'))}${results.updated}`,
|
|
33
|
+
`${pad(t('labelUnchanged'))}${results.unchanged}`,
|
|
34
|
+
`${pad(t('labelPreserved'))}${results.preserved}`,
|
|
35
|
+
`${pad(t('labelSkills'))}${results.installedSkills > 0 ? t('yes') : t('no')}`,
|
|
36
|
+
`${pad(t('labelVersion'))}${packageJson.version}`,
|
|
37
|
+
results.missingDependencies > 0 ? `${pad(t('labelMissing'))}${results.missingDependencies}` : null,
|
|
38
|
+
results.errors > 0 ? `${pad(t('labelErrors'))}${results.errors}` : null
|
|
23
39
|
].filter(Boolean).join('\n');
|
|
24
40
|
|
|
25
|
-
ui.note(stats, ctx.dryRun ? '
|
|
41
|
+
ui.note(stats, ctx.dryRun ? t('summaryDryTitle') : t('summaryTitle'));
|
|
26
42
|
|
|
27
43
|
if (results.preserved > 0) {
|
|
28
44
|
const list = results.preservedFiles.slice(0, 8).map((f) => ` ⊘ ${f}`).join('\n');
|
|
29
|
-
const more = results.preservedFiles.length > 8 ? `\n ...
|
|
30
|
-
ui.warn(`${results.preserved}
|
|
31
|
-
+ 'Re-run with --force-overwrite to replace them (a backup is kept under .cafekit-backup/).');
|
|
45
|
+
const more = results.preservedFiles.length > 8 ? `\n ... +${results.preservedFiles.length - 8}` : '';
|
|
46
|
+
ui.warn(`${t('preservedNote', { n: results.preserved })}\n${list}${more}`);
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
if (ctx.dryRun) {
|
|
35
|
-
ui.outro('
|
|
50
|
+
ui.outro(t('dryRunOnly'));
|
|
36
51
|
return;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
const lines = [];
|
|
40
55
|
for (const key of platforms) {
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
lines.push(key === 'claude' ? t('nsClaude') : t('nsOpencode'));
|
|
57
|
+
}
|
|
58
|
+
const keySkills = skillsNeedingKeys(platforms);
|
|
59
|
+
if (keySkills.length > 0) {
|
|
60
|
+
lines.push(t('nsKeys', { skills: keySkills.join(', ') }));
|
|
43
61
|
}
|
|
44
|
-
const envHosts = platforms.map((key) => `${PLATFORMS[key].folder}/.env`).join(' and ');
|
|
45
|
-
lines.push(`API keys isolated in ${envHosts}`);
|
|
46
62
|
if (!options.forceOverwrite) {
|
|
47
|
-
lines.push('
|
|
63
|
+
lines.push(t('nsForce'));
|
|
48
64
|
}
|
|
49
|
-
ui.note(lines.join('\n'), '
|
|
65
|
+
ui.note(lines.join('\n'), t('nextStepsTitle'));
|
|
50
66
|
|
|
51
|
-
ui.outro(
|
|
67
|
+
ui.outro(t('outroDone'));
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
module.exports = { printSummary };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.
|
|
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 {
|