@haposoft/cafekit 0.11.7 → 0.11.8

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/i18n.js CHANGED
@@ -75,6 +75,7 @@ const MESSAGES = {
75
75
  chromiumInstalling: 'Downloading Chromium for chrome-devtools',
76
76
  chromiumReady: 'Chromium ready',
77
77
  chromiumSkipped: 'Chromium download skipped — chrome-devtools will prompt on first use.',
78
+ chromiumSystemChromeFound: 'System Chrome detected ({path}), skipping download',
78
79
  optionalToolsTitle: 'Optional tools to install manually',
79
80
 
80
81
  // Summary stats
@@ -180,6 +181,7 @@ const MESSAGES = {
180
181
  chromiumInstalling: 'chrome-devtools 用 Chromium をダウンロード中',
181
182
  chromiumReady: 'Chromium の準備ができました',
182
183
  chromiumSkipped: 'Chromium のダウンロードをスキップ — 初回使用時に chrome-devtools が自動取得します。',
184
+ chromiumSystemChromeFound: 'システムChromeを検出 ({path})、ダウンロードをスキップします',
183
185
  optionalToolsTitle: '手動でインストールが必要なオプションツール',
184
186
 
185
187
  summaryTitle: 'インストール完了',
@@ -282,6 +284,7 @@ const MESSAGES = {
282
284
  chromiumInstalling: 'Đang tải Chromium cho chrome-devtools',
283
285
  chromiumReady: 'Chromium đã sẵn sàng',
284
286
  chromiumSkipped: 'Bỏ qua tải Chromium — chrome-devtools sẽ tự tải khi dùng lần đầu.',
287
+ chromiumSystemChromeFound: 'Đã phát hiện Chrome hệ thống ({path}), bỏ qua tải Chromium',
285
288
  optionalToolsTitle: 'Công cụ tuỳ chọn cần cài thủ công',
286
289
 
287
290
  summaryTitle: 'Cài đặt hoàn tất',
@@ -77,9 +77,10 @@ function pipInstall(skillsDir, requirementsPath, upgrade = false) {
77
77
  }
78
78
 
79
79
  /** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. */
80
- function npmInstall(dir, upgrade = false) {
80
+ function npmInstall(dir, upgrade = false, extraEnv = {}) {
81
81
  const args = upgrade ? ['update', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund'];
82
- return run('npm', args, { cwd: dir }).ok;
82
+ const env = { ...process.env, ...extraEnv };
83
+ return run('npm', args, { cwd: dir, env }).ok;
83
84
  }
84
85
 
85
86
  /**
@@ -88,14 +89,22 @@ function npmInstall(dir, upgrade = false) {
88
89
  * fall back to the bundled @puppeteer/browsers CLI. `npx puppeteer ...` is
89
90
  * avoided — puppeteer ships no `puppeteer` bin under that invocation and `--yes`
90
91
  * would refetch from the registry.
92
+ *
93
+ * When `interactive` is true, stdio is inherited so the native progress bar
94
+ * renders directly on the user's terminal (TTY-only, no pipe capture).
91
95
  */
92
- function installChromium(scriptsDir) {
96
+ function installChromium(scriptsDir, interactive = false) {
97
+ const stdioOpt = interactive ? 'inherit' : 'pipe';
93
98
  // Absolute path so it resolves regardless of the child process cwd.
94
99
  const installMjs = path.resolve(scriptsDir, 'node_modules', 'puppeteer', 'install.mjs');
95
100
  if (fs.existsSync(installMjs)) {
96
- return run('node', [installMjs], { cwd: scriptsDir }).ok;
101
+ const opts = { cwd: scriptsDir, stdio: stdioOpt };
102
+ if (interactive) opts.encoding = 'utf8';
103
+ return run('node', [installMjs], opts).ok;
97
104
  }
98
- return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir }).ok;
105
+ const opts = { cwd: scriptsDir, stdio: stdioOpt };
106
+ if (interactive) opts.encoding = 'utf8';
107
+ return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], opts).ok;
99
108
  }
100
109
 
101
110
  /** Does a skill scripts dir declare a given npm dependency? */
@@ -113,6 +122,55 @@ function installPlaywrightBrowser(scriptsDir) {
113
122
  return run('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir }).ok;
114
123
  }
115
124
 
125
+ /**
126
+ * Detect system Chrome/Chromium installation (cross-platform).
127
+ * Returns the executable path if found, undefined otherwise.
128
+ * Supports macOS, Windows, Linux (including snap + PATH fallback).
129
+ */
130
+ function detectSystemChrome() {
131
+ const candidates = (() => {
132
+ switch (process.platform) {
133
+ case 'darwin':
134
+ return [
135
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
136
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
137
+ ];
138
+ case 'win32': {
139
+ const pf = process.env.PROGRAMFILES;
140
+ const pf86 = process.env['PROGRAMFILES(X86)'];
141
+ const local = process.env.LOCALAPPDATA;
142
+ return [
143
+ pf && `${pf}\\Google\\Chrome\\Application\\chrome.exe`,
144
+ pf86 && `${pf86}\\Google\\Chrome\\Application\\chrome.exe`,
145
+ local && `${local}\\Google\\Chrome\\Application\\chrome.exe`,
146
+ ].filter(Boolean);
147
+ }
148
+ default: // Linux & others
149
+ return [
150
+ '/usr/bin/google-chrome-stable',
151
+ '/usr/bin/google-chrome',
152
+ '/usr/bin/chromium-browser',
153
+ '/usr/bin/chromium',
154
+ '/snap/bin/chromium',
155
+ ];
156
+ }
157
+ })();
158
+
159
+ for (const p of candidates) {
160
+ try { if (fs.existsSync(p)) return p; } catch { /* skip */ }
161
+ }
162
+
163
+ // Linux: last resort — check PATH
164
+ if (process.platform === 'linux') {
165
+ for (const cmd of ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium']) {
166
+ const r = run(isWindows ? 'where' : 'which', [cmd]);
167
+ if (r.ok && r.stdout.trim()) return r.stdout.trim();
168
+ }
169
+ }
170
+
171
+ return undefined;
172
+ }
173
+
116
174
  /**
117
175
  * List installed skills that ship a runnable scripts/requirements.txt
118
176
  * (excludes the tests/ requirements). Returns [{ skill, file }].
@@ -176,6 +234,7 @@ module.exports = {
176
234
  pipInstall,
177
235
  npmInstall,
178
236
  installChromium,
237
+ detectSystemChrome,
179
238
  collectRequirements,
180
239
  collectSkillPackages,
181
240
  pkgHasDep,
@@ -66,9 +66,20 @@ function setupPython(ctx, skillsDir) {
66
66
 
67
67
  /** Run skill-local npm installs/upgrades + browser binaries for a platform's skills. */
68
68
  function setupNode(ctx, skillsDir) {
69
+ // Detect system Chrome before npm install — skip ~300MB Chromium download if found
70
+ const chromeScripts = path.join(skillsDir, 'chrome-devtools', 'scripts');
71
+ const hasChromePkg = fs.existsSync(path.join(chromeScripts, 'package.json'));
72
+ const systemChrome = hasChromePkg ? dep.detectSystemChrome() : null;
73
+
69
74
  for (const { skill, dir, hasNodeModules } of dep.collectSkillPackages(skillsDir)) {
70
75
  ctx.ui.startSpinner(ctx.t('npmInstalling', { skill }));
71
- const ok = dep.npmInstall(dir, hasNodeModules); // upgrade if node_modules exists
76
+
77
+ // chrome-devtools: skip puppeteer postinstall Chromium download if system Chrome exists
78
+ const extraEnv = (skill === 'chrome-devtools' && systemChrome)
79
+ ? { PUPPETEER_SKIP_DOWNLOAD: 'true' }
80
+ : {};
81
+ const ok = dep.npmInstall(dir, hasNodeModules, extraEnv);
82
+
72
83
  if (ok) {
73
84
  ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
74
85
  } else {
@@ -84,12 +95,16 @@ function setupNode(ctx, skillsDir) {
84
95
  }
85
96
  }
86
97
 
87
- // Chromium for chrome-devtools (puppeteer-based).
88
- const chromeScripts = path.join(skillsDir, 'chrome-devtools', 'scripts');
89
- if (fs.existsSync(path.join(chromeScripts, 'package.json'))) {
90
- ctx.ui.startSpinner(ctx.t('chromiumInstalling'));
91
- const ok = dep.installChromium(chromeScripts);
92
- ctx.ui.stopSpinner(ok ? ctx.t('chromiumReady') : ctx.t('chromiumSkipped'));
98
+ // Chromium for chrome-devtools (puppeteer-based, ~300MB).
99
+ // Uses stdio:inherit for native progress bar when downloading (TTY-only).
100
+ if (hasChromePkg) {
101
+ if (systemChrome) {
102
+ ctx.ui.info(ctx.t('chromiumSystemChromeFound', { path: systemChrome }));
103
+ } else {
104
+ ctx.ui.info(ctx.t('chromiumInstalling'));
105
+ const ok = dep.installChromium(chromeScripts, true);
106
+ ctx.ui.info(ok ? '✓ ' + ctx.t('chromiumReady') : ctx.t('chromiumSkipped'));
107
+ }
93
108
  }
94
109
  }
95
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.11.7",
3
+ "version": "0.11.8",
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",
@@ -191,6 +191,39 @@ node "$SKILL_DIR/navigate.js" --url https://example.com
191
191
 
192
192
  **Linux/WSL only**: Run `"$SKILL_DIR/install-deps.sh"` first for Chrome system libraries.
193
193
 
194
+ ## Chrome/Chromium Configuration
195
+
196
+ By default, chrome-devtools bundles Chromium via puppeteer (~300MB download on first install). To use your existing Chrome installation instead:
197
+
198
+ ### Auto-Detection
199
+
200
+ chrome-devtools auto-detects Chrome at standard OS paths and uses it automatically:
201
+
202
+ | OS | Checked Paths |
203
+ |----|---------------|
204
+ | macOS | `/Applications/Google Chrome.app/...`, `/Applications/Chromium.app/...` |
205
+ | Windows | `%PROGRAMFILES%\Google\Chrome\...`, `%PROGRAMFILES(X86)%\...`, `%LOCALAPPDATA%\...` |
206
+ | Linux | `/usr/bin/google-chrome*`, `/usr/bin/chromium*`, `/snap/bin/chromium` |
207
+
208
+ ### Manual Configuration
209
+
210
+ To force a specific Chrome installation, copy `.env.example` to `.env` and set `CHROME_EXECUTABLE_PATH`:
211
+
212
+ ```bash
213
+ cp "$SKILL_DIR/.env.example" "$SKILL_DIR/.env"
214
+ # Edit .env and set CHROME_EXECUTABLE_PATH to your Chrome executable
215
+ ```
216
+
217
+ ### Skip Chromium Download
218
+
219
+ To skip the ~300MB download during `npm install` when Chrome is already available:
220
+
221
+ ```bash
222
+ PUPPETEER_SKIP_DOWNLOAD=true npm install --prefix "$SKILL_DIR"
223
+ ```
224
+
225
+ If no system Chrome is found, puppeteer's bundled Chromium is used as a fallback.
226
+
194
227
  ## Session Persistence
195
228
 
196
229
  Browser state persists across script executions via WebSocket endpoint file (`.browser-session.json`).
@@ -0,0 +1,20 @@
1
+ # Chrome/Chromium Configuration
2
+ # ============================================================================
3
+
4
+ # Custom Chrome/Chromium executable path (optional)
5
+ # chrome-devtools auto-detects system Chrome at standard OS paths.
6
+ # Set this only if Chrome is installed in a non-standard location.
7
+ #
8
+ # macOS:
9
+ # CHROME_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
10
+ #
11
+ # Windows:
12
+ # CHROME_EXECUTABLE_PATH=C:\Program Files\Google\Chrome\Application\chrome.exe
13
+ #
14
+ # Linux:
15
+ # CHROME_EXECUTABLE_PATH=/usr/bin/google-chrome
16
+
17
+ # Skip puppeteer's Chromium download during npm install (optional)
18
+ # Set to true if you already have Chrome/Chromium installed and want to skip
19
+ # the ~300MB Chromium download during `npm install`.
20
+ # PUPPETEER_SKIP_DOWNLOAD=true
@@ -48,6 +48,63 @@ export function resolveHeadless(value) {
48
48
  return false;
49
49
  }
50
50
 
51
+ /**
52
+ * Resolve Chrome/Chromium executable path.
53
+ * Fallback chain: CHROME_EXECUTABLE_PATH → PUPPETEER_EXECUTABLE_PATH →
54
+ * OS auto-detect → undefined (puppeteer bundled Chromium).
55
+ * @returns {string|undefined}
56
+ */
57
+ export function resolveExecutablePath() {
58
+ // 1. Env-var overrides (CafeKit + Puppeteer conventions)
59
+ for (const key of ['CHROME_EXECUTABLE_PATH', 'PUPPETEER_EXECUTABLE_PATH']) {
60
+ const p = process.env[key];
61
+ if (!p) continue;
62
+ try {
63
+ if (fs.existsSync(p)) { log(`Chrome (${key}): ${p}`); return p; }
64
+ log(`${key} set but not found: ${p}`);
65
+ } catch { /* permission error — skip */ }
66
+ }
67
+
68
+ // 2. OS auto-detection
69
+ const candidates = (() => {
70
+ switch (process.platform) {
71
+ case 'darwin':
72
+ return [
73
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
74
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
75
+ ];
76
+ case 'win32': {
77
+ const pf = process.env.PROGRAMFILES;
78
+ const pf86 = process.env['PROGRAMFILES(X86)'];
79
+ const local = process.env.LOCALAPPDATA;
80
+ return [
81
+ pf && `${pf}\\Google\\Chrome\\Application\\chrome.exe`,
82
+ pf86 && `${pf86}\\Google\\Chrome\\Application\\chrome.exe`,
83
+ local && `${local}\\Google\\Chrome\\Application\\chrome.exe`,
84
+ ].filter(Boolean);
85
+ }
86
+ default: // Linux & others
87
+ return [
88
+ '/usr/bin/google-chrome-stable',
89
+ '/usr/bin/google-chrome',
90
+ '/usr/bin/chromium-browser',
91
+ '/usr/bin/chromium',
92
+ '/snap/bin/chromium',
93
+ ];
94
+ }
95
+ })();
96
+
97
+ for (const p of candidates) {
98
+ try {
99
+ if (fs.existsSync(p)) { log(`Chrome auto-detected: ${p}`); return p; }
100
+ } catch { /* skip */ }
101
+ }
102
+
103
+ // 3. Bundled Chromium fallback
104
+ log('No system Chrome found, using bundled Chromium');
105
+ return undefined;
106
+ }
107
+
51
108
  /**
52
109
  * Get default Chrome profile path based on OS
53
110
  * @returns {string} - Path to Chrome's default user data directory
@@ -253,12 +310,16 @@ export async function getBrowser(options = {}) {
253
310
  log(`Using default Chrome profile: ${userDataDir}`);
254
311
  }
255
312
 
313
+ // Resolve Chrome executable — env var or OS auto-detect (fallback: bundled Chromium)
314
+ const executablePath = resolveExecutablePath();
315
+
256
316
  // Destructure known properties — only pass Puppeteer-valid options to launch()
257
317
  const { headless, args: extraArgs, viewport, useDefaultProfile, profile, browserUrl, wsEndpoint: _ws, userDataDir: _udd, ...restOptions } = options;
258
318
 
259
319
  // Launch new browser
260
320
  const launchOptions = {
261
321
  headless: resolveHeadless(headless),
322
+ ...(executablePath && { executablePath }),
262
323
  args: [
263
324
  '--no-sandbox',
264
325
  '--disable-setuid-sandbox',