@stacksjs/defaults 0.75.308 → 0.75.310

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.
@@ -15,6 +15,10 @@ Protocol (CDP) using only Bun's native `Bun.spawn`, `fetch`, and `WebSocket`.
15
15
 
16
16
  The driver lives at `storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts`. Run it with `bun`:
17
17
 
18
+ Keep `chrome.ts` beside the driver when copying the standalone skill. It is the
19
+ Chrome executable preparation shared with `@stacksjs/testing/browser` and uses
20
+ only Bun and Node APIs.
21
+
18
22
  ```bash
19
23
  bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts <command> <url> [flags]
20
24
  ```
@@ -23,6 +23,7 @@ import { spawn } from 'bun'
23
23
  import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
24
24
  import { tmpdir } from 'node:os'
25
25
  import { join } from 'node:path'
26
+ import { chromeHasUpdater, findChrome, prepareMacChrome } from './chrome'
26
27
 
27
28
  // ── Browser discovery ──────────────────────────────────────────────────────
28
29
 
@@ -49,6 +50,7 @@ function collectCandidates(): string[] {
49
50
  // DevTools target. Prefer an installed Chrome before spending their bounds.
50
51
  for (const bin of ['google-chrome-stable', 'google-chrome', 'chromium', 'chromium-browser', 'brave-browser', 'microsoft-edge', 'chrome'])
51
52
  add(which(bin))
53
+ add(findChrome())
52
54
 
53
55
  for (const p of [
54
56
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
@@ -199,17 +201,19 @@ export async function waitForBrowserPort(proc: { exitCode: number | null }, port
199
201
  }
200
202
 
201
203
  async function tryLaunch(browser: string): Promise<Session | null> {
204
+ const executable = chromeHasUpdater(browser) ? prepareMacChrome({ source: browser }).executable : browser
202
205
  const userDataDir = join(tmpdir(), `stacks-browse-${process.pid}-${Math.floor(Number(process.hrtime.bigint() % 1000000n))}`)
203
206
  mkdirSync(userDataDir, { recursive: true })
204
207
 
205
- const isHeadlessShell = /chrome-headless-shell|headless_shell/.test(browser)
208
+ const isHeadlessShell = /chrome-headless-shell|headless_shell/.test(executable)
206
209
  const proc = spawn([
207
- browser,
210
+ executable,
208
211
  ...(isHeadlessShell ? [] : ['--headless=new']),
209
212
  '--remote-debugging-port=0',
210
213
  `--user-data-dir=${userDataDir}`,
211
214
  '--no-first-run',
212
215
  '--no-default-browser-check',
216
+ ...(process.platform === 'darwin' ? ['--use-mock-keychain'] : []),
213
217
  '--disable-gpu',
214
218
  '--disable-dev-shm-usage',
215
219
  '--hide-scrollbars',
@@ -223,7 +227,7 @@ async function tryLaunch(browser: string): Promise<Session | null> {
223
227
  let port: number | null = null
224
228
  try {
225
229
  port = await waitForBrowserPort(proc, portFile, 15_000)
226
- if (port !== null) return { proc, port, userDataDir, browser }
230
+ if (port !== null) return { proc, port, userDataDir, browser: executable }
227
231
  }
228
232
  finally {
229
233
  if (port === null) {
@@ -0,0 +1,106 @@
1
+ // Shared Chrome executable preparation for the standalone skill and testing package.
2
+ import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import process from 'node:process'
6
+
7
+ const MAC_CHROMES = [
8
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
9
+ '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
10
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
11
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
12
+ ]
13
+ const PATH_CHROMES = ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']
14
+ const UPDATER_KEYS = ['KSProductID', 'KSUpdateURL', 'KSChannelID', 'KSVersion']
15
+ const PRIVATE_BUNDLE_ID = 'dev.stacks.testing.chrome'
16
+
17
+ /** The Chrome to drive: `CHROME_PATH`, else the first installed one found. Null when there is none. */
18
+ export function findChrome(): string | null {
19
+ if (process.env.CHROME_PATH) return process.env.CHROME_PATH
20
+ if (process.platform === 'darwin') {
21
+ const found = MAC_CHROMES.find(path => existsSync(path))
22
+ if (found) return found
23
+ }
24
+ for (const name of PATH_CHROMES) {
25
+ const found = Bun.which(name)
26
+ if (found) return found
27
+ }
28
+ return null
29
+ }
30
+
31
+ /** The `.app` bundle an executable inside `Contents/MacOS/` belongs to, or null. */
32
+ function appBundleOf(executable: string): string | null {
33
+ const match = /^(.*?\.app)\/Contents\/MacOS\/[^/]+$/.exec(executable)
34
+ return match ? match[1]! : null
35
+ }
36
+
37
+ function plist(info: string, command: string): { ok: boolean, out: string } {
38
+ const result = Bun.spawnSync(['/usr/libexec/PlistBuddy', '-c', command, info], { stdout: 'pipe', stderr: 'pipe' })
39
+ return { ok: result.exitCode === 0, out: result.stdout.toString().trim() }
40
+ }
41
+
42
+ /** True when this Chrome would be killed by GoogleUpdater (it still carries the updater's keys). */
43
+ export function chromeHasUpdater(executable: string): boolean {
44
+ const app = appBundleOf(executable)
45
+ if (!app || process.platform !== 'darwin') return false
46
+ const info = join(app, 'Contents/Info.plist')
47
+ return existsSync(info) && UPDATER_KEYS.some(key => plist(info, `Print :${key}`).ok)
48
+ }
49
+
50
+ export interface PreparedChrome {
51
+ /** The executable to launch. */
52
+ executable: string
53
+ /** The cloned `.app`. */
54
+ app: string
55
+ /** Reused from an earlier run rather than cloned now. */
56
+ reused: boolean
57
+ }
58
+
59
+ /**
60
+ * A private clone of a macOS Chrome that GoogleUpdater leaves alone (see the
61
+ * module docs): `cp -cR`, the `KS*` keys deleted from `Info.plist`, its own
62
+ * `CFBundleIdentifier`, `codesign --force --deep --sign -`. Kept in
63
+ * `directory` (default a per-version folder in the temp directory) and
64
+ * reused while its version matches the source's.
65
+ */
66
+ export function prepareMacChrome(options: { source?: string, directory?: string } = {}): PreparedChrome {
67
+ if (process.platform !== 'darwin') throw new Error('prepareMacChrome is for macOS; elsewhere launch Chrome as installed')
68
+ const source = options.source ?? MAC_CHROMES[0]!
69
+ const sourceApp = appBundleOf(source)
70
+ if (!sourceApp || !existsSync(sourceApp)) throw new Error(`No Chrome app bundle at ${source}`)
71
+ const sourceInfo = join(sourceApp, 'Contents/Info.plist')
72
+ const version = plist(sourceInfo, 'Print :CFBundleShortVersionString').out || 'unknown'
73
+ const executableName = plist(sourceInfo, 'Print :CFBundleExecutable').out || 'Google Chrome'
74
+ const directory = options.directory ?? join(tmpdir(), `stacks-chrome-${version.replace(/[^\w.-]/g, '_')}`)
75
+ const app = join(directory, 'Chrome.app')
76
+ const executable = join(app, 'Contents/MacOS', executableName)
77
+ const info = join(app, 'Contents/Info.plist')
78
+
79
+ if (existsSync(executable) && plist(info, 'Print :CFBundleIdentifier').out === PRIVATE_BUNDLE_ID
80
+ && plist(info, 'Print :CFBundleShortVersionString').out === version)
81
+ return { executable, app, reused: true }
82
+
83
+ // Build beside the final path and move it in, so a run killed half-way
84
+ // never leaves a half-signed app that a later run would reuse.
85
+ mkdirSync(directory, { recursive: true })
86
+ const staging = mkdtempSync(join(directory, 'staging-'))
87
+ const staged = join(staging, 'Chrome.app')
88
+ const run = (command: string[], what: string) => {
89
+ const result = Bun.spawnSync(command, { stdout: 'pipe', stderr: 'pipe' })
90
+ if (result.exitCode !== 0) throw new Error(`${what} failed: ${result.stderr.toString().trim()}`)
91
+ }
92
+ try {
93
+ run(['/bin/cp', '-cR', sourceApp, staged], 'Cloning Chrome')
94
+ const stagedInfo = join(staged, 'Contents/Info.plist')
95
+ for (const key of UPDATER_KEYS) plist(stagedInfo, `Delete :${key}`)
96
+ run(['/usr/libexec/PlistBuddy', '-c', `Set :CFBundleIdentifier ${PRIVATE_BUNDLE_ID}`, stagedInfo], 'Renaming the Chrome bundle')
97
+ run(['codesign', '--force', '--deep', '--sign', '-', staged], 'Re-signing Chrome')
98
+ rmSync(app, { recursive: true, force: true })
99
+ renameSync(staged, app)
100
+ }
101
+ finally {
102
+ rmSync(staging, { recursive: true, force: true })
103
+ }
104
+ return { executable, app, reused: false }
105
+ }
106
+
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.308",
5
+ "version": "0.75.310",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.308",
5
+ "version": "0.75.310",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@iconify-json/f7": "^1.2.2",
47
47
  "@iconify-json/hugeicons": "^1.2.35",
48
- "@stacksjs/mobile": "0.75.308",
48
+ "@stacksjs/mobile": "0.75.310",
49
49
  "@stacksjs/sanitizer": "0.2.691",
50
50
  "@stacksjs/stx": "0.2.691",
51
51
  "@xterm/addon-fit": "^0.11.0",