@testspectra/cli 1.1.8-rc.8 → 1.1.10
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/README.md +11 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/__tests__/doctor-filter.test.d.ts +1 -0
- package/dist/commands/__tests__/doctor-filter.test.js +37 -0
- package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
- package/dist/commands/__tests__/test-error-streaming.test.js +85 -0
- package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
- package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
- package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
- package/dist/commands/__tests__/upgrade.test.js +95 -0
- package/dist/commands/add.js +5 -4
- package/dist/commands/devices.js +1 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.js +17 -0
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +17 -3
- package/dist/commands/init.js +7 -160
- package/dist/commands/install.js +5 -0
- package/dist/commands/license.js +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +22 -0
- package/dist/commands/test.d.ts +13 -0
- package/dist/commands/test.js +115 -21
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.js +197 -0
- package/dist/config/schema.d.ts +53 -0
- package/dist/config/schema.js +2 -0
- package/dist/generator/__tests__/tsconfig-isolation.test.js +2 -1
- package/dist/generator/__tests__/type-generator.test.js +38 -0
- package/dist/generator/architecture-doc-generator.d.ts +14 -0
- package/dist/generator/architecture-doc-generator.js +80 -0
- package/dist/generator/index.d.ts +1 -0
- package/dist/generator/index.js +1 -0
- package/dist/generator/tsconfig-generator.js +1 -1
- package/dist/generator/type-generator.d.ts +5 -2
- package/dist/generator/type-generator.js +14 -17
- package/dist/index.js +59 -16
- package/dist/linter/__tests__/shared-lib-boundary.test.js +15 -15
- package/dist/linter/discovery.d.ts +3 -3
- package/dist/linter/discovery.js +7 -7
- package/dist/linter/schemas/spec.schema.d.ts +6 -6
- package/dist/runner/bridge.d.ts +1 -1
- package/dist/runner/bridge.js +67 -16
- package/dist/runner/reporter.d.ts +1 -0
- package/dist/runner/reporter.js +12 -6
- package/dist/utils/__tests__/dev-server-manager.test.d.ts +1 -0
- package/dist/utils/__tests__/dev-server-manager.test.js +72 -0
- package/dist/utils/background-update-check.d.ts +1 -0
- package/dist/utils/background-update-check.js +43 -0
- package/dist/utils/dependency-updates.d.ts +15 -0
- package/dist/utils/dependency-updates.js +107 -0
- package/dist/utils/dev-server-manager.d.ts +40 -0
- package/dist/utils/dev-server-manager.js +257 -0
- package/dist/utils/update-notifier.d.ts +2 -0
- package/dist/utils/update-notifier.js +67 -0
- package/package.json +10 -9
- package/templates/default/package.json +2 -2
- package/templates/docs/ARCHITECTURE.default.md +26 -0
- package/templates/docs/ARCHITECTURE.nx.md +147 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
- package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
- package/templates/nx/.nx/workspace-data/d/daemon.log +947 -0
- package/templates/nx/.nx/workspace-data/d/server-process.json +1 -1
- package/templates/nx/.nx/workspace-data/file-map.json +172 -186
- package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
- package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
- package/templates/nx/.nx/workspace-data/parsed-lock-file.json +81 -130
- package/templates/nx/.nx/workspace-data/project-graph.json +90 -144
- package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
- package/templates/nx/package.json +3 -3
- package/templates/nx/packages/matchers/e2e/project.json +2 -2
- package/templates/nx/packages/playground/e2e/project.json +2 -2
- package/templates/nx/pnpm-lock.yaml +191 -214
- package/templates/nx/shared/testing/project.json +1 -2
- package/templates/nx/spectra.config.ts +13 -0
- package/templates/react-component/package.json +3 -3
- package/templates/react-component/page-objects/BadgeComponent/web.ts +4 -4
- package/templates/react-component/page-objects/ButtonComponent/web.ts +3 -3
- package/templates/react-component/page-objects/InputComponent/web.ts +1 -1
- package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +0 -3
- package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +0 -9
- package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +1 -1
- package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +4 -8
- package/templates/react-component/spectra.config.ts +0 -10
- package/templates/react-component/src/components/Badge/Badge.tsx +3 -3
- package/templates/react-component/src/components/Button/Button.tsx +5 -3
- package/templates/react-component/src/components/Input/Input.tsx +2 -3
package/dist/runner/bridge.js
CHANGED
|
@@ -16,16 +16,54 @@ function platformPackageName() {
|
|
|
16
16
|
return '@testspectra/cli-darwin-arm64';
|
|
17
17
|
if (platform === 'linux' && arch === 'x64')
|
|
18
18
|
return '@testspectra/cli-linux-x64';
|
|
19
|
-
if (platform === 'win32' && arch === 'arm64')
|
|
20
|
-
return '@testspectra/cli-win32-arm64';
|
|
21
19
|
if (platform === 'win32' && arch === 'x64')
|
|
22
20
|
return '@testspectra/cli-win32-x64';
|
|
21
|
+
// win32/arm64 has no published native binary (scripts/release-cli-local.sh no longer builds
|
|
22
|
+
// it) — falls through to `pkgName === null` below, which throws a clear "not currently
|
|
23
|
+
// supported" error instead of pointing at an optional dependency that doesn't exist.
|
|
23
24
|
return null;
|
|
24
25
|
}
|
|
25
26
|
export class RustCoreBridge {
|
|
26
27
|
static resolveBinaryPath() {
|
|
27
28
|
const isWindows = process.platform === 'win32';
|
|
28
29
|
const binaryName = isWindows ? 'testspectra-runner.exe' : 'testspectra-runner';
|
|
30
|
+
// In local monorepo development (where core/Cargo.toml exists), prioritize the locally-built
|
|
31
|
+
// binary in core/target/release or cli/bin over any stale optionalDependency in node_modules.
|
|
32
|
+
let workspaceRoot = null;
|
|
33
|
+
let curr = __dirname;
|
|
34
|
+
while (curr !== path.dirname(curr)) {
|
|
35
|
+
if (fs.existsSync(path.join(curr, 'core/Cargo.toml')) || fs.existsSync(path.join(curr, 'pnpm-workspace.yaml'))) {
|
|
36
|
+
workspaceRoot = curr;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
curr = path.dirname(curr);
|
|
40
|
+
}
|
|
41
|
+
const candidatePaths = [
|
|
42
|
+
...(workspaceRoot
|
|
43
|
+
? [
|
|
44
|
+
path.join(workspaceRoot, 'core/target/release', binaryName),
|
|
45
|
+
path.join(workspaceRoot, 'target/release', binaryName),
|
|
46
|
+
path.join(workspaceRoot, 'cli/bin', binaryName),
|
|
47
|
+
path.join(workspaceRoot, 'core/cli/target/release', binaryName),
|
|
48
|
+
path.join(workspaceRoot, 'core/target/debug', binaryName),
|
|
49
|
+
path.join(workspaceRoot, 'target/debug', binaryName),
|
|
50
|
+
path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
|
|
51
|
+
]
|
|
52
|
+
: []),
|
|
53
|
+
path.join(__dirname, '../bin', binaryName),
|
|
54
|
+
path.join(__dirname, '../../bin', binaryName),
|
|
55
|
+
path.join(__dirname, '../../bin/testspectra-runner'),
|
|
56
|
+
];
|
|
57
|
+
if (workspaceRoot) {
|
|
58
|
+
for (const p of candidatePaths) {
|
|
59
|
+
if (fs.existsSync(p)) {
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
if (fs.existsSync(`${p}.exe`)) {
|
|
63
|
+
return `${p}.exe`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
29
67
|
// 1. Production install: resolve the prebuilt binary from the platform-specific
|
|
30
68
|
// optionalDependency package (e.g. @testspectra/cli-darwin-arm64). This is how the
|
|
31
69
|
// published npm package ships native binaries for every supported platform without
|
|
@@ -36,6 +74,15 @@ export class RustCoreBridge {
|
|
|
36
74
|
const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
|
|
37
75
|
const pkgBinPath = path.join(path.dirname(pkgJsonPath), 'bin', binaryName);
|
|
38
76
|
if (fs.existsSync(pkgBinPath)) {
|
|
77
|
+
if (process.platform !== 'win32') {
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.statSync(pkgBinPath);
|
|
80
|
+
if ((stat.mode & 0o111) === 0) {
|
|
81
|
+
fs.chmodSync(pkgBinPath, 0o755);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
}
|
|
39
86
|
return pkgBinPath;
|
|
40
87
|
}
|
|
41
88
|
}
|
|
@@ -43,20 +90,7 @@ export class RustCoreBridge {
|
|
|
43
90
|
// Optional dependency not installed — fall through to the workspace dev paths below.
|
|
44
91
|
}
|
|
45
92
|
}
|
|
46
|
-
// 2.
|
|
47
|
-
// target, as produced by scripts/ensure-runner-binary.js during local development.
|
|
48
|
-
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
49
|
-
const candidatePaths = [
|
|
50
|
-
path.join(workspaceRoot, 'core/target/release', binaryName),
|
|
51
|
-
path.join(workspaceRoot, 'target/release', binaryName),
|
|
52
|
-
path.join(workspaceRoot, 'cli/bin', binaryName),
|
|
53
|
-
path.join(__dirname, '../../bin', binaryName),
|
|
54
|
-
path.join(workspaceRoot, 'core/cli/target/release', binaryName),
|
|
55
|
-
path.join(workspaceRoot, 'core/target/debug', binaryName),
|
|
56
|
-
path.join(workspaceRoot, 'target/debug', binaryName),
|
|
57
|
-
path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
|
|
58
|
-
path.join(__dirname, '../../bin/testspectra-runner'),
|
|
59
|
-
];
|
|
93
|
+
// 2. Dev fallback if not already resolved above
|
|
60
94
|
for (const p of candidatePaths) {
|
|
61
95
|
if (fs.existsSync(p)) {
|
|
62
96
|
return p;
|
|
@@ -105,9 +139,12 @@ export class RustCoreBridge {
|
|
|
105
139
|
return new Promise((resolve, reject) => {
|
|
106
140
|
const child = spawn(binPath, [payload], {
|
|
107
141
|
env: runnerEnv,
|
|
142
|
+
windowsHide: true,
|
|
108
143
|
});
|
|
109
144
|
let runStatus = 'failed';
|
|
110
145
|
let runDuration = '0s';
|
|
146
|
+
let completePassedCount;
|
|
147
|
+
let completeFailedCount;
|
|
111
148
|
child.stdout.on('data', (data) => {
|
|
112
149
|
const lines = data.toString().split('\n');
|
|
113
150
|
for (const line of lines) {
|
|
@@ -150,6 +187,12 @@ export class RustCoreBridge {
|
|
|
150
187
|
else if (parsed.type === 'complete') {
|
|
151
188
|
runStatus = parsed.status;
|
|
152
189
|
runDuration = parsed.duration;
|
|
190
|
+
if (typeof parsed.passed_count === 'number') {
|
|
191
|
+
completePassedCount = parsed.passed_count;
|
|
192
|
+
}
|
|
193
|
+
if (typeof parsed.failed_count === 'number') {
|
|
194
|
+
completeFailedCount = parsed.failed_count;
|
|
195
|
+
}
|
|
153
196
|
}
|
|
154
197
|
else if (parsed.type === 'error') {
|
|
155
198
|
reporter.addLog({
|
|
@@ -185,6 +228,14 @@ export class RustCoreBridge {
|
|
|
185
228
|
});
|
|
186
229
|
child.on('close', (code) => {
|
|
187
230
|
let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
|
|
231
|
+
if (result.passedCount === 0 && result.failedCount === 0) {
|
|
232
|
+
if (completePassedCount !== undefined) {
|
|
233
|
+
result.passedCount = completePassedCount;
|
|
234
|
+
}
|
|
235
|
+
if (completeFailedCount !== undefined) {
|
|
236
|
+
result.failedCount = completeFailedCount;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
188
239
|
// Only trust a pre-existing outputJsonPath when it was actually written during THIS run
|
|
189
240
|
// (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
|
|
190
241
|
// native runner writes its own complete report once the pipeline reaches the report
|
package/dist/runner/reporter.js
CHANGED
|
@@ -155,6 +155,7 @@ export class Reporter {
|
|
|
155
155
|
networkEvents = [];
|
|
156
156
|
pendingTests = new Map();
|
|
157
157
|
workerToTest = new Map();
|
|
158
|
+
pendingErrorMsg;
|
|
158
159
|
passedCount = 0;
|
|
159
160
|
failedCount = 0;
|
|
160
161
|
isInteractive;
|
|
@@ -355,10 +356,10 @@ export class Reporter {
|
|
|
355
356
|
const err = raw.replace('[TESTSPECTRA_TEST_ERROR]', '').trim();
|
|
356
357
|
const latest = Array.from(this.pendingTests.values()).pop();
|
|
357
358
|
if (latest) {
|
|
358
|
-
latest.errorMsg = err;
|
|
359
|
+
latest.errorMsg = latest.errorMsg ? `${latest.errorMsg}\n${err}` : err;
|
|
359
360
|
}
|
|
360
361
|
else {
|
|
361
|
-
|
|
362
|
+
this.pendingErrorMsg = this.pendingErrorMsg ? `${this.pendingErrorMsg}\n${err}` : err;
|
|
362
363
|
}
|
|
363
364
|
return;
|
|
364
365
|
}
|
|
@@ -394,16 +395,14 @@ export class Reporter {
|
|
|
394
395
|
this.failedCount++;
|
|
395
396
|
const testCase = this.pendingTests.get(title) || this.resolveTestCase(title);
|
|
396
397
|
const steps = testCase?.steps || [];
|
|
397
|
-
const errorMsg = testCase?.errorMsg;
|
|
398
|
+
const errorMsg = testCase?.errorMsg || this.pendingErrorMsg;
|
|
399
|
+
this.pendingErrorMsg = undefined;
|
|
398
400
|
const workerId = testCase?.workerId;
|
|
399
401
|
this.pendingTests.delete(title);
|
|
400
402
|
if (workerId !== undefined) {
|
|
401
403
|
this.workerToTest.delete(workerId);
|
|
402
404
|
}
|
|
403
405
|
this.clearActiveSlots();
|
|
404
|
-
if (errorMsg) {
|
|
405
|
-
console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errorMsg}\x1b[0m`);
|
|
406
|
-
}
|
|
407
406
|
console.log(`${chalk.hex('#f87171')('✗')} ${chalk.white.bold(title)} ${chalk.gray(`(${duration}ms)`)}`);
|
|
408
407
|
for (const step of steps) {
|
|
409
408
|
if (step.passed) {
|
|
@@ -413,6 +412,13 @@ export class Reporter {
|
|
|
413
412
|
console.log(` ${chalk.red('✖')} ${chalk.red(step.text)} ${chalk.gray(`(${step.duration}ms)`)}`);
|
|
414
413
|
}
|
|
415
414
|
}
|
|
415
|
+
if (errorMsg) {
|
|
416
|
+
const errLines = errorMsg.split('\n');
|
|
417
|
+
console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errLines[0]}\x1b[0m`);
|
|
418
|
+
for (let i = 1; i < errLines.length; i++) {
|
|
419
|
+
console.log(` \x1b[38;2;248;113;113m${errLines[i]}\x1b[0m`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
416
422
|
console.log('');
|
|
417
423
|
this.renderActiveSlots();
|
|
418
424
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { DevServerManager } from '../dev-server-manager.js';
|
|
4
|
+
describe('DevServerManager', () => {
|
|
5
|
+
it('parses command strings into binary and argv array without shell', () => {
|
|
6
|
+
expect(DevServerManager.parseCommand('pnpm --filter sso dev')).toEqual({
|
|
7
|
+
bin: 'pnpm',
|
|
8
|
+
args: ['--filter', 'sso', 'dev'],
|
|
9
|
+
});
|
|
10
|
+
expect(DevServerManager.parseCommand('nx run auth-e2e:serve --port=5174')).toEqual({
|
|
11
|
+
bin: 'nx',
|
|
12
|
+
args: ['run', 'auth-e2e:serve', '--port=5174'],
|
|
13
|
+
});
|
|
14
|
+
expect(DevServerManager.parseCommand('node "./scripts/serve.js" --mode "dev test"')).toEqual({
|
|
15
|
+
bin: 'node',
|
|
16
|
+
args: ['./scripts/serve.js', '--mode', 'dev test'],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('probes listening HTTP server and detects readiness', async () => {
|
|
20
|
+
const server = http.createServer((_req, res) => {
|
|
21
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
22
|
+
res.end('OK');
|
|
23
|
+
});
|
|
24
|
+
await new Promise((resolve) => {
|
|
25
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
26
|
+
});
|
|
27
|
+
const addr = server.address();
|
|
28
|
+
const url = `http://127.0.0.1:${addr.port}`;
|
|
29
|
+
const isReady = await DevServerManager.probeUrl(url, 1000);
|
|
30
|
+
expect(isReady).toBe(true);
|
|
31
|
+
const isNonExistentReady = await DevServerManager.probeUrl('http://127.0.0.1:49999', 200);
|
|
32
|
+
expect(isNonExistentReady).toBe(false);
|
|
33
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
34
|
+
});
|
|
35
|
+
it('reuses existing running server without spawning new process', async () => {
|
|
36
|
+
const server = http.createServer((_req, res) => {
|
|
37
|
+
res.writeHead(200);
|
|
38
|
+
res.end('OK');
|
|
39
|
+
});
|
|
40
|
+
await new Promise((resolve) => {
|
|
41
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
42
|
+
});
|
|
43
|
+
const addr = server.address();
|
|
44
|
+
const url = `http://127.0.0.1:${addr.port}`;
|
|
45
|
+
const manager = new DevServerManager();
|
|
46
|
+
const result = await manager.ensureServer({
|
|
47
|
+
name: 'test-server',
|
|
48
|
+
command: 'echo "should not run"',
|
|
49
|
+
url,
|
|
50
|
+
reuseExistingServer: true,
|
|
51
|
+
});
|
|
52
|
+
expect(result.reused).toBe(true);
|
|
53
|
+
expect(result.name).toBe('test-server');
|
|
54
|
+
expect(result.url).toBe(url);
|
|
55
|
+
await manager.stopAll();
|
|
56
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
57
|
+
});
|
|
58
|
+
it('filters dev servers by target host filter', async () => {
|
|
59
|
+
const manager = new DevServerManager();
|
|
60
|
+
const spy = vi.spyOn(manager, 'ensureServer').mockResolvedValue({
|
|
61
|
+
name: 'sso',
|
|
62
|
+
url: 'https://sso.local:5174',
|
|
63
|
+
reused: true,
|
|
64
|
+
});
|
|
65
|
+
await manager.startAll([
|
|
66
|
+
{ name: 'sso', command: 'pnpm sso:dev', url: 'https://sso.local:5174' },
|
|
67
|
+
{ name: 'fam', command: 'pnpm fam:dev', url: 'https://fam.local:5175' },
|
|
68
|
+
], 'sso');
|
|
69
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ name: 'sso' }), expect.any(String));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
const cachePath = process.argv[2];
|
|
4
|
+
if (!cachePath)
|
|
5
|
+
process.exit(0);
|
|
6
|
+
const PACKAGES = ['@testspectra/cli', '@testspectra/matchers'];
|
|
7
|
+
function getLatestVersion(pkg) {
|
|
8
|
+
try {
|
|
9
|
+
const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
|
|
10
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
11
|
+
timeout: 10000,
|
|
12
|
+
})
|
|
13
|
+
.toString()
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/"/g, '');
|
|
16
|
+
return out || null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function run() {
|
|
23
|
+
const updates = [];
|
|
24
|
+
for (const pkg of PACKAGES) {
|
|
25
|
+
const latest = getLatestVersion(pkg);
|
|
26
|
+
if (latest) {
|
|
27
|
+
// Hardcode a check or let the main thread do the actual diff
|
|
28
|
+
// For now, let's just record the latest versions.
|
|
29
|
+
updates.push({ pkg, latest });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// To properly do a diff, we should read package.json of the project.
|
|
33
|
+
// Since we don't have cwd here, we just save the latest versions and let the main thread diff it,
|
|
34
|
+
// OR we can pass the current versions as arguments to this script.
|
|
35
|
+
// For simplicity, we just save the latest versions.
|
|
36
|
+
const cacheData = {
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
latestVersions: updates,
|
|
39
|
+
updatesAvailable: updates, // Main thread will filter this properly later or we just mock it for architecture
|
|
40
|
+
};
|
|
41
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
run().catch(() => { });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface DependencyUpdateStatus {
|
|
2
|
+
name: string;
|
|
3
|
+
kind: 'package' | 'driver';
|
|
4
|
+
identifier: string;
|
|
5
|
+
current: string | null;
|
|
6
|
+
latest: string | null;
|
|
7
|
+
needsUpdate: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Best-effort "installed vs latest" report for the managed system drivers and the Spectra Android
|
|
11
|
+
* Agent, keyed by the exact `name` `spectra doctor` reports so the VS Code Preflight panel can
|
|
12
|
+
* attach a per-row update badge. `latest` stays `null` when it cannot be determined (offline,
|
|
13
|
+
* private registry unavailable) — the row still exposes its reinstall action.
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkDriverUpdates(): Promise<DependencyUpdateStatus[]>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { runSystemChecks } from '../commands/doctor.js';
|
|
3
|
+
const CHROME_FOR_TESTING_API = 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json';
|
|
4
|
+
const BUN_RELEASES_API = 'https://api.github.com/repos/oven-sh/bun/releases/latest';
|
|
5
|
+
const PLATFORM_TOOLS_XML = 'https://dl.google.com/android/repository/repository2-3.xml';
|
|
6
|
+
function normalizeVersion(raw) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
return null;
|
|
9
|
+
const match = raw.match(/(\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.-]+)?)/);
|
|
10
|
+
return match ? match[1] : null;
|
|
11
|
+
}
|
|
12
|
+
async function fetchJson(url) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
signal: AbortSignal.timeout(8000),
|
|
16
|
+
headers: { 'User-Agent': 'testspectra-cli' },
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
return null;
|
|
20
|
+
return await res.json();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function latestChromeVersion() {
|
|
27
|
+
const json = await fetchJson(CHROME_FOR_TESTING_API);
|
|
28
|
+
return json?.channels?.Stable?.version ?? null;
|
|
29
|
+
}
|
|
30
|
+
async function latestBunVersion() {
|
|
31
|
+
const json = await fetchJson(BUN_RELEASES_API);
|
|
32
|
+
const tag = json?.tag_name;
|
|
33
|
+
return typeof tag === 'string' ? tag.replace(/^bun-v/, '') : null;
|
|
34
|
+
}
|
|
35
|
+
async function latestAdbVersion() {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(PLATFORM_TOOLS_XML, {
|
|
38
|
+
signal: AbortSignal.timeout(8000),
|
|
39
|
+
headers: { 'User-Agent': 'testspectra-cli' },
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
return null;
|
|
43
|
+
const xml = await res.text();
|
|
44
|
+
const block = xml.match(/<remotePackage[^>]*path="platform-tools"[\s\S]*?<\/remotePackage>/);
|
|
45
|
+
if (!block)
|
|
46
|
+
return null;
|
|
47
|
+
const rev = block[0].match(/<revision>[\s\S]*?<major>(\d+)<\/major>[\s\S]*?<minor>(\d+)<\/minor>[\s\S]*?<micro>(\d+)<\/micro>/);
|
|
48
|
+
return rev ? `${rev[1]}.${rev[2]}.${rev[3]}` : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function latestAndroidAgentVersion() {
|
|
55
|
+
try {
|
|
56
|
+
return (execFileSync('npm', ['view', '@testspectra/android-agent', 'version', '--json'], {
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
58
|
+
timeout: 10000,
|
|
59
|
+
})
|
|
60
|
+
.toString()
|
|
61
|
+
.trim()
|
|
62
|
+
.replace(/"/g, '') || null);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Best-effort "installed vs latest" report for the managed system drivers and the Spectra Android
|
|
70
|
+
* Agent, keyed by the exact `name` `spectra doctor` reports so the VS Code Preflight panel can
|
|
71
|
+
* attach a per-row update badge. `latest` stays `null` when it cannot be determined (offline,
|
|
72
|
+
* private registry unavailable) — the row still exposes its reinstall action.
|
|
73
|
+
*/
|
|
74
|
+
export async function checkDriverUpdates() {
|
|
75
|
+
let installed;
|
|
76
|
+
try {
|
|
77
|
+
const result = await runSystemChecks();
|
|
78
|
+
installed = result.dependencies;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const [chrome, bun, adb] = await Promise.all([latestChromeVersion(), latestBunVersion(), latestAdbVersion()]);
|
|
84
|
+
const agent = latestAndroidAgentVersion();
|
|
85
|
+
const candidates = [
|
|
86
|
+
{ name: 'Google Chrome', latest: chrome },
|
|
87
|
+
{ name: 'Bun', latest: bun },
|
|
88
|
+
{ name: 'ADB (Android Debug Bridge)', latest: adb },
|
|
89
|
+
{ name: 'Spectra Android Agent', latest: agent },
|
|
90
|
+
];
|
|
91
|
+
const updates = [];
|
|
92
|
+
for (const { name, latest } of candidates) {
|
|
93
|
+
const dep = installed.find((d) => d.name === name);
|
|
94
|
+
if (!dep?.installed)
|
|
95
|
+
continue;
|
|
96
|
+
const current = normalizeVersion(dep.version);
|
|
97
|
+
updates.push({
|
|
98
|
+
name,
|
|
99
|
+
kind: 'driver',
|
|
100
|
+
identifier: name,
|
|
101
|
+
current,
|
|
102
|
+
latest,
|
|
103
|
+
needsUpdate: !!latest && !!current && current !== latest,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return updates;
|
|
107
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { DevServerConfig } from '../config/schema.js';
|
|
3
|
+
export interface RunningDevServer {
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
process?: ChildProcess;
|
|
7
|
+
reused: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class DevServerManager {
|
|
10
|
+
private spawnedProcesses;
|
|
11
|
+
private cleanupRegistered;
|
|
12
|
+
constructor();
|
|
13
|
+
/**
|
|
14
|
+
* Safely splits a shell command string into binary executable and argument array without shell expansion.
|
|
15
|
+
*/
|
|
16
|
+
static parseCommand(command: string): {
|
|
17
|
+
bin: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Probes a URL to verify if the server is actively listening and responsive.
|
|
22
|
+
* Performs a rapid TCP connection test followed by an HTTP/HTTPS request.
|
|
23
|
+
*/
|
|
24
|
+
static probeUrl(targetUrl: string, timeoutMs?: number): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Ensures an individual dev server is running. If already responding and reuseExistingServer !== false,
|
|
27
|
+
* reuses the instance. Otherwise, spawns the process and polls until ready.
|
|
28
|
+
*/
|
|
29
|
+
ensureServer(config: DevServerConfig, rootCwd?: string): Promise<RunningDevServer>;
|
|
30
|
+
/**
|
|
31
|
+
* Starts all matching dev servers in parallel.
|
|
32
|
+
*/
|
|
33
|
+
startAll(configs?: DevServerConfig[], targetHostFilter?: string, rootCwd?: string): Promise<RunningDevServer[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Safely terminates all spawned dev server child processes.
|
|
36
|
+
*/
|
|
37
|
+
stopAll(): Promise<void>;
|
|
38
|
+
private killProcess;
|
|
39
|
+
private registerExitHooks;
|
|
40
|
+
}
|