@haposoft/cafekit 0.11.7 → 0.11.9

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',
@@ -13,11 +13,11 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const { spawnSync } = require('child_process');
16
+ const { spawnSync, spawn } = require('child_process');
17
17
 
18
18
  const isWindows = process.platform === 'win32';
19
19
 
20
- /** Run a command, return { ok, stdout, stderr, status }. Never throws. */
20
+ /** Run a command synchronously, return { ok, stdout, stderr, status }. Never throws. */
21
21
  function run(cmd, args, opts = {}) {
22
22
  const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
23
23
  return {
@@ -28,6 +28,31 @@ function run(cmd, args, opts = {}) {
28
28
  };
29
29
  }
30
30
 
31
+ /**
32
+ * Run a command asynchronously — keeps the event loop alive so spinners can animate.
33
+ * Returns a Promise<{ ok, stdout, stderr, status }>. Never rejects.
34
+ */
35
+ function runAsync(cmd, args, opts = {}) {
36
+ return new Promise((resolve) => {
37
+ const child = spawn(cmd, args, { stdio: 'pipe', ...opts });
38
+ let stdout = '';
39
+ let stderr = '';
40
+ if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
41
+ if (child.stderr) child.stderr.on('data', (d) => { stderr += d.toString(); });
42
+ child.on('close', (code) => {
43
+ resolve({
44
+ ok: code === 0,
45
+ status: code,
46
+ stdout,
47
+ stderr
48
+ });
49
+ });
50
+ child.on('error', (err) => {
51
+ resolve({ ok: false, status: null, stdout, stderr: stderr || err.message });
52
+ });
53
+ });
54
+ }
55
+
31
56
  /** Is a command available on PATH? */
32
57
  function hasCmd(name) {
33
58
  const probe = isWindows ? run('where', [name]) : run('which', [name]);
@@ -76,26 +101,37 @@ function pipInstall(skillsDir, requirementsPath, upgrade = false) {
76
101
  return run(venvPython(skillsDir), args).ok;
77
102
  }
78
103
 
79
- /** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. */
80
- function npmInstall(dir, upgrade = false) {
104
+ /** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. Async — keeps spinner alive. */
105
+ async function npmInstall(dir, upgrade = false, extraEnv = {}) {
81
106
  const args = upgrade ? ['update', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund'];
82
- return run('npm', args, { cwd: dir }).ok;
107
+ const env = { ...process.env, ...extraEnv };
108
+ const r = await runAsync('npm', args, { cwd: dir, env });
109
+ return r.ok;
83
110
  }
84
111
 
85
112
  /**
86
113
  * Download the Puppeteer Chromium browser for chrome-devtools.
87
114
  * Prefer puppeteer's own install script (cache-aware, same as its postinstall);
88
- * fall back to the bundled @puppeteer/browsers CLI. `npx puppeteer ...` is
89
- * avoided — puppeteer ships no `puppeteer` bin under that invocation and `--yes`
90
- * would refetch from the registry.
115
+ * fall back to the bundled @puppeteer/browsers CLI.
116
+ *
117
+ * Non-interactive (default): async — keeps spinner alive during download.
118
+ * Interactive: sync with stdio inherit — native progress bar renders in TTY.
91
119
  */
92
- function installChromium(scriptsDir) {
120
+ async function installChromium(scriptsDir, interactive = false) {
93
121
  // Absolute path so it resolves regardless of the child process cwd.
94
122
  const installMjs = path.resolve(scriptsDir, 'node_modules', 'puppeteer', 'install.mjs');
95
123
  if (fs.existsSync(installMjs)) {
96
- return run('node', [installMjs], { cwd: scriptsDir }).ok;
124
+ if (interactive) {
125
+ return run('node', [installMjs], { cwd: scriptsDir, stdio: 'inherit' }).ok;
126
+ }
127
+ const r = await runAsync('node', [installMjs], { cwd: scriptsDir });
128
+ return r.ok;
129
+ }
130
+ if (interactive) {
131
+ return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir, stdio: 'inherit' }).ok;
97
132
  }
98
- return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir }).ok;
133
+ const r = await runAsync('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir });
134
+ return r.ok;
99
135
  }
100
136
 
101
137
  /** Does a skill scripts dir declare a given npm dependency? */
@@ -108,9 +144,59 @@ function pkgHasDep(scriptsDir, name) {
108
144
  }
109
145
  }
110
146
 
111
- /** Download the Playwright Chromium browser (for the pptx html2pptx workflow). */
112
- function installPlaywrightBrowser(scriptsDir) {
113
- return run('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir }).ok;
147
+ /** Download the Playwright Chromium browser (for the pptx html2pptx workflow). Async — keeps spinner alive. */
148
+ async function installPlaywrightBrowser(scriptsDir) {
149
+ const r = await runAsync('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir });
150
+ return r.ok;
151
+ }
152
+
153
+ /**
154
+ * Detect system Chrome/Chromium installation (cross-platform).
155
+ * Returns the executable path if found, undefined otherwise.
156
+ * Supports macOS, Windows, Linux (including snap + PATH fallback).
157
+ */
158
+ function detectSystemChrome() {
159
+ const candidates = (() => {
160
+ switch (process.platform) {
161
+ case 'darwin':
162
+ return [
163
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
164
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
165
+ ];
166
+ case 'win32': {
167
+ const pf = process.env.PROGRAMFILES;
168
+ const pf86 = process.env['PROGRAMFILES(X86)'];
169
+ const local = process.env.LOCALAPPDATA;
170
+ return [
171
+ pf && `${pf}\\Google\\Chrome\\Application\\chrome.exe`,
172
+ pf86 && `${pf86}\\Google\\Chrome\\Application\\chrome.exe`,
173
+ local && `${local}\\Google\\Chrome\\Application\\chrome.exe`,
174
+ ].filter(Boolean);
175
+ }
176
+ default: // Linux & others
177
+ return [
178
+ '/usr/bin/google-chrome-stable',
179
+ '/usr/bin/google-chrome',
180
+ '/usr/bin/chromium-browser',
181
+ '/usr/bin/chromium',
182
+ '/snap/bin/chromium',
183
+ ];
184
+ }
185
+ })();
186
+
187
+ for (const p of candidates) {
188
+ try { if (fs.existsSync(p)) return p; } catch { /* skip */ }
189
+ }
190
+
191
+ // Linux: last resort — check PATH
192
+ if (process.platform === 'linux') {
193
+ for (const cmd of ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium']) {
194
+ const r = run(isWindows ? 'where' : 'which', [cmd]);
195
+ if (r.ok && r.stdout.trim()) return r.stdout.trim();
196
+ }
197
+ }
198
+
199
+ return undefined;
114
200
  }
115
201
 
116
202
  /**
@@ -176,6 +262,7 @@ module.exports = {
176
262
  pipInstall,
177
263
  npmInstall,
178
264
  installChromium,
265
+ detectSystemChrome,
179
266
  collectRequirements,
180
267
  collectSkillPackages,
181
268
  pkgHasDep,
@@ -65,10 +65,21 @@ function setupPython(ctx, skillsDir) {
65
65
  }
66
66
 
67
67
  /** Run skill-local npm installs/upgrades + browser binaries for a platform's skills. */
68
- function setupNode(ctx, skillsDir) {
68
+ async 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 = await dep.npmInstall(dir, hasNodeModules, extraEnv);
82
+
72
83
  if (ok) {
73
84
  ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
74
85
  } else {
@@ -79,17 +90,21 @@ function setupNode(ctx, skillsDir) {
79
90
  // Skills using Playwright (e.g. pptx) also need a browser binary.
80
91
  if (dep.pkgHasDep(dir, 'playwright')) {
81
92
  ctx.ui.startSpinner(ctx.t('playwrightInstalling', { skill }));
82
- const pw = dep.installPlaywrightBrowser(dir);
93
+ const pw = await dep.installPlaywrightBrowser(dir);
83
94
  ctx.ui.stopSpinner(pw ? ctx.t('playwrightReady', { skill }) : ctx.t('playwrightSkipped', { skill }));
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 = await dep.installChromium(chromeScripts, true);
106
+ ctx.ui.info(ok ? '✓ ' + ctx.t('chromiumReady') : ctx.t('chromiumSkipped'));
107
+ }
93
108
  }
94
109
  }
95
110
 
@@ -113,7 +128,7 @@ async function setupSkillDeps(ctx) {
113
128
 
114
129
  for (const key of ctx.platforms) {
115
130
  setupPython(ctx, PLATFORMS[key].skillsDir);
116
- setupNode(ctx, PLATFORMS[key].skillsDir);
131
+ await setupNode(ctx, PLATFORMS[key].skillsDir);
117
132
  }
118
133
 
119
134
  guideManual(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.11.7",
3
+ "version": "0.11.9",
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',