vigthoria-cli 1.13.31 → 1.13.34
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/dist/utils/api.d.ts +3 -0
- package/dist/utils/api.js +65 -8
- package/dist/utils/frontend-preview-service.d.ts +2 -1
- package/dist/utils/frontend-preview-service.js +55 -10
- package/dist/utils/preview-screenshot-adapter.d.ts +30 -1
- package/dist/utils/preview-screenshot-adapter.js +407 -1
- package/install.ps1 +6 -6
- package/install.sh +2 -2
- package/package.json +1 -1
package/dist/utils/api.d.ts
CHANGED
|
@@ -254,6 +254,9 @@ export declare class APIClient {
|
|
|
254
254
|
recoverAgentWorkspaceFiles(context?: Record<string, any>, streamedFiles?: Record<string, string>, expectedFiles?: string[]): void;
|
|
255
255
|
normalizeAgentWorkspaceRelativePath(rawPath: string, rootPath?: string): string;
|
|
256
256
|
private isExplicitSingleFileFrontendRequest;
|
|
257
|
+
private isFocusedAgentRepair;
|
|
258
|
+
private buildDirectFileRuntimeInteraction;
|
|
259
|
+
private buildAgentFinalPreviewContext;
|
|
257
260
|
ensureAgentFrontendPolish(message?: string, context?: Record<string, any>): Promise<void>;
|
|
258
261
|
private injectSectionBeforeFooter;
|
|
259
262
|
private injectNavLink;
|
package/dist/utils/api.js
CHANGED
|
@@ -2225,19 +2225,25 @@ menu {
|
|
|
2225
2225
|
};
|
|
2226
2226
|
}
|
|
2227
2227
|
if (name === 'runtime_check') {
|
|
2228
|
-
const
|
|
2228
|
+
const previewArgs = { ...args };
|
|
2229
|
+
delete previewArgs.expect_text;
|
|
2230
|
+
const previewResult = await this.executeV3ClientToolRequest({ ...event, name: 'preview_check', arguments: previewArgs }, context);
|
|
2229
2231
|
if (!previewResult.success)
|
|
2230
2232
|
return previewResult;
|
|
2231
2233
|
const relativeTarget = target.relativePath && target.relativePath !== '.' ? target.relativePath : 'index.html';
|
|
2232
|
-
const
|
|
2234
|
+
const runtimePrompt = String(context.rawPrompt || context.contextualPrompt || context.prompt
|
|
2235
|
+
|| `Verify the interactive frontend runtime at ${relativeTarget}.`);
|
|
2236
|
+
const gate = await this.frontendPreviewService.runTemplateServicePreviewGate(runtimePrompt, {
|
|
2233
2237
|
...context,
|
|
2234
2238
|
targetPath: rootPath,
|
|
2235
2239
|
workspacePath: rootPath,
|
|
2236
2240
|
projectPath: rootPath,
|
|
2237
|
-
rawPrompt:
|
|
2241
|
+
rawPrompt: runtimePrompt,
|
|
2238
2242
|
forceFrontendPreview: true,
|
|
2239
2243
|
localScreenshotProof: true,
|
|
2244
|
+
functionalRuntimeProof: true,
|
|
2240
2245
|
requireScreenshot: true,
|
|
2246
|
+
runtimeInteractionProof: this.buildDirectFileRuntimeInteraction(runtimePrompt, args),
|
|
2241
2247
|
});
|
|
2242
2248
|
if (!gate.passed || gate.artifacts?.screenshotCaptured !== true) {
|
|
2243
2249
|
return {
|
|
@@ -2251,7 +2257,8 @@ menu {
|
|
|
2251
2257
|
output: JSON.stringify({
|
|
2252
2258
|
entry: gate.entryPath || relativeTarget,
|
|
2253
2259
|
screenshotCaptured: true,
|
|
2254
|
-
proof: '
|
|
2260
|
+
proof: 'direct-file-system-browser-interaction',
|
|
2261
|
+
runtimeInteraction: gate.artifacts?.runtimeInteraction || null,
|
|
2255
2262
|
}),
|
|
2256
2263
|
};
|
|
2257
2264
|
}
|
|
@@ -2561,12 +2568,62 @@ menu {
|
|
|
2561
2568
|
|| /\b[a-z0-9_./-]+\.html?\b[^.\n]{0,100}\b(?:only|alone)\b/i.test(request);
|
|
2562
2569
|
return explicitOneFileLanguage;
|
|
2563
2570
|
}
|
|
2571
|
+
isFocusedAgentRepair(context = {}) {
|
|
2572
|
+
const taskKinds = [
|
|
2573
|
+
context.agentTaskType,
|
|
2574
|
+
context.executionHints?.task_kind,
|
|
2575
|
+
context.agentRoute?.taskKind,
|
|
2576
|
+
].map((value) => String(value || '').trim().toLowerCase());
|
|
2577
|
+
return taskKinds.includes('repair');
|
|
2578
|
+
}
|
|
2579
|
+
buildDirectFileRuntimeInteraction(prompt, args = {}) {
|
|
2580
|
+
const request = String(prompt || '');
|
|
2581
|
+
const keyMatch = request.match(/\b(?:press|hit|tap)\s+(?:the\s+)?(?:key\s+)?(space(?:bar)?|enter|return|esc(?:ape)?|arrow\s*(?:up|down|left|right)|[a-z0-9])\b/i);
|
|
2582
|
+
const rawKey = String(keyMatch?.[1] || '').replace(/\s+/g, '');
|
|
2583
|
+
const keyAliases = {
|
|
2584
|
+
return: 'Enter',
|
|
2585
|
+
esc: 'Escape',
|
|
2586
|
+
spacebar: 'Space',
|
|
2587
|
+
};
|
|
2588
|
+
const key = rawKey
|
|
2589
|
+
? (keyAliases[rawKey.toLowerCase()] || rawKey)
|
|
2590
|
+
: undefined;
|
|
2591
|
+
const expectText = String(args.expect_text || '').trim() || undefined;
|
|
2592
|
+
return {
|
|
2593
|
+
...(key ? { key } : {}),
|
|
2594
|
+
...(expectText ? { expectText } : {}),
|
|
2595
|
+
requireStateChange: Boolean(key),
|
|
2596
|
+
waitMs: 1_750,
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
buildAgentFinalPreviewContext(message, context = {}) {
|
|
2600
|
+
if (!this.isFocusedAgentRepair(context)) {
|
|
2601
|
+
return context;
|
|
2602
|
+
}
|
|
2603
|
+
return {
|
|
2604
|
+
...context,
|
|
2605
|
+
rawPrompt: context.rawPrompt || message,
|
|
2606
|
+
forceFrontendPreview: true,
|
|
2607
|
+
localScreenshotProof: true,
|
|
2608
|
+
functionalRuntimeProof: true,
|
|
2609
|
+
requireScreenshot: true,
|
|
2610
|
+
runtimeInteractionProof: this.buildDirectFileRuntimeInteraction(String(context.rawPrompt || message || '')),
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2564
2613
|
async ensureAgentFrontendPolish(message = '', context = {}) {
|
|
2565
2614
|
const rootPath = this.resolveAgentTargetPath(context);
|
|
2566
2615
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
2567
2616
|
return;
|
|
2568
2617
|
}
|
|
2569
2618
|
const prompt = String(message || '');
|
|
2619
|
+
// A focused repair owns only the evidence-proven source mutation. It is
|
|
2620
|
+
// not authorization for the CLI to add responsive CSS, motion helpers, or
|
|
2621
|
+
// fallback script files after the agent has finished. The final repair
|
|
2622
|
+
// gate below still captures a real browser screenshot, but it must remain
|
|
2623
|
+
// read-only with respect to the user's application files.
|
|
2624
|
+
if (this.isFocusedAgentRepair(context)) {
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2570
2627
|
const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
|
|
2571
2628
|
const looksLikeFrontendTask = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|responsive|interactive|game|canvas)/i.test(prompt)
|
|
2572
2629
|
|| expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
|
|
@@ -3623,7 +3680,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3623
3680
|
this.recoverAgentWorkspaceFiles(executionContext, continuationData.files || {}, expectedFiles);
|
|
3624
3681
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3625
3682
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3626
|
-
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3683
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
|
|
3627
3684
|
const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
3628
3685
|
return this.finalizeV3AgentWorkflowResponse(continuationData, {
|
|
3629
3686
|
content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
|
|
@@ -3668,7 +3725,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3668
3725
|
}
|
|
3669
3726
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3670
3727
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3671
|
-
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3728
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
|
|
3672
3729
|
return this.finalizeV3AgentWorkflowResponse(data, {
|
|
3673
3730
|
content: this.formatV3AgentResponse(data),
|
|
3674
3731
|
taskId: data.task_id || null,
|
|
@@ -3699,7 +3756,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3699
3756
|
this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
|
|
3700
3757
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3701
3758
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3702
|
-
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3759
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
|
|
3703
3760
|
return this.finalizeV3AgentWorkflowResponse(error.partialData, {
|
|
3704
3761
|
content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
3705
3762
|
taskId: error.partialData.task_id || null,
|
|
@@ -3766,7 +3823,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3766
3823
|
if (appName) {
|
|
3767
3824
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
|
|
3768
3825
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3769
|
-
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3826
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
|
|
3770
3827
|
return this.finalizeV3AgentWorkflowResponse({}, {
|
|
3771
3828
|
content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
|
|
3772
3829
|
taskId: null,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MutationJournalReport } from './mutation-journal.js';
|
|
2
|
-
import type { PreviewScreenshotPort } from './preview-screenshot-adapter.js';
|
|
2
|
+
import type { PreviewRuntimeInteractionResult, PreviewScreenshotPort } from './preview-screenshot-adapter.js';
|
|
3
3
|
export interface FrontendPreviewGateResult {
|
|
4
4
|
required: boolean;
|
|
5
5
|
passed: boolean;
|
|
@@ -19,6 +19,7 @@ export interface FrontendPreviewGateResult {
|
|
|
19
19
|
previewFileUrl?: string;
|
|
20
20
|
screenshotCaptured?: boolean;
|
|
21
21
|
screenshotError?: string;
|
|
22
|
+
runtimeInteraction?: PreviewRuntimeInteractionResult;
|
|
22
23
|
};
|
|
23
24
|
modes?: {
|
|
24
25
|
design?: {
|
|
@@ -272,6 +272,14 @@ export class FrontendPreviewService {
|
|
|
272
272
|
const html = String(artifacts.html || '');
|
|
273
273
|
const css = String(artifacts.css || '');
|
|
274
274
|
const js = String(artifacts.js || '');
|
|
275
|
+
// runtime_check proves that an existing page can be opened by a real local
|
|
276
|
+
// browser. It is not a responsive-design audit. Requiring viewport metadata,
|
|
277
|
+
// stylesheets, and responsive CSS made focused JavaScript/CORS repairs fail
|
|
278
|
+
// even after Chromium produced the required screenshot. The screenshot and
|
|
279
|
+
// artifact persistence remain fail-closed in persistFrontendPreviewArtifacts.
|
|
280
|
+
if (options.functionalRuntime) {
|
|
281
|
+
return { ok: true, reasons: [] };
|
|
282
|
+
}
|
|
275
283
|
if (!summary.hasViewportMeta) {
|
|
276
284
|
reasons.push('missing viewport metadata');
|
|
277
285
|
}
|
|
@@ -305,13 +313,30 @@ export class FrontendPreviewService {
|
|
|
305
313
|
const screenshotPath = resolveWorkspacePath(rootPath, `.vigthoria/proof/preview/${baseName}.png`, { allowMissing: true });
|
|
306
314
|
const entryAbsolutePath = resolveWorkspacePath(rootPath, previewGate.entryPath);
|
|
307
315
|
const previewFileUrl = `file://${entryAbsolutePath}`;
|
|
308
|
-
const
|
|
316
|
+
const runtimeOptions = context.runtimeInteractionProof && typeof context.runtimeInteractionProof === 'object'
|
|
317
|
+
? context.runtimeInteractionProof
|
|
318
|
+
: null;
|
|
319
|
+
const runtimeInteraction = runtimeOptions
|
|
320
|
+
? (typeof this.dependencies.screenshotAdapter.captureRuntime === 'function'
|
|
321
|
+
? await this.dependencies.screenshotAdapter.captureRuntime(entryAbsolutePath, screenshotPath, runtimeOptions)
|
|
322
|
+
: {
|
|
323
|
+
captured: false,
|
|
324
|
+
passed: false,
|
|
325
|
+
error: 'required direct-file browser interaction adapter is unavailable',
|
|
326
|
+
})
|
|
327
|
+
: null;
|
|
328
|
+
const screenshot = runtimeInteraction
|
|
329
|
+
|| await this.dependencies.screenshotAdapter.capture(entryAbsolutePath, screenshotPath);
|
|
309
330
|
const screenshotRequired = context.requireScreenshot === true;
|
|
310
|
-
const
|
|
331
|
+
const screenshotFailure = screenshotRequired && !screenshot.captured;
|
|
332
|
+
const runtimeFailure = runtimeInteraction !== null && !runtimeInteraction.passed;
|
|
333
|
+
const effectivePreviewGate = screenshotFailure || runtimeFailure
|
|
311
334
|
? {
|
|
312
335
|
...previewGate,
|
|
313
336
|
passed: false,
|
|
314
|
-
error: `${previewGate.error ? `${previewGate.error} ` : ''}
|
|
337
|
+
error: `${previewGate.error ? `${previewGate.error} ` : ''}${runtimeFailure
|
|
338
|
+
? (runtimeInteraction.error || 'Required direct-file browser interaction proof failed.')
|
|
339
|
+
: `Required screenshot proof is unavailable: ${screenshot.error || 'capture failed'}.`}`,
|
|
315
340
|
}
|
|
316
341
|
: previewGate;
|
|
317
342
|
const manifest = {
|
|
@@ -335,6 +360,16 @@ export class FrontendPreviewService {
|
|
|
335
360
|
captured: screenshot.captured,
|
|
336
361
|
error: screenshot.error || null,
|
|
337
362
|
},
|
|
363
|
+
runtimeInteraction: runtimeInteraction ? {
|
|
364
|
+
passed: runtimeInteraction.passed,
|
|
365
|
+
action: runtimeInteraction.action || null,
|
|
366
|
+
initialBodyText: runtimeInteraction.initialBodyText || '',
|
|
367
|
+
finalBodyText: runtimeInteraction.finalBodyText || '',
|
|
368
|
+
bodyChanged: runtimeInteraction.bodyChanged === true,
|
|
369
|
+
visualChanged: runtimeInteraction.visualChanged === true,
|
|
370
|
+
runtimeErrors: runtimeInteraction.runtimeErrors || [],
|
|
371
|
+
error: runtimeInteraction.error || null,
|
|
372
|
+
} : null,
|
|
338
373
|
};
|
|
339
374
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
340
375
|
return {
|
|
@@ -345,6 +380,7 @@ export class FrontendPreviewService {
|
|
|
345
380
|
previewFileUrl,
|
|
346
381
|
screenshotCaptured: screenshot.captured,
|
|
347
382
|
screenshotError: screenshot.error,
|
|
383
|
+
...(runtimeInteraction ? { runtimeInteraction } : {}),
|
|
348
384
|
},
|
|
349
385
|
};
|
|
350
386
|
}
|
|
@@ -415,7 +451,10 @@ export class FrontendPreviewService {
|
|
|
415
451
|
proofSource: 'local-screenshot-adapter',
|
|
416
452
|
};
|
|
417
453
|
const constrainedStatic = isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''));
|
|
418
|
-
const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, {
|
|
454
|
+
const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, {
|
|
455
|
+
constrainedStatic,
|
|
456
|
+
functionalRuntime: context.functionalRuntimeProof === true,
|
|
457
|
+
});
|
|
419
458
|
const localGate = {
|
|
420
459
|
required: true,
|
|
421
460
|
passed: visualProof.ok,
|
|
@@ -491,12 +530,18 @@ export class FrontendPreviewService {
|
|
|
491
530
|
const summary = payload?.summary || {};
|
|
492
531
|
const constrainedStatic = isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''))
|
|
493
532
|
&& modes?.production?.qualityTier === 'scope-complete';
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
533
|
+
const functionalRuntime = context.functionalRuntimeProof === true;
|
|
534
|
+
const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, {
|
|
535
|
+
constrainedStatic,
|
|
536
|
+
functionalRuntime,
|
|
537
|
+
});
|
|
538
|
+
const modeProofReady = functionalRuntime
|
|
539
|
+
? payload?.success === true && modes?.live?.ready === true
|
|
540
|
+
: payload?.success === true
|
|
541
|
+
&& modes?.design?.ready === true
|
|
542
|
+
&& modes?.live?.ready === true
|
|
543
|
+
&& modes?.production?.ready === true;
|
|
544
|
+
const passed = modeProofReady && visualProof.ok;
|
|
500
545
|
const previewGate = {
|
|
501
546
|
required: true,
|
|
502
547
|
passed,
|
|
@@ -1,17 +1,43 @@
|
|
|
1
|
+
export type PreviewRuntimeInteractionOptions = {
|
|
2
|
+
key?: string;
|
|
3
|
+
expectText?: string;
|
|
4
|
+
requireStateChange?: boolean;
|
|
5
|
+
waitMs?: number;
|
|
6
|
+
};
|
|
1
7
|
export type PreviewScreenshotResult = {
|
|
2
8
|
captured: boolean;
|
|
3
9
|
error?: string;
|
|
4
10
|
};
|
|
11
|
+
export type PreviewRuntimeInteractionResult = PreviewScreenshotResult & {
|
|
12
|
+
passed: boolean;
|
|
13
|
+
action?: string;
|
|
14
|
+
initialBodyText?: string;
|
|
15
|
+
finalBodyText?: string;
|
|
16
|
+
bodyChanged?: boolean;
|
|
17
|
+
visualChanged?: boolean;
|
|
18
|
+
runtimeErrors?: string[];
|
|
19
|
+
};
|
|
5
20
|
export interface PreviewScreenshotPort {
|
|
6
21
|
capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
|
|
22
|
+
captureRuntime?(entryAbsolutePath: string, screenshotPath: string, options: PreviewRuntimeInteractionOptions): Promise<PreviewRuntimeInteractionResult>;
|
|
7
23
|
}
|
|
8
24
|
export type PreviewBrowserResolution = {
|
|
9
25
|
executablePath: string;
|
|
10
26
|
source: 'system';
|
|
11
27
|
};
|
|
12
28
|
export type PreviewBrowserRunner = (executablePath: string, args: readonly string[], platform: NodeJS.Platform, environment?: NodeJS.ProcessEnv) => Promise<void>;
|
|
29
|
+
type PreviewRuntimeBrowserObservation = {
|
|
30
|
+
action?: string;
|
|
31
|
+
initialBodyText: string;
|
|
32
|
+
finalBodyText: string;
|
|
33
|
+
bodyChanged: boolean;
|
|
34
|
+
visualChanged: boolean;
|
|
35
|
+
runtimeErrors: string[];
|
|
36
|
+
};
|
|
37
|
+
export type PreviewRuntimeBrowserRunner = (executablePath: string, args: readonly string[], platform: NodeJS.Platform, environment: NodeJS.ProcessEnv, screenshotPath: string, options: PreviewRuntimeInteractionOptions) => Promise<PreviewRuntimeBrowserObservation>;
|
|
13
38
|
export declare function resolvePreviewBrowserExecutable(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean): PreviewBrowserResolution | null;
|
|
14
39
|
export declare const runPreviewBrowser: PreviewBrowserRunner;
|
|
40
|
+
export declare const runPreviewRuntimeBrowser: PreviewRuntimeBrowserRunner;
|
|
15
41
|
/** Dependency-free system-browser adapter. No browser download or archive
|
|
16
42
|
* extractor is shipped, and only fixed operating-system installation paths
|
|
17
43
|
* are eligible.
|
|
@@ -23,8 +49,11 @@ export declare class SystemBrowserScreenshotAdapter implements PreviewScreenshot
|
|
|
23
49
|
private readonly platform;
|
|
24
50
|
private readonly exists;
|
|
25
51
|
private readonly runner;
|
|
26
|
-
|
|
52
|
+
private readonly runtimeRunner;
|
|
53
|
+
constructor(environment?: NodeJS.ProcessEnv, allocateTemp?: (prefix: string, requestedMaxBytes?: number) => string, releaseTemp?: (directory: string) => void, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean, runner?: PreviewBrowserRunner, runtimeRunner?: PreviewRuntimeBrowserRunner);
|
|
27
54
|
capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
|
|
55
|
+
captureRuntime(entryAbsolutePath: string, screenshotPath: string, options: PreviewRuntimeInteractionOptions): Promise<PreviewRuntimeInteractionResult>;
|
|
56
|
+
private captureRuntimeOnce;
|
|
28
57
|
private captureOnce;
|
|
29
58
|
}
|
|
30
59
|
/** @deprecated Internal compatibility alias retained for older deep imports. */
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import WebSocket from 'ws';
|
|
5
6
|
import { redactSensitiveText, safeChildProcessEnv } from './secret-policy.js';
|
|
6
7
|
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from './runtime-temp.js';
|
|
7
8
|
const SCREENSHOT_WIDTH = 800;
|
|
@@ -114,6 +115,278 @@ export const runPreviewBrowser = (executablePath, args, platform, environment =
|
|
|
114
115
|
finish(new Error(`browser screenshot process exited with ${signal || code}`));
|
|
115
116
|
});
|
|
116
117
|
});
|
|
118
|
+
class PreviewCdpClient {
|
|
119
|
+
socket;
|
|
120
|
+
nextId = 1;
|
|
121
|
+
pending = new Map();
|
|
122
|
+
eventListeners = new Set();
|
|
123
|
+
constructor(socket) {
|
|
124
|
+
this.socket = socket;
|
|
125
|
+
socket.on('message', (data) => {
|
|
126
|
+
let message;
|
|
127
|
+
try {
|
|
128
|
+
message = JSON.parse(data.toString());
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (typeof message.id === 'number') {
|
|
134
|
+
const request = this.pending.get(message.id);
|
|
135
|
+
if (!request)
|
|
136
|
+
return;
|
|
137
|
+
clearTimeout(request.timer);
|
|
138
|
+
this.pending.delete(message.id);
|
|
139
|
+
if (message.error)
|
|
140
|
+
request.reject(new Error(message.error.message || 'DevTools command failed'));
|
|
141
|
+
else
|
|
142
|
+
request.resolve(message.result || {});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const listener of this.eventListeners)
|
|
146
|
+
listener(message);
|
|
147
|
+
});
|
|
148
|
+
socket.on('error', (error) => this.rejectPending(error));
|
|
149
|
+
socket.on('close', () => this.rejectPending(new Error('browser DevTools connection closed')));
|
|
150
|
+
}
|
|
151
|
+
onEvent(listener) {
|
|
152
|
+
this.eventListeners.add(listener);
|
|
153
|
+
return () => this.eventListeners.delete(listener);
|
|
154
|
+
}
|
|
155
|
+
send(method, params = {}, sessionId) {
|
|
156
|
+
const id = this.nextId++;
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
this.pending.delete(id);
|
|
160
|
+
reject(new Error(`browser DevTools command ${method} timed out`));
|
|
161
|
+
}, 7_500);
|
|
162
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
163
|
+
this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }), (error) => {
|
|
164
|
+
if (!error)
|
|
165
|
+
return;
|
|
166
|
+
const request = this.pending.get(id);
|
|
167
|
+
if (!request)
|
|
168
|
+
return;
|
|
169
|
+
clearTimeout(request.timer);
|
|
170
|
+
this.pending.delete(id);
|
|
171
|
+
reject(error);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
close() {
|
|
176
|
+
try {
|
|
177
|
+
this.socket.close();
|
|
178
|
+
}
|
|
179
|
+
catch { /* browser already exited */ }
|
|
180
|
+
}
|
|
181
|
+
rejectPending(error) {
|
|
182
|
+
for (const [id, request] of this.pending) {
|
|
183
|
+
clearTimeout(request.timer);
|
|
184
|
+
request.reject(error);
|
|
185
|
+
this.pending.delete(id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function waitForDevToolsSocket(child) {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
let buffer = '';
|
|
192
|
+
let settled = false;
|
|
193
|
+
const finish = (error, socketUrl) => {
|
|
194
|
+
if (settled)
|
|
195
|
+
return;
|
|
196
|
+
settled = true;
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
child.stderr?.off('data', onData);
|
|
199
|
+
child.off('error', onError);
|
|
200
|
+
child.off('exit', onExit);
|
|
201
|
+
if (error)
|
|
202
|
+
reject(error);
|
|
203
|
+
else
|
|
204
|
+
resolve(socketUrl);
|
|
205
|
+
};
|
|
206
|
+
const onData = (chunk) => {
|
|
207
|
+
buffer = `${buffer}${chunk.toString()}`.slice(-16_384);
|
|
208
|
+
const match = buffer.match(/DevTools listening on (ws:\/\/[^\s]+)/i);
|
|
209
|
+
if (match)
|
|
210
|
+
finish(undefined, match[1]);
|
|
211
|
+
};
|
|
212
|
+
const onError = (error) => finish(error);
|
|
213
|
+
const onExit = (code, signal) => finish(new Error(`browser runtime process exited before DevTools was ready with ${signal || code}`));
|
|
214
|
+
const timer = setTimeout(() => finish(new Error('browser DevTools endpoint did not become ready')), 10_000);
|
|
215
|
+
child.stderr?.on('data', onData);
|
|
216
|
+
child.once('error', onError);
|
|
217
|
+
child.once('exit', onExit);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function connectDevTools(socketUrl) {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const socket = new WebSocket(socketUrl, { perMessageDeflate: false });
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
try {
|
|
225
|
+
socket.terminate();
|
|
226
|
+
}
|
|
227
|
+
catch { /* connection never opened */ }
|
|
228
|
+
reject(new Error('browser DevTools WebSocket connection timed out'));
|
|
229
|
+
}, 7_500);
|
|
230
|
+
socket.once('open', () => { clearTimeout(timer); resolve(socket); });
|
|
231
|
+
socket.once('error', (error) => { clearTimeout(timer); reject(error); });
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function normalizeRuntimeBody(value) {
|
|
235
|
+
return redactSensitiveText(String(value || '').replace(/\s+/g, ' ').trim()).slice(0, 4_000);
|
|
236
|
+
}
|
|
237
|
+
function runtimeKeyDefinition(rawKey = '') {
|
|
238
|
+
const normalized = rawKey.trim().toLowerCase().replace(/\s+/g, '');
|
|
239
|
+
const fixed = {
|
|
240
|
+
space: { key: ' ', code: 'Space', virtualKeyCode: 32, text: ' ' },
|
|
241
|
+
spacebar: { key: ' ', code: 'Space', virtualKeyCode: 32, text: ' ' },
|
|
242
|
+
enter: { key: 'Enter', code: 'Enter', virtualKeyCode: 13, text: '\r' },
|
|
243
|
+
escape: { key: 'Escape', code: 'Escape', virtualKeyCode: 27 },
|
|
244
|
+
esc: { key: 'Escape', code: 'Escape', virtualKeyCode: 27 },
|
|
245
|
+
arrowup: { key: 'ArrowUp', code: 'ArrowUp', virtualKeyCode: 38 },
|
|
246
|
+
arrowdown: { key: 'ArrowDown', code: 'ArrowDown', virtualKeyCode: 40 },
|
|
247
|
+
arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', virtualKeyCode: 37 },
|
|
248
|
+
arrowright: { key: 'ArrowRight', code: 'ArrowRight', virtualKeyCode: 39 },
|
|
249
|
+
};
|
|
250
|
+
if (fixed[normalized])
|
|
251
|
+
return fixed[normalized];
|
|
252
|
+
if (/^[a-z0-9]$/.test(normalized)) {
|
|
253
|
+
const upper = normalized.toUpperCase();
|
|
254
|
+
return { key: normalized, code: /[a-z]/.test(normalized) ? `Key${upper}` : `Digit${normalized}`, virtualKeyCode: upper.charCodeAt(0), text: normalized };
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
async function waitForRuntimeReady(client, sessionId) {
|
|
259
|
+
const deadline = Date.now() + 8_000;
|
|
260
|
+
while (Date.now() < deadline) {
|
|
261
|
+
const evaluated = await client.send('Runtime.evaluate', {
|
|
262
|
+
expression: 'document.readyState',
|
|
263
|
+
returnByValue: true,
|
|
264
|
+
}, sessionId);
|
|
265
|
+
const readyState = String(evaluated.result?.value || '');
|
|
266
|
+
if (readyState === 'interactive' || readyState === 'complete')
|
|
267
|
+
return;
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
269
|
+
}
|
|
270
|
+
throw new Error('browser document did not become ready');
|
|
271
|
+
}
|
|
272
|
+
async function readRuntimeBody(client, sessionId) {
|
|
273
|
+
const evaluated = await client.send('Runtime.evaluate', {
|
|
274
|
+
expression: 'document.body ? document.body.innerText : ""',
|
|
275
|
+
returnByValue: true,
|
|
276
|
+
}, sessionId);
|
|
277
|
+
return normalizeRuntimeBody(evaluated.result?.value);
|
|
278
|
+
}
|
|
279
|
+
async function captureRuntimePng(client, sessionId) {
|
|
280
|
+
const captured = await client.send('Page.captureScreenshot', {
|
|
281
|
+
format: 'png',
|
|
282
|
+
fromSurface: true,
|
|
283
|
+
captureBeyondViewport: false,
|
|
284
|
+
}, sessionId);
|
|
285
|
+
if (typeof captured.data !== 'string' || captured.data.length === 0) {
|
|
286
|
+
throw new Error('browser DevTools did not return screenshot bytes');
|
|
287
|
+
}
|
|
288
|
+
return Buffer.from(captured.data, 'base64');
|
|
289
|
+
}
|
|
290
|
+
export const runPreviewRuntimeBrowser = async (executablePath, args, platform, environment, screenshotPath, options) => {
|
|
291
|
+
const child = spawn(executablePath, [...args], {
|
|
292
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
293
|
+
windowsHide: true,
|
|
294
|
+
env: safeChildProcessEnv(environment),
|
|
295
|
+
detached: platform !== 'win32',
|
|
296
|
+
});
|
|
297
|
+
let client = null;
|
|
298
|
+
const runtimeErrors = [];
|
|
299
|
+
try {
|
|
300
|
+
const socketUrl = await waitForDevToolsSocket(child);
|
|
301
|
+
client = new PreviewCdpClient(await connectDevTools(socketUrl));
|
|
302
|
+
const targets = await client.send('Target.getTargets');
|
|
303
|
+
const pageTarget = Array.isArray(targets.targetInfos)
|
|
304
|
+
? targets.targetInfos.find((target) => target?.type === 'page')
|
|
305
|
+
: null;
|
|
306
|
+
if (!pageTarget?.targetId)
|
|
307
|
+
throw new Error('browser DevTools did not expose a page target');
|
|
308
|
+
const attached = await client.send('Target.attachToTarget', { targetId: pageTarget.targetId, flatten: true });
|
|
309
|
+
const sessionId = String(attached.sessionId || '');
|
|
310
|
+
if (!sessionId)
|
|
311
|
+
throw new Error('browser DevTools did not attach to the page target');
|
|
312
|
+
client.onEvent((event) => {
|
|
313
|
+
if (event.sessionId !== sessionId)
|
|
314
|
+
return;
|
|
315
|
+
if (event.method === 'Runtime.exceptionThrown') {
|
|
316
|
+
const description = event.params?.exceptionDetails?.exception?.description
|
|
317
|
+
|| event.params?.exceptionDetails?.text
|
|
318
|
+
|| 'Uncaught runtime exception';
|
|
319
|
+
runtimeErrors.push(normalizeRuntimeBody(description));
|
|
320
|
+
}
|
|
321
|
+
if (event.method === 'Log.entryAdded' && event.params?.entry?.level === 'error') {
|
|
322
|
+
runtimeErrors.push(normalizeRuntimeBody(event.params.entry.text || 'Browser console error'));
|
|
323
|
+
}
|
|
324
|
+
if (event.method === 'Network.loadingFailed') {
|
|
325
|
+
const resourceType = String(event.params?.type || '');
|
|
326
|
+
if (['Document', 'Script', 'Fetch', 'XHR', 'Stylesheet'].includes(resourceType)) {
|
|
327
|
+
runtimeErrors.push(normalizeRuntimeBody(`Failed ${resourceType} request: ${event.params?.errorText || 'unknown browser error'}`));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
await client.send('Page.enable', {}, sessionId);
|
|
332
|
+
await client.send('Runtime.enable', {}, sessionId);
|
|
333
|
+
await client.send('Log.enable', {}, sessionId);
|
|
334
|
+
await client.send('Network.enable', {}, sessionId);
|
|
335
|
+
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
336
|
+
width: SCREENSHOT_WIDTH,
|
|
337
|
+
height: SCREENSHOT_HEIGHT,
|
|
338
|
+
deviceScaleFactor: 1,
|
|
339
|
+
mobile: false,
|
|
340
|
+
}, sessionId);
|
|
341
|
+
const entryUrl = String(args.at(-1) || '');
|
|
342
|
+
await client.send('Page.navigate', { url: entryUrl }, sessionId);
|
|
343
|
+
await waitForRuntimeReady(client, sessionId);
|
|
344
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
345
|
+
const initialBodyText = await readRuntimeBody(client, sessionId);
|
|
346
|
+
const initialPng = await captureRuntimePng(client, sessionId);
|
|
347
|
+
const key = options.key ? runtimeKeyDefinition(options.key) : null;
|
|
348
|
+
if (options.key && !key)
|
|
349
|
+
throw new Error(`unsupported browser interaction key: ${options.key}`);
|
|
350
|
+
let action;
|
|
351
|
+
if (key) {
|
|
352
|
+
action = `press:${key.code}`;
|
|
353
|
+
await client.send('Input.dispatchKeyEvent', {
|
|
354
|
+
type: 'keyDown', key: key.key, code: key.code,
|
|
355
|
+
windowsVirtualKeyCode: key.virtualKeyCode, nativeVirtualKeyCode: key.virtualKeyCode,
|
|
356
|
+
...(key.text ? { text: key.text, unmodifiedText: key.text } : {}),
|
|
357
|
+
}, sessionId);
|
|
358
|
+
await client.send('Input.dispatchKeyEvent', {
|
|
359
|
+
type: 'keyUp', key: key.key, code: key.code,
|
|
360
|
+
windowsVirtualKeyCode: key.virtualKeyCode, nativeVirtualKeyCode: key.virtualKeyCode,
|
|
361
|
+
}, sessionId);
|
|
362
|
+
}
|
|
363
|
+
const waitMs = Math.max(250, Math.min(Number(options.waitMs) || 1_750, 5_000));
|
|
364
|
+
const deadline = Date.now() + waitMs;
|
|
365
|
+
let finalBodyText = initialBodyText;
|
|
366
|
+
while (Date.now() < deadline) {
|
|
367
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
368
|
+
finalBodyText = await readRuntimeBody(client, sessionId);
|
|
369
|
+
if (options.expectText && finalBodyText.includes(options.expectText))
|
|
370
|
+
break;
|
|
371
|
+
if (!options.expectText && options.requireStateChange && finalBodyText !== initialBodyText)
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
const finalPng = await captureRuntimePng(client, sessionId);
|
|
375
|
+
fs.writeFileSync(screenshotPath, finalPng, { mode: 0o600 });
|
|
376
|
+
return {
|
|
377
|
+
action,
|
|
378
|
+
initialBodyText,
|
|
379
|
+
finalBodyText,
|
|
380
|
+
bodyChanged: finalBodyText !== initialBodyText,
|
|
381
|
+
visualChanged: !finalPng.equals(initialPng),
|
|
382
|
+
runtimeErrors: Array.from(new Set(runtimeErrors.filter(Boolean))).slice(0, 12),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
client?.close();
|
|
387
|
+
await terminateBrowserProcess(child, platform);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
117
390
|
function pngCrc32(data) {
|
|
118
391
|
let crc = 0xffffffff;
|
|
119
392
|
for (const byte of data) {
|
|
@@ -198,13 +471,15 @@ export class SystemBrowserScreenshotAdapter {
|
|
|
198
471
|
platform;
|
|
199
472
|
exists;
|
|
200
473
|
runner;
|
|
201
|
-
|
|
474
|
+
runtimeRunner;
|
|
475
|
+
constructor(environment = process.env, allocateTemp = createRuntimeTempDirectory, releaseTemp = removeRuntimeTempDirectory, platform = process.platform, exists = fs.existsSync, runner = runPreviewBrowser, runtimeRunner = runPreviewRuntimeBrowser) {
|
|
202
476
|
this.environment = environment;
|
|
203
477
|
this.allocateTemp = allocateTemp;
|
|
204
478
|
this.releaseTemp = releaseTemp;
|
|
205
479
|
this.platform = platform;
|
|
206
480
|
this.exists = exists;
|
|
207
481
|
this.runner = runner;
|
|
482
|
+
this.runtimeRunner = runtimeRunner;
|
|
208
483
|
}
|
|
209
484
|
async capture(entryAbsolutePath, screenshotPath) {
|
|
210
485
|
if (this.environment.VIGTHORIA_DISABLE_PREVIEW_SCREENSHOT === '1') {
|
|
@@ -227,6 +502,137 @@ export class SystemBrowserScreenshotAdapter {
|
|
|
227
502
|
}
|
|
228
503
|
return lastResult;
|
|
229
504
|
}
|
|
505
|
+
async captureRuntime(entryAbsolutePath, screenshotPath, options) {
|
|
506
|
+
if (this.environment.VIGTHORIA_DISABLE_PREVIEW_SCREENSHOT === '1') {
|
|
507
|
+
return { captured: false, passed: false, error: 'runtime screenshot capture explicitly unavailable' };
|
|
508
|
+
}
|
|
509
|
+
const resolution = resolvePreviewBrowserExecutable(this.environment, this.platform, this.exists);
|
|
510
|
+
if (!resolution) {
|
|
511
|
+
return {
|
|
512
|
+
captured: false,
|
|
513
|
+
passed: false,
|
|
514
|
+
error: 'required system browser is unavailable; install Microsoft Edge, Chrome, or Chromium',
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
let lastResult = {
|
|
518
|
+
captured: false,
|
|
519
|
+
passed: false,
|
|
520
|
+
error: 'browser runtime proof was not attempted',
|
|
521
|
+
};
|
|
522
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
523
|
+
lastResult = await this.captureRuntimeOnce(resolution, entryAbsolutePath, screenshotPath, options);
|
|
524
|
+
if (lastResult.passed || !isTransientBrowserFailure(lastResult))
|
|
525
|
+
return lastResult;
|
|
526
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
527
|
+
}
|
|
528
|
+
return lastResult;
|
|
529
|
+
}
|
|
530
|
+
async captureRuntimeOnce(resolution, entryAbsolutePath, screenshotPath, options) {
|
|
531
|
+
let browserProfile = null;
|
|
532
|
+
let result;
|
|
533
|
+
try {
|
|
534
|
+
browserProfile = this.allocateTemp('browser-runtime-', 128 * 1024 * 1024);
|
|
535
|
+
const browserUserData = path.join(browserProfile, 'profile');
|
|
536
|
+
const browserScratch = path.join(browserProfile, 'scratch');
|
|
537
|
+
if (this.platform !== 'win32' && Buffer.byteLength(browserScratch) > POSIX_BROWSER_SCRATCH_MAX_BYTES) {
|
|
538
|
+
throw new Error('managed browser temporary path is too long; configure a shorter dedicated VIGTHORIA_TEMP_DIR');
|
|
539
|
+
}
|
|
540
|
+
fs.mkdirSync(browserUserData, { recursive: false, mode: 0o700 });
|
|
541
|
+
fs.mkdirSync(browserScratch, { recursive: false, mode: 0o700 });
|
|
542
|
+
fs.mkdirSync(path.dirname(screenshotPath), { recursive: true, mode: 0o700 });
|
|
543
|
+
try {
|
|
544
|
+
fs.unlinkSync(screenshotPath);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (error?.code !== 'ENOENT')
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
const args = [
|
|
551
|
+
'--headless=new',
|
|
552
|
+
'--disable-gpu',
|
|
553
|
+
'--disable-dev-shm-usage',
|
|
554
|
+
'--hide-scrollbars',
|
|
555
|
+
'--no-first-run',
|
|
556
|
+
'--no-default-browser-check',
|
|
557
|
+
'--disable-background-networking',
|
|
558
|
+
'--disable-component-update',
|
|
559
|
+
'--disable-sync',
|
|
560
|
+
'--metrics-recording-only',
|
|
561
|
+
'--proxy-server=http://127.0.0.1:9',
|
|
562
|
+
'--proxy-bypass-list=<-loopback>',
|
|
563
|
+
'--remote-debugging-port=0',
|
|
564
|
+
`--user-data-dir=${browserUserData}`,
|
|
565
|
+
`--window-size=${SCREENSHOT_WIDTH},${SCREENSHOT_HEIGHT}`,
|
|
566
|
+
pathToFileURL(entryAbsolutePath).toString(),
|
|
567
|
+
];
|
|
568
|
+
const browserEnvironment = safeChildProcessEnv(this.environment, {
|
|
569
|
+
TMPDIR: browserScratch,
|
|
570
|
+
TMP: browserScratch,
|
|
571
|
+
TEMP: browserScratch,
|
|
572
|
+
});
|
|
573
|
+
const observation = await this.runtimeRunner(resolution.executablePath, args, this.platform, browserEnvironment, screenshotPath, options);
|
|
574
|
+
validatePng(screenshotPath);
|
|
575
|
+
const runtimeErrors = observation.runtimeErrors.map((entry) => redactSensitiveText(entry)).slice(0, 12);
|
|
576
|
+
const expectedText = String(options.expectText || '').trim();
|
|
577
|
+
const expectedTextMatched = !expectedText || observation.finalBodyText.includes(expectedText);
|
|
578
|
+
const stateChanged = observation.bodyChanged || observation.visualChanged;
|
|
579
|
+
const reasons = [];
|
|
580
|
+
if (runtimeErrors.length > 0)
|
|
581
|
+
reasons.push(`browser errors: ${runtimeErrors.join(' | ')}`);
|
|
582
|
+
if (!expectedTextMatched)
|
|
583
|
+
reasons.push(`expected rendered text was not found after the interaction: ${redactSensitiveText(expectedText)}`);
|
|
584
|
+
if (options.requireStateChange && !stateChanged) {
|
|
585
|
+
reasons.push(`${observation.action || 'requested interaction'} produced no DOM-text or visual change`);
|
|
586
|
+
}
|
|
587
|
+
result = {
|
|
588
|
+
captured: true,
|
|
589
|
+
passed: reasons.length === 0,
|
|
590
|
+
action: observation.action,
|
|
591
|
+
initialBodyText: observation.initialBodyText,
|
|
592
|
+
finalBodyText: observation.finalBodyText,
|
|
593
|
+
bodyChanged: observation.bodyChanged,
|
|
594
|
+
visualChanged: observation.visualChanged,
|
|
595
|
+
runtimeErrors,
|
|
596
|
+
error: reasons.length > 0 ? `Direct-file browser runtime proof failed: ${reasons.join('; ')}.` : undefined,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
try {
|
|
601
|
+
fs.unlinkSync(screenshotPath);
|
|
602
|
+
}
|
|
603
|
+
catch { /* no failed proof artifact */ }
|
|
604
|
+
result = {
|
|
605
|
+
captured: false,
|
|
606
|
+
passed: false,
|
|
607
|
+
error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
finally {
|
|
611
|
+
if (browserProfile) {
|
|
612
|
+
try {
|
|
613
|
+
this.releaseTemp(browserProfile);
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
617
|
+
try {
|
|
618
|
+
this.releaseTemp(browserProfile);
|
|
619
|
+
}
|
|
620
|
+
catch (cleanupError) {
|
|
621
|
+
try {
|
|
622
|
+
fs.unlinkSync(screenshotPath);
|
|
623
|
+
}
|
|
624
|
+
catch { /* proof is invalid when cleanup is incomplete */ }
|
|
625
|
+
result = {
|
|
626
|
+
captured: false,
|
|
627
|
+
passed: false,
|
|
628
|
+
error: redactSensitiveText(`browser profile cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
230
636
|
async captureOnce(resolution, entryAbsolutePath, screenshotPath) {
|
|
231
637
|
let browserProfile = null;
|
|
232
638
|
let result;
|
package/install.ps1
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
$ErrorActionPreference = "Stop"
|
|
6
6
|
|
|
7
7
|
# Configuration
|
|
8
|
-
$CLI_VERSION = "1.13.
|
|
8
|
+
$CLI_VERSION = "1.13.34"
|
|
9
9
|
$INSTALL_DIR = "$env:USERPROFILE\.vigthoria"
|
|
10
10
|
$NPM_PACKAGE = "vigthoria-cli"
|
|
11
11
|
$MANIFEST_URL = "https://extension.vigthoria.io/downloads/manifest.json"
|
|
@@ -32,7 +32,7 @@ function Assert-TrustedReleaseUrl([string]$Value) {
|
|
|
32
32
|
function Resolve-ReleaseManifest {
|
|
33
33
|
# The hosted path is enabled only when the packaged shared policy verifier
|
|
34
34
|
# can validate strict metadata and the detached Ed25519 signature.
|
|
35
|
-
if (-not (Test-Path $RELEASE_RESOLVER)) { return }
|
|
35
|
+
if (-not $RELEASE_RESOLVER -or -not (Test-Path -LiteralPath $RELEASE_RESOLVER)) { return }
|
|
36
36
|
try {
|
|
37
37
|
$trustedManifest = Assert-TrustedReleaseUrl $MANIFEST_URL
|
|
38
38
|
$resolvedJson = & node $RELEASE_RESOLVER $trustedManifest stable
|
|
@@ -166,7 +166,7 @@ function Install-VigthoriaCLI-NPM {
|
|
|
166
166
|
Write-Host ""
|
|
167
167
|
Write-Host "[INSTALL] Installing Vigthoria CLI..." -ForegroundColor Cyan
|
|
168
168
|
|
|
169
|
-
if ($HOSTED_TARBALL_SHA256) {
|
|
169
|
+
if ($HOSTED_TARBALL_SHA256 -and $RELEASE_INSTALLER -and (Test-Path -LiteralPath $RELEASE_INSTALLER)) {
|
|
170
170
|
$releaseTemp = Join-Path (Get-VigthoriaTempRoot) ("install-" + [Guid]::NewGuid().ToString("N"))
|
|
171
171
|
$releaseArchive = Join-Path $releaseTemp "candidate.tgz"
|
|
172
172
|
try {
|
|
@@ -175,7 +175,7 @@ function Install-VigthoriaCLI-NPM {
|
|
|
175
175
|
Invoke-WebRequest -Uri $trustedTarball -OutFile $releaseArchive -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 60 -ErrorAction Stop
|
|
176
176
|
$actualSha256 = (Get-FileHash -Path $releaseArchive -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
177
177
|
if ($actualSha256 -ne $HOSTED_TARBALL_SHA256) { throw "Release checksum mismatch" }
|
|
178
|
-
if (-not (Test-Path $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
|
|
178
|
+
if (-not $RELEASE_INSTALLER -or -not (Test-Path -LiteralPath $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
|
|
179
179
|
& node $RELEASE_INSTALLER $releaseArchive $CLI_VERSION
|
|
180
180
|
if ($LASTEXITCODE -ne 0) { throw "transactional installer exited with status $LASTEXITCODE" }
|
|
181
181
|
Write-Host "[OK] Vigthoria CLI installed from the checksum-verified release!" -ForegroundColor Green
|
|
@@ -192,7 +192,7 @@ function Install-VigthoriaCLI-NPM {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
try {
|
|
195
|
-
if (Test-Path $RELEASE_INSTALLER) {
|
|
195
|
+
if ($RELEASE_INSTALLER -and (Test-Path -LiteralPath $RELEASE_INSTALLER)) {
|
|
196
196
|
& node $RELEASE_INSTALLER "$NPM_PACKAGE@$CLI_VERSION" $CLI_VERSION
|
|
197
197
|
if ($LASTEXITCODE -ne 0) { throw "transactional installer exited with status $LASTEXITCODE" }
|
|
198
198
|
} else {
|
|
@@ -215,7 +215,7 @@ function Install-VigthoriaCLI-Direct {
|
|
|
215
215
|
$tarballPath = Join-Path $releaseTemp "candidate.tgz"
|
|
216
216
|
try {
|
|
217
217
|
if (-not $HOSTED_TARBALL_SHA256) { throw "Direct installation requires a signed manifest SHA-256" }
|
|
218
|
-
if (-not (Test-Path $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
|
|
218
|
+
if (-not $RELEASE_INSTALLER -or -not (Test-Path -LiteralPath $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
|
|
219
219
|
New-Item -ItemType Directory -Path $releaseTemp -ErrorAction Stop | Out-Null
|
|
220
220
|
$tarballUrl = Assert-TrustedReleaseUrl $HOSTED_TARBALL_URL
|
|
221
221
|
Invoke-WebRequest -Uri $tarballUrl -OutFile $tarballPath -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 60 -ErrorAction Stop
|
package/install.sh
CHANGED
|
@@ -26,7 +26,7 @@ else
|
|
|
26
26
|
fi
|
|
27
27
|
|
|
28
28
|
# Configuration
|
|
29
|
-
CLI_VERSION="1.13.
|
|
29
|
+
CLI_VERSION="1.13.34"
|
|
30
30
|
INSTALL_DIR="$HOME/.vigthoria"
|
|
31
31
|
MANIFEST_URL="${VIGTHORIA_UPDATE_MANIFEST_URL:-https://extension.vigthoria.io/downloads/manifest.json}"
|
|
32
32
|
HOSTED_TARBALL_URL="https://extension.vigthoria.io/downloads/vigthoria-cli-${CLI_VERSION}.tgz"
|
|
@@ -257,7 +257,7 @@ install_cli() {
|
|
|
257
257
|
mkdir -p "$INSTALL_DIR"
|
|
258
258
|
|
|
259
259
|
# Option 1: Download and verify the exact hosted release package.
|
|
260
|
-
if [[ -n "$HOSTED_TARBALL_SHA256" ]]; then
|
|
260
|
+
if [[ -n "$HOSTED_TARBALL_SHA256" && -f "$RELEASE_INSTALLER" ]]; then
|
|
261
261
|
echo "Downloading checksum-verified hosted release package..."
|
|
262
262
|
VIGTHORIA_INSTALL_TEMP_ROOT="$(prepare_temp_root)" || return 1
|
|
263
263
|
RELEASE_TMP_DIR="$(mktemp -d "$VIGTHORIA_INSTALL_TEMP_ROOT/install.XXXXXX")"
|