@haposoft/cafekit 0.11.5 → 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/install.js +19 -1
- package/bin/lib/i18n.js +57 -3
- package/bin/lib/registry.js +61 -0
- package/bin/lib/skill-deps.js +64 -5
- package/bin/lib/version-check.js +51 -6
- package/bin/phases/post-install.js +28 -0
- package/bin/phases/select-platform.js +69 -0
- package/bin/phases/skills-setup.js +22 -7
- package/package.json +1 -1
- package/src/claude/skills/chrome-devtools/SKILL.md +33 -0
- package/src/claude/skills/chrome-devtools/scripts/.env.example +20 -0
- package/src/claude/skills/chrome-devtools/scripts/lib/browser.js +61 -0
package/bin/install.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - --dry-run — preview only, no filesystem changes.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
const { spawnSync } = require('child_process');
|
|
18
19
|
const { buildContext, PLATFORMS, packageJson } = require('./lib/context');
|
|
19
20
|
const lock = require('./lib/lock');
|
|
20
21
|
const backup = require('./lib/backup');
|
|
@@ -129,10 +130,27 @@ async function main() {
|
|
|
129
130
|
await resolvePlatforms(ctx);
|
|
130
131
|
if (ctx.cancelled) { lock.release(); process.exit(0); }
|
|
131
132
|
|
|
132
|
-
// Version check: same → exit, downgrade → confirm
|
|
133
|
+
// Version check: same → exit, downgrade → confirm, upgrade → version picker
|
|
133
134
|
await checkVersions(ctx);
|
|
134
135
|
if (ctx.cancelled) { lock.release(); process.exit(0); }
|
|
135
136
|
|
|
137
|
+
// User picked a different version from the picker — re-exec that version
|
|
138
|
+
if (ctx.reexec) {
|
|
139
|
+
lock.release();
|
|
140
|
+
ctx.ui.info(ctx.t('versionReexec', { v: ctx.reexec }));
|
|
141
|
+
// Forward original flags; add --force-overwrite so the re-exec'd installer
|
|
142
|
+
// skips the version picker and proceeds directly with installation.
|
|
143
|
+
const origFlags = process.argv.slice(2).filter(
|
|
144
|
+
(f) => f !== '--force-overwrite' && f !== '-f' && f !== '-u' && f !== '--upgrade'
|
|
145
|
+
);
|
|
146
|
+
const result = spawnSync(
|
|
147
|
+
'npx',
|
|
148
|
+
['-y', `@haposoft/cafekit@${ctx.reexec}`, '--force-overwrite', ...origFlags],
|
|
149
|
+
{ stdio: 'inherit', shell: process.platform === 'win32' }
|
|
150
|
+
);
|
|
151
|
+
process.exit(result.status ?? 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
136
154
|
// ── Pre-run snapshot for rollback ─────────────────────
|
|
137
155
|
// Capture platform folders AND the root files the pipeline mutates
|
|
138
156
|
// (CLAUDE.md, .gitignore) so a mid-run failure rolls back cleanly.
|
package/bin/lib/i18n.js
CHANGED
|
@@ -51,6 +51,11 @@ const MESSAGES = {
|
|
|
51
51
|
addressingSet: '{name} will address you as "{addr}"',
|
|
52
52
|
addressingInvalid: 'Letters only please — addressing skipped.',
|
|
53
53
|
|
|
54
|
+
// Addressing update
|
|
55
|
+
addressingUpdatePrompt: 'You are currently addressed as "{name}". Change it?',
|
|
56
|
+
keepAddressingOption: 'Keep "{name}"',
|
|
57
|
+
changeAddressingOption:'Change to something else',
|
|
58
|
+
|
|
54
59
|
// Skills / deps setup
|
|
55
60
|
skillDepsConfirm: 'Install skill dependencies? (Python venv, pip, npm, Chromium — takes a few minutes)',
|
|
56
61
|
skillsSkipped: 'Skill dependencies skipped.\n Run later: npx @haposoft/cafekit --with-skills-deps',
|
|
@@ -70,6 +75,7 @@ const MESSAGES = {
|
|
|
70
75
|
chromiumInstalling: 'Downloading Chromium for chrome-devtools',
|
|
71
76
|
chromiumReady: 'Chromium ready',
|
|
72
77
|
chromiumSkipped: 'Chromium download skipped — chrome-devtools will prompt on first use.',
|
|
78
|
+
chromiumSystemChromeFound: 'System Chrome detected ({path}), skipping download',
|
|
73
79
|
optionalToolsTitle: 'Optional tools to install manually',
|
|
74
80
|
|
|
75
81
|
// Summary stats
|
|
@@ -105,7 +111,19 @@ const MESSAGES = {
|
|
|
105
111
|
updateOption: 'Update to {v}',
|
|
106
112
|
reinstallOption: 'Reinstall (overwrite managed files)',
|
|
107
113
|
reinstallCurrentOption: 'Reinstall {v}',
|
|
108
|
-
skipOption: 'Skip (exit)'
|
|
114
|
+
skipOption: 'Skip (exit)',
|
|
115
|
+
|
|
116
|
+
// Platform/language auto-skip when already installed
|
|
117
|
+
installingFor: 'Installing for: {names}',
|
|
118
|
+
langKept: 'Language: {lang} (saved — skipping prompt)',
|
|
119
|
+
platformKept: 'Platform: {names} (saved — skipping prompt)',
|
|
120
|
+
|
|
121
|
+
// Version picker
|
|
122
|
+
versionPickPrompt: 'Select CafeKit version to install:',
|
|
123
|
+
versionPickCurrent: '(installed)',
|
|
124
|
+
versionPickLatest: '(latest)',
|
|
125
|
+
versionReexec: 'Switching to CafeKit {v}…',
|
|
126
|
+
versionFetchFailed: 'Could not fetch version list. Using default menu.'
|
|
109
127
|
},
|
|
110
128
|
|
|
111
129
|
// ─── 日本語 ─────────────────────────────────────────────────────────────────
|
|
@@ -140,6 +158,11 @@ const MESSAGES = {
|
|
|
140
158
|
addressingSet: '{name} はあなたを「{addr}」とお呼びします',
|
|
141
159
|
addressingInvalid: '文字のみ入力できます — 呼称の設定をスキップしました。',
|
|
142
160
|
|
|
161
|
+
// Addressing update
|
|
162
|
+
addressingUpdatePrompt: '現在「{name}」と呼ばれています。変更しますか?',
|
|
163
|
+
keepAddressingOption: '「{name}」のまま',
|
|
164
|
+
changeAddressingOption:'別の名前に変更',
|
|
165
|
+
|
|
143
166
|
skillDepsConfirm: 'スキルの依存関係をインストールしますか?(Python venv、pip、npm、Chromium — 数分かかります)',
|
|
144
167
|
skillsSkipped: 'スキルの依存関係をスキップしました。\n 後で実行: npx @haposoft/cafekit --with-skills-deps',
|
|
145
168
|
venvCreating: 'Python 環境を準備しています',
|
|
@@ -158,6 +181,7 @@ const MESSAGES = {
|
|
|
158
181
|
chromiumInstalling: 'chrome-devtools 用 Chromium をダウンロード中',
|
|
159
182
|
chromiumReady: 'Chromium の準備ができました',
|
|
160
183
|
chromiumSkipped: 'Chromium のダウンロードをスキップ — 初回使用時に chrome-devtools が自動取得します。',
|
|
184
|
+
chromiumSystemChromeFound: 'システムChromeを検出 ({path})、ダウンロードをスキップします',
|
|
161
185
|
optionalToolsTitle: '手動でインストールが必要なオプションツール',
|
|
162
186
|
|
|
163
187
|
summaryTitle: 'インストール完了',
|
|
@@ -190,7 +214,19 @@ const MESSAGES = {
|
|
|
190
214
|
updateOption: '{v} にアップデート',
|
|
191
215
|
reinstallOption: '再インストール(管理対象ファイルを上書き)',
|
|
192
216
|
reinstallCurrentOption: '{v} を再インストール',
|
|
193
|
-
skipOption: 'スキップ(終了)'
|
|
217
|
+
skipOption: 'スキップ(終了)',
|
|
218
|
+
|
|
219
|
+
// Platform/language auto-skip when already installed
|
|
220
|
+
installingFor: 'インストール先: {names}',
|
|
221
|
+
langKept: '言語: {lang} (保存済み — スキップ)',
|
|
222
|
+
platformKept: 'プラットフォーム: {names} (保存済み — スキップ)',
|
|
223
|
+
|
|
224
|
+
// Version picker
|
|
225
|
+
versionPickPrompt: 'インストールする CafeKit バージョンを選択:',
|
|
226
|
+
versionPickCurrent: '(インストール済み)',
|
|
227
|
+
versionPickLatest: '(最新)',
|
|
228
|
+
versionReexec: 'CafeKit {v} に切り替えます…',
|
|
229
|
+
versionFetchFailed: 'バージョン一覧を取得できませんでした。デフォルトメニューを表示します。'
|
|
194
230
|
},
|
|
195
231
|
|
|
196
232
|
// ─── Tiếng Việt ─────────────────────────────────────────────────────────────
|
|
@@ -225,6 +261,11 @@ const MESSAGES = {
|
|
|
225
261
|
addressingSet: '{name} sẽ gọi bạn là "{addr}"',
|
|
226
262
|
addressingInvalid: 'Chỉ nhập chữ cái — bỏ qua thiết lập xưng hô.',
|
|
227
263
|
|
|
264
|
+
// Addressing update
|
|
265
|
+
addressingUpdatePrompt: 'Bạn đang được gọi là "{name}". Bạn có muốn đổi không?',
|
|
266
|
+
keepAddressingOption: 'Giữ "{name}"',
|
|
267
|
+
changeAddressingOption:'Đổi tên khác',
|
|
268
|
+
|
|
228
269
|
skillDepsConfirm: 'Cài dependencies cho skill ngay? (Python venv, pip, npm, Chromium — mất vài phút)',
|
|
229
270
|
skillsSkipped: 'Đã bỏ qua dependencies skill.\n Cài sau bằng lệnh: npx @haposoft/cafekit --with-skills-deps',
|
|
230
271
|
venvCreating: 'Đang thiết lập môi trường Python',
|
|
@@ -243,6 +284,7 @@ const MESSAGES = {
|
|
|
243
284
|
chromiumInstalling: 'Đang tải Chromium cho chrome-devtools',
|
|
244
285
|
chromiumReady: 'Chromium đã sẵn sàng',
|
|
245
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',
|
|
246
288
|
optionalToolsTitle: 'Công cụ tuỳ chọn cần cài thủ công',
|
|
247
289
|
|
|
248
290
|
summaryTitle: 'Cài đặt hoàn tất',
|
|
@@ -275,7 +317,19 @@ const MESSAGES = {
|
|
|
275
317
|
updateOption: 'Cập nhật lên {v}',
|
|
276
318
|
reinstallOption: 'Cài lại (ghi đè file được quản lý)',
|
|
277
319
|
reinstallCurrentOption: 'Cài lại {v}',
|
|
278
|
-
skipOption: 'Bỏ qua (thoát)'
|
|
320
|
+
skipOption: 'Bỏ qua (thoát)',
|
|
321
|
+
|
|
322
|
+
// Platform/language auto-skip when already installed
|
|
323
|
+
installingFor: 'Cài cho: {names}',
|
|
324
|
+
langKept: 'Ngôn ngữ: {lang} (đã lưu — bỏ qua chọn lại)',
|
|
325
|
+
platformKept: 'Nền tảng: {names} (đã lưu — bỏ qua chọn lại)',
|
|
326
|
+
|
|
327
|
+
// Version picker
|
|
328
|
+
versionPickPrompt: 'Chọn phiên bản CafeKit để cài:',
|
|
329
|
+
versionPickCurrent: '(đang cài)',
|
|
330
|
+
versionPickLatest: '(mới nhất)',
|
|
331
|
+
versionReexec: 'Đang chuyển sang cài CafeKit {v}…',
|
|
332
|
+
versionFetchFailed: 'Không lấy được danh sách phiên bản. Dùng menu mặc định.'
|
|
279
333
|
}
|
|
280
334
|
};
|
|
281
335
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm registry queries for the installer version picker.
|
|
3
|
+
*
|
|
4
|
+
* Intentionally standalone — no imports from other installer lib files
|
|
5
|
+
* to avoid circular dependencies. Timeout is short (3 s) so a slow/offline
|
|
6
|
+
* network never blocks the install flow; callers must handle a null result.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const https = require('https');
|
|
12
|
+
|
|
13
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@haposoft/cafekit';
|
|
14
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
15
|
+
const MAX_VERSIONS = 5;
|
|
16
|
+
|
|
17
|
+
/** Sort version strings descending (newest first), semver-ish. */
|
|
18
|
+
function sortDesc(versions) {
|
|
19
|
+
return [...versions].sort((a, b) => {
|
|
20
|
+
const toNum = (v) => String(v).split('.').map((n) => parseInt(n, 10) || 0);
|
|
21
|
+
const [aN, bN] = [toNum(a), toNum(b)];
|
|
22
|
+
for (let i = 0; i < 3; i++) {
|
|
23
|
+
const d = (bN[i] || 0) - (aN[i] || 0);
|
|
24
|
+
if (d !== 0) return d;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch up to MAX_VERSIONS recent published versions from the npm registry.
|
|
32
|
+
* Returns an object `{ versions: string[], latest: string }` or null on any error.
|
|
33
|
+
*/
|
|
34
|
+
function fetchRecentVersions() {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const req = https.get(
|
|
37
|
+
REGISTRY_URL,
|
|
38
|
+
{ timeout: FETCH_TIMEOUT_MS, headers: { Accept: 'application/json' } },
|
|
39
|
+
(res) => {
|
|
40
|
+
let body = '';
|
|
41
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
42
|
+
res.on('end', () => {
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(body);
|
|
45
|
+
const allVersions = Object.keys(data.versions || {});
|
|
46
|
+
if (allVersions.length === 0) { resolve(null); return; }
|
|
47
|
+
const sorted = sortDesc(allVersions);
|
|
48
|
+
const latest = (data['dist-tags'] && data['dist-tags'].latest) || sorted[0];
|
|
49
|
+
resolve({ versions: sorted.slice(0, MAX_VERSIONS), latest });
|
|
50
|
+
} catch {
|
|
51
|
+
resolve(null);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
req.on('error', () => resolve(null));
|
|
57
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { fetchRecentVersions };
|
package/bin/lib/skill-deps.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
101
|
+
const opts = { cwd: scriptsDir, stdio: stdioOpt };
|
|
102
|
+
if (interactive) opts.encoding = 'utf8';
|
|
103
|
+
return run('node', [installMjs], opts).ok;
|
|
97
104
|
}
|
|
98
|
-
|
|
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,
|
package/bin/lib/version-check.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { PLATFORMS, packageJson } = require('./context');
|
|
16
|
+
const { fetchRecentVersions } = require('./registry');
|
|
16
17
|
|
|
17
18
|
/** Simple semver-ish comparison. Returns -1 | 0 | 1. */
|
|
18
19
|
function cmpVersion(a, b) {
|
|
@@ -43,6 +44,7 @@ function getInstalledVersion(platformKey) {
|
|
|
43
44
|
/**
|
|
44
45
|
* Check versions for all selected platforms and decide whether to proceed.
|
|
45
46
|
* Sets ctx.cancelled on same-version (without --force) or unconfirmed downgrade.
|
|
47
|
+
* Sets ctx.isUpdate = true when upgrading from an older version.
|
|
46
48
|
* Shows interactive prompt when version differs, asking user what to do.
|
|
47
49
|
*/
|
|
48
50
|
async function checkVersions(ctx) {
|
|
@@ -60,6 +62,7 @@ async function checkVersions(ctx) {
|
|
|
60
62
|
|
|
61
63
|
// All fresh installs - skip version check, proceed normally
|
|
62
64
|
if (platformsWithInstall.length === 0) {
|
|
65
|
+
ctx.isUpdate = false;
|
|
63
66
|
return ctx;
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -73,6 +76,8 @@ async function checkVersions(ctx) {
|
|
|
73
76
|
|
|
74
77
|
// ── Same version ───────────────────────────────────────────────────────────
|
|
75
78
|
if (cmp === 0) {
|
|
79
|
+
ctx.isUpdate = false;
|
|
80
|
+
|
|
76
81
|
if (ctx.options.forceOverwrite) {
|
|
77
82
|
ctx.ui.info(ctx.t ? ctx.t('versionForceReinstall', { v: installedVersion }) : `Reinstalling CafeKit ${installedVersion} (--force-overwrite)...`);
|
|
78
83
|
return ctx;
|
|
@@ -111,13 +116,51 @@ async function checkVersions(ctx) {
|
|
|
111
116
|
|
|
112
117
|
// ── Upgrade available ───────────────────────────────────────────────────────
|
|
113
118
|
if (cmp < 0) {
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
ctx.isUpdate = true;
|
|
120
|
+
|
|
121
|
+
// --force-overwrite or non-interactive: proceed without asking
|
|
122
|
+
if (!ctx.interactive || ctx.options.forceOverwrite) {
|
|
116
123
|
return ctx;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
//
|
|
120
|
-
const
|
|
126
|
+
// Try to fetch recent versions for the picker; fall back to 3-option menu on failure
|
|
127
|
+
const registry = await fetchRecentVersions();
|
|
128
|
+
|
|
129
|
+
if (registry) {
|
|
130
|
+
// Build picker options: each version labelled with (installed) / (latest) hints
|
|
131
|
+
const pickerOptions = registry.versions.map((v) => {
|
|
132
|
+
let hint = '';
|
|
133
|
+
if (v === firstPlatform.installed) hint = ctx.t ? ctx.t('versionPickCurrent') : '(installed)';
|
|
134
|
+
else if (v === registry.latest) hint = ctx.t ? ctx.t('versionPickLatest') : '(latest)';
|
|
135
|
+
const label = hint ? `${v} ${hint}` : v;
|
|
136
|
+
return { value: v, label };
|
|
137
|
+
});
|
|
138
|
+
pickerOptions.push({ value: 'skip', label: ctx.t ? ctx.t('skipOption') : 'Skip (exit)' });
|
|
139
|
+
|
|
140
|
+
const picked = await ctx.ui.select({
|
|
141
|
+
message: ctx.t ? ctx.t('versionPickPrompt') : 'Select CafeKit version to install:',
|
|
142
|
+
options: pickerOptions
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (ctx.ui.isCancel(picked) || picked === 'skip') {
|
|
146
|
+
ctx.cancelled = true;
|
|
147
|
+
return ctx;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (picked !== incoming) {
|
|
151
|
+
// User chose a version different from the one currently running — signal re-exec
|
|
152
|
+
ctx.reexec = picked;
|
|
153
|
+
return ctx;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// User confirmed the incoming (latest) version — proceed normally
|
|
157
|
+
return ctx;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Offline or fetch failed: fall back to the classic 3-option menu
|
|
161
|
+
ctx.ui.warn(ctx.t ? ctx.t('versionFetchFailed') : 'Could not fetch version list. Using default menu.');
|
|
162
|
+
|
|
163
|
+
const fallbackOptions = [
|
|
121
164
|
{ value: 'update', label: ctx.t ? ctx.t('updateOption', { v: incoming }) : `Update to ${incoming}` },
|
|
122
165
|
{ value: 'reinstall', label: ctx.t ? ctx.t('reinstallCurrentOption', { v: firstPlatform.installed }) : `Reinstall ${firstPlatform.installed}` },
|
|
123
166
|
{ value: 'skip', label: ctx.t ? ctx.t('skipOption') : 'Skip (exit)' }
|
|
@@ -127,7 +170,7 @@ async function checkVersions(ctx) {
|
|
|
127
170
|
message: ctx.t
|
|
128
171
|
? ctx.t('versionUpgradePrompt', { from: firstPlatform.installed, to: incoming })
|
|
129
172
|
: `CafeKit ${firstPlatform.installed} → ${incoming}: Update available!`,
|
|
130
|
-
options
|
|
173
|
+
options: fallbackOptions
|
|
131
174
|
});
|
|
132
175
|
|
|
133
176
|
if (ctx.ui.isCancel(answer) || answer === 'skip') {
|
|
@@ -137,14 +180,16 @@ async function checkVersions(ctx) {
|
|
|
137
180
|
|
|
138
181
|
if (answer === 'reinstall') {
|
|
139
182
|
ctx.options.forceOverwrite = true;
|
|
183
|
+
ctx.isUpdate = false; // Reinstall treats as fresh install
|
|
140
184
|
}
|
|
141
185
|
|
|
142
|
-
// 'update' proceeds normally (no special flag needed)
|
|
143
186
|
return ctx;
|
|
144
187
|
}
|
|
145
188
|
|
|
146
189
|
// ── Downgrade ──────────────────────────────────────────────────────────────
|
|
147
190
|
if (cmp > 0) {
|
|
191
|
+
ctx.isUpdate = false;
|
|
192
|
+
|
|
148
193
|
const msg = ctx.t
|
|
149
194
|
? ctx.t('versionDowngrade', { from: firstPlatform.installed, to: incoming })
|
|
150
195
|
: `Downgrading ${firstPlatform.installed} → ${incoming}. This may remove features.`;
|
|
@@ -130,6 +130,34 @@ ${name} always addresses the user as "${userAddress}" throughout the conversatio
|
|
|
130
130
|
async function setupAddressing(ctx) {
|
|
131
131
|
if (!ctx.platforms.includes('claude')) return;
|
|
132
132
|
|
|
133
|
+
// Check if there's already an addressing section in CLAUDE.md
|
|
134
|
+
const claudeMdFile = path.join(process.cwd(), 'CLAUDE.md');
|
|
135
|
+
let existingName = null;
|
|
136
|
+
if (fs.existsSync(claudeMdFile)) {
|
|
137
|
+
const content = fs.readFileSync(claudeMdFile, 'utf8');
|
|
138
|
+
const match = content.match(/##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?always addresses the user as "([^"]+)"/);
|
|
139
|
+
if (match) {
|
|
140
|
+
existingName = match[1];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// For updates: ask if user wants to keep the existing name
|
|
145
|
+
if (existingName && ctx.isUpdate) {
|
|
146
|
+
const options = [
|
|
147
|
+
{ value: 'keep', label: ctx.t('keepAddressingOption', { addr: existingName }) || `Giữ "${existingName}"` },
|
|
148
|
+
{ value: 'change', label: ctx.t('changeAddressingOption') || 'Đổi tên mới' }
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const choice = await ctx.ui.select({
|
|
152
|
+
message: ctx.t('addressingUpdatePrompt') || `Bạn đang dùng "${existingName}" để xưng hô. Muốn đổi không?`,
|
|
153
|
+
options
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (ctx.ui.isCancel(choice)) return;
|
|
157
|
+
if (choice === 'keep') return;
|
|
158
|
+
// If 'change', continue to ask for new name
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
const answer = await ctx.ui.text(
|
|
134
162
|
{ message: ctx.t('addressingQuestion'), placeholder: ctx.t('addressingPlaceholder') },
|
|
135
163
|
''
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* or prompted. Sets ctx.platforms, or ctx.cancelled if the user backs out.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
9
11
|
const {
|
|
10
12
|
PLATFORMS,
|
|
11
13
|
detectPlatforms,
|
|
@@ -16,10 +18,34 @@ const { SUPPORTED, LANGUAGE_LABELS, OTHER_LABEL } = require('../lib/i18n');
|
|
|
16
18
|
|
|
17
19
|
const OTHER = '__other__';
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Read saved locale label from .claude/runtime.json (written by post-install).
|
|
23
|
+
* Returns the freeform label string (e.g. "Tiếng Việt") or null.
|
|
24
|
+
*/
|
|
25
|
+
function getInstalledLocale() {
|
|
26
|
+
const runtimeJson = path.join(process.cwd(), '.claude', 'runtime.json');
|
|
27
|
+
if (!fs.existsSync(runtimeJson)) return null;
|
|
28
|
+
try {
|
|
29
|
+
const data = JSON.parse(fs.readFileSync(runtimeJson, 'utf8'));
|
|
30
|
+
return (data && data.locale && typeof data.locale.responseLanguage === 'string')
|
|
31
|
+
? data.locale.responseLanguage
|
|
32
|
+
: null;
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
/** First step: pick the installer/UI language (interactive only). */
|
|
20
37
|
async function selectLanguage(ctx) {
|
|
21
38
|
if (!ctx.interactive || ctx.options.lang) return ctx;
|
|
22
39
|
|
|
40
|
+
// If already installed, restore saved locale and skip the prompt
|
|
41
|
+
const savedLocale = getInstalledLocale();
|
|
42
|
+
if (savedLocale) {
|
|
43
|
+
const code = Object.keys(LANGUAGE_LABELS).find((k) => LANGUAGE_LABELS[k] === savedLocale) || 'en';
|
|
44
|
+
ctx.setLang(code, savedLocale); // updates ctx.t to the saved language
|
|
45
|
+
ctx.ui.info(ctx.t('langKept', { lang: savedLocale }));
|
|
46
|
+
return ctx;
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
const options = [
|
|
24
50
|
...SUPPORTED.map((code) => ({ value: code, label: LANGUAGE_LABELS[code] })),
|
|
25
51
|
{ value: OTHER, label: OTHER_LABEL.en + ' / その他 / Ngôn ngữ khác' }
|
|
@@ -75,8 +101,27 @@ async function confirmAllDetected(ctx, detected) {
|
|
|
75
101
|
|
|
76
102
|
/**
|
|
77
103
|
* Resolve ctx.platforms via detection + prompts. Sets ctx.cancelled on backout.
|
|
104
|
+
* If cafekit.json exists (prior install), reads platform from it to skip prompt.
|
|
105
|
+
* Note: ctx.isUpdate is set later by checkVersions and cannot be used here.
|
|
78
106
|
*/
|
|
79
107
|
async function resolvePlatforms(ctx) {
|
|
108
|
+
// Skip platform prompt when a prior install is detected via cafekit.json
|
|
109
|
+
const savedPlatform = getInstalledPlatform();
|
|
110
|
+
if (savedPlatform) {
|
|
111
|
+
ctx.platforms = [savedPlatform];
|
|
112
|
+
const platformNames = PLATFORMS[savedPlatform].name;
|
|
113
|
+
ctx.ui.info(ctx.t('platformKept', { names: platformNames }));
|
|
114
|
+
|
|
115
|
+
if (ctx.options.forceOverwrite) {
|
|
116
|
+
ctx.ui.info(ctx.t('modeForce'));
|
|
117
|
+
} else if (ctx.dryRun) {
|
|
118
|
+
ctx.ui.info(ctx.t('modeDryRun'));
|
|
119
|
+
} else {
|
|
120
|
+
ctx.ui.info(ctx.t('modeInstall'));
|
|
121
|
+
}
|
|
122
|
+
return ctx;
|
|
123
|
+
}
|
|
124
|
+
|
|
80
125
|
let platforms = detectPlatforms();
|
|
81
126
|
|
|
82
127
|
if (platforms.length === 0) {
|
|
@@ -120,4 +165,28 @@ async function resolvePlatforms(ctx) {
|
|
|
120
165
|
return ctx;
|
|
121
166
|
}
|
|
122
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Get the installed platform from cafekit.json files.
|
|
170
|
+
* Returns the platform key (e.g. 'claude', 'opencode') or null.
|
|
171
|
+
*/
|
|
172
|
+
function getInstalledPlatform() {
|
|
173
|
+
const claudeJson = path.join(process.cwd(), '.claude', 'cafekit.json');
|
|
174
|
+
if (fs.existsSync(claudeJson)) {
|
|
175
|
+
try {
|
|
176
|
+
const data = JSON.parse(fs.readFileSync(claudeJson, 'utf8'));
|
|
177
|
+
if (data.platform) return data.platform;
|
|
178
|
+
} catch { /* ignore */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const opencodeJson = path.join(process.cwd(), '.opencode', 'cafekit.json');
|
|
182
|
+
if (fs.existsSync(opencodeJson)) {
|
|
183
|
+
try {
|
|
184
|
+
const data = JSON.parse(fs.readFileSync(opencodeJson, 'utf8'));
|
|
185
|
+
if (data.platform) return data.platform;
|
|
186
|
+
} catch { /* ignore */ }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
123
192
|
module.exports = { selectLanguage, promptPlatformSelection, resolvePlatforms };
|
|
@@ -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
|
-
|
|
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
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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',
|