@testsmith/api-spector 0.3.3 → 0.3.4

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/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
+ const fs = require('fs')
5
+ const os = require('os')
4
6
  const path = require('path')
5
- const { spawn } = require('child_process')
7
+ const { spawn, spawnSync } = require('child_process')
6
8
 
7
9
  const [, , cmd = 'ui', ...rest] = process.argv
8
10
 
@@ -61,23 +63,140 @@ if (!command) {
61
63
  process.exit(1)
62
64
  }
63
65
 
64
- // ui: spawn electron with the app dir
65
- if (command.runner === 'electron') {
66
- // `require('electron')` throws if electron's postinstall didn't download
67
- // the platform binary (common behind corporate proxies on Windows: the
68
- // npm install completes but the GitHub Releases download is blocked).
69
- // The raw stack trace is intimidating; turn it into actionable steps.
70
- let electron
66
+ // ─── Electron binary self-repair ─────────────────────────────────────────────
67
+ //
68
+ // `require('electron')` throws when the postinstall didn't download the
69
+ // platform binary. On corporate machines the ~100 MB zip often IS fully
70
+ // downloaded into electron's cache it's the extraction into node_modules
71
+ // that got interrupted (antivirus, killed install, …). In that case we can
72
+ // repair the install ourselves, using the OS's own unzip tooling (which is
73
+ // not affected by whatever broke Node's extractor), and launch anyway.
74
+
75
+ // Mirrors getPlatformPath() in electron's install.js — path.txt must contain
76
+ // exactly this value.
77
+ function electronPlatformPath() {
78
+ switch (process.platform) {
79
+ case 'win32': return 'electron.exe'
80
+ case 'darwin':
81
+ case 'mas': return 'Electron.app/Contents/MacOS/Electron'
82
+ default: return 'electron'
83
+ }
84
+ }
85
+
86
+ // Default cache roots used by @electron/get, per OS. `electron_config_cache`
87
+ // overrides them (same variable electron's own installer respects).
88
+ function electronCacheDirs() {
89
+ if (process.env.electron_config_cache) return [process.env.electron_config_cache]
90
+ const home = os.homedir()
91
+ if (process.platform === 'win32') {
92
+ return [path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'electron', 'Cache')]
93
+ }
94
+ if (process.platform === 'darwin') {
95
+ return [path.join(home, 'Library', 'Caches', 'electron')]
96
+ }
97
+ return [path.join(process.env.XDG_CACHE_HOME || path.join(home, '.cache'), 'electron')]
98
+ }
99
+
100
+ // Find a fully-downloaded electron zip for this version/platform/arch in the
101
+ // cache. Entries live in hash-named subdirectories; a real zip is >20 MB —
102
+ // anything smaller is a truncated download or a proxy's HTML block page.
103
+ function findCachedElectronZip(version) {
104
+ const wanted = `electron-v${version}-${process.platform}-${process.arch}.zip`
105
+ for (const root of electronCacheDirs()) {
106
+ let entries
107
+ try { entries = fs.readdirSync(root) } catch { continue }
108
+ for (const entry of ['', ...entries]) {
109
+ const candidate = path.join(root, entry, wanted)
110
+ try {
111
+ if (fs.statSync(candidate).size > 20 * 1024 * 1024) return candidate
112
+ } catch { /* not there — keep looking */ }
113
+ }
114
+ }
115
+ return null
116
+ }
117
+
118
+ // Extract with OS-native tools: PowerShell on Windows, ditto on macOS (it
119
+ // preserves the symlinks inside Electron.app, plain unzip does not), unzip on
120
+ // Linux. Deliberately NOT extract-zip — when we get here, that path already
121
+ // failed once on this machine.
122
+ function extractZipNative(zip, destDir) {
123
+ let r
124
+ if (process.platform === 'win32') {
125
+ r = spawnSync('powershell.exe', [
126
+ '-NoProfile', '-NonInteractive', '-Command',
127
+ `Expand-Archive -LiteralPath "${zip}" -DestinationPath "${destDir}" -Force`,
128
+ ], { stdio: 'ignore' })
129
+ } else if (process.platform === 'darwin') {
130
+ r = spawnSync('ditto', ['-x', '-k', zip, destDir], { stdio: 'ignore' })
131
+ } else {
132
+ r = spawnSync('unzip', ['-o', '-q', zip, '-d', destDir], { stdio: 'ignore' })
133
+ }
134
+ return Boolean(r && r.status === 0)
135
+ }
136
+
137
+ // Attempt to rebuild node_modules/electron/dist from a cached zip.
138
+ // Returns 'repaired', 'no-zip', or a { zip } object when extraction failed.
139
+ function tryRepairElectron() {
140
+ let pkgPath
141
+ try { pkgPath = require.resolve('electron/package.json') } catch { return 'no-zip' }
142
+ const electronDir = path.dirname(pkgPath)
143
+ const version = require(pkgPath).version
144
+ const zip = findCachedElectronZip(version)
145
+ if (!zip) return 'no-zip'
146
+
147
+ console.error(` Electron ${version} was already downloaded — repairing the`)
148
+ console.error(' installation from the local cache...')
149
+ const distDir = path.join(electronDir, 'dist')
150
+ try { fs.rmSync(distDir, { recursive: true, force: true }) } catch { /* best effort */ }
151
+ if (!extractZipNative(zip, distDir) || !fs.existsSync(path.join(distDir, electronPlatformPath()))) {
152
+ return { zip }
153
+ }
154
+ fs.writeFileSync(path.join(electronDir, 'path.txt'), electronPlatformPath())
155
+ console.error(' Repaired.')
156
+ console.error('')
157
+ return 'repaired'
158
+ }
159
+
160
+ // Resolve the electron executable, classifying the failure modes.
161
+ function loadElectron() {
71
162
  try {
72
- electron = require('electron')
163
+ const electron = require('electron')
164
+ // path.txt can exist while dist/ is incomplete (interrupted extraction) —
165
+ // require() succeeds but points at a binary that isn't there.
166
+ if (typeof electron === 'string' && !fs.existsSync(electron)) {
167
+ return { status: 'binary-missing' }
168
+ }
169
+ return { status: 'ok', electron }
73
170
  } catch (err) {
74
171
  const msg = err && err.message ? err.message : String(err)
75
- const notInstalled = /Cannot find module 'electron'/i.test(msg)
76
- const binaryMissing = /Electron failed to install correctly/i.test(msg)
172
+ if (/Cannot find module 'electron'/i.test(msg)) return { status: 'not-installed' }
173
+ if (/Electron failed to install correctly/i.test(msg)) return { status: 'binary-missing' }
174
+ return { status: 'error', message: msg }
175
+ }
176
+ }
177
+
178
+ const TROUBLESHOOTING_URL =
179
+ 'https://github.com/testsmith-io/api-spector/blob/main/docs/getting-started/troubleshooting.md'
180
+
181
+ // ui: spawn electron with the app dir
182
+ if (command.runner === 'electron') {
183
+ let loaded = loadElectron()
184
+ let failedZip = null
185
+
186
+ if (loaded.status === 'binary-missing') {
187
+ const repair = tryRepairElectron()
188
+ if (repair === 'repaired') {
189
+ loaded = loadElectron()
190
+ } else if (repair && repair.zip) {
191
+ failedZip = repair.zip
192
+ }
193
+ }
194
+
195
+ if (loaded.status !== 'ok') {
77
196
  console.error('')
78
197
  console.error(' API Spector — failed to launch the UI.')
79
198
  console.error('')
80
- if (notInstalled) {
199
+ if (loaded.status === 'not-installed') {
81
200
  console.error(' The electron package is not installed alongside API Spector.')
82
201
  console.error(' Versions 0.3.1 and 0.3.2 shipped without it by mistake.')
83
202
  console.error('')
@@ -89,39 +208,61 @@ if (command.runner === 'electron') {
89
208
  console.error('')
90
209
  console.error(' 2. Or keep this version and install electron yourself:')
91
210
  console.error(' npm install -D electron@31')
211
+ } else if (loaded.status === 'binary-missing') {
212
+ let electronDir = null
213
+ let version = '<version>'
214
+ try {
215
+ const pkgPath = require.resolve('electron/package.json')
216
+ electronDir = path.dirname(pkgPath)
217
+ version = require(pkgPath).version
218
+ } catch { /* keep placeholders */ }
219
+ const zipName = `electron-v${version}-${process.platform}-${process.arch}.zip`
220
+
221
+ if (failedZip) {
222
+ console.error(' Electron\'s binary is missing. A downloaded copy exists at')
223
+ console.error(` ${failedZip}`)
224
+ console.error(' but it could not be extracted — the file may be corrupt (delete')
225
+ console.error(' it and reinstall), or antivirus is blocking the extraction.')
226
+ } else {
227
+ console.error(' Electron is installed, but its platform binary is missing and no')
228
+ console.error(' usable download was found in the local cache. The download during')
229
+ console.error(' `npm install` was probably blocked.')
230
+ console.error('')
231
+ console.error(' Common causes on corporate machines:')
232
+ console.error('')
233
+ console.error(' - Proxy blocks github.com downloads. Note: npm\'s proxy settings')
234
+ console.error(' do NOT apply to electron\'s downloader — it needs:')
235
+ console.error(' ELECTRON_GET_USE_PROXY=1')
236
+ console.error(' GLOBAL_AGENT_HTTPS_PROXY=http://your-proxy:port')
237
+ console.error(' then: npm install -D @testsmith/api-spector --force')
238
+ console.error('')
239
+ console.error(' - TLS-intercepting proxy (certificate errors): point Node at')
240
+ console.error(' your corporate root CA:')
241
+ console.error(' NODE_EXTRA_CA_CERTS=/path/to/corporate-root-ca.pem')
242
+ console.error('')
243
+ console.error(' - ELECTRON_SKIP_BINARY_DOWNLOAD=1 set machine-wide (some IT')
244
+ console.error(' images do this) — unset it and reinstall.')
245
+ }
92
246
  console.error('')
93
- console.error(' CLI subcommands (run / mock / record / contract / wsdl) do not')
94
- console.error(' need electron and work even while this is broken.')
95
- } else if (binaryMissing) {
96
- const installDir = path.dirname(__dirname)
97
- console.error(' Electron is installed, but its platform binary is missing — the')
98
- console.error(' download during `npm install` did not complete (often a proxy or')
99
- console.error(' firewall blocking github.com / electronjs.org).')
100
- console.error('')
101
- console.error(' Fix options (try in order):')
102
- console.error('')
103
- console.error(' 1. Reinstall and force the postinstall script to run:')
104
- console.error(' npm install -D @testsmith/api-spector --force')
105
- console.error(' (use -g instead of -D if you installed globally)')
106
- console.error('')
107
- console.error(' 2. Behind a proxy? Set npm + electron mirrors and reinstall:')
108
- console.error(' npm config set proxy http://your-proxy:port')
109
- console.error(' npm config set https-proxy http://your-proxy:port')
110
- console.error(' set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/')
111
- console.error(' npm install -D @testsmith/api-spector --force')
112
- console.error('')
113
- console.error(' 3. Re-run electron\'s postinstall manually:')
114
- console.error(` cd "${path.join(installDir, 'node_modules', 'electron')}"`)
115
- console.error(' node install.js')
116
- console.error('')
117
- console.error(' CLI subcommands (run / mock / record / contract / wsdl) do not')
118
- console.error(' need the UI binary and should work even while this is broken.')
247
+ console.error(' Manual fix (works without any of the above): download')
248
+ console.error(` https://github.com/electron/electron/releases/download/v${version}/${zipName}`)
249
+ console.error(' in a browser, extract ALL of it into:')
250
+ console.error(` ${electronDir ? path.join(electronDir, 'dist') : '<node_modules>/electron/dist'}`)
251
+ console.error(` and create a file "path.txt" next to "dist" containing exactly:`)
252
+ console.error(` ${electronPlatformPath()}`)
119
253
  } else {
120
- console.error(` ${msg}`)
254
+ console.error(` ${loaded.message}`)
121
255
  }
122
256
  console.error('')
257
+ console.error(' CLI subcommands (run / mock / record / contract / wsdl) do not')
258
+ console.error(' need the UI binary and work even while this is broken.')
259
+ console.error('')
260
+ console.error(` Full troubleshooting guide: ${TROUBLESHOOTING_URL}`)
261
+ console.error('')
123
262
  process.exit(1)
124
263
  }
264
+
265
+ const electron = loaded.electron
125
266
  const appDir = path.join(__dirname, '..')
126
267
  // Forward the user's cwd so the main process can decide whether to open a
127
268
  // workspace in this folder, or fall through to the welcome screen. Without
@@ -393,7 +393,7 @@ async function main() {
393
393
  if (envName && !env) {
394
394
  console.warn(cliCommon.color(`Warning: environment "${envName}" not found. Running without environment.`, cliCommon.C.yellow));
395
395
  }
396
- const version = `v${"0.3.3"}`;
396
+ const version = `v${"0.3.4"}`;
397
397
  console.log("");
398
398
  console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
399
399
  console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));