autokap 1.6.0 → 1.6.2
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/browser-pool.d.ts +1 -0
- package/dist/browser-pool.js +2 -0
- package/dist/browser.js +13 -1
- package/dist/cli-contract.d.ts +6 -0
- package/dist/cli-doctor.d.ts +1 -0
- package/dist/cli-doctor.js +121 -8
- package/dist/cli-runner.js +22 -8
- package/dist/cli.js +1 -0
- package/dist/execution-schema.d.ts +6 -0
- package/dist/execution-schema.js +12 -0
- package/dist/execution-types.d.ts +52 -2
- package/dist/mockup.d.ts +66 -2
- package/dist/mockup.js +31 -14
- package/dist/opcode-runner.js +24 -0
- package/dist/program-signing.d.ts +3 -0
- package/dist/program-signing.js +4 -0
- package/dist/transform-browser-url.d.ts +6 -0
- package/dist/transform-browser-url.js +28 -0
- package/dist/types.d.ts +11 -0
- package/dist/video-narration-schema.d.ts +3 -0
- package/dist/web-playwright-local.d.ts +2 -0
- package/dist/web-playwright-local.js +0 -0
- package/package.json +5 -1
package/dist/browser-pool.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare class BrowserPool {
|
|
|
19
19
|
lang?: string;
|
|
20
20
|
colorScheme?: 'light' | 'dark';
|
|
21
21
|
storageState?: BrowserStorageState;
|
|
22
|
+
extraHttpHeaders?: Record<string, string>;
|
|
22
23
|
}): Promise<BrowserContext>;
|
|
23
24
|
/**
|
|
24
25
|
* Release a context back to the pool. Closes the context and unblocks
|
package/dist/browser-pool.js
CHANGED
|
@@ -67,12 +67,14 @@ class BrowserPool {
|
|
|
67
67
|
await new Promise((resolve) => this.queue.push(resolve));
|
|
68
68
|
}
|
|
69
69
|
await this.ensureBrowser();
|
|
70
|
+
const extra = options?.extraHttpHeaders;
|
|
70
71
|
const context = await this.browser.newContext({
|
|
71
72
|
viewport,
|
|
72
73
|
deviceScaleFactor: normalizeDeviceScaleFactor(deviceScaleFactor),
|
|
73
74
|
locale: options?.lang ? options.lang : 'en-US',
|
|
74
75
|
colorScheme: options?.colorScheme ?? 'light',
|
|
75
76
|
storageState: options?.storageState,
|
|
77
|
+
...(extra && Object.keys(extra).length > 0 ? { extraHTTPHeaders: extra } : {}),
|
|
76
78
|
});
|
|
77
79
|
this.activeContexts++;
|
|
78
80
|
this.captureCount++;
|
package/dist/browser.js
CHANGED
|
@@ -911,6 +911,7 @@ export class Browser {
|
|
|
911
911
|
lang: langToLocale(options.lang ?? 'en'),
|
|
912
912
|
colorScheme: options.colorScheme ?? 'light',
|
|
913
913
|
storageState: options.storageState,
|
|
914
|
+
extraHttpHeaders: options.extraHttpHeaders,
|
|
914
915
|
});
|
|
915
916
|
instance.page = await instance.context.newPage();
|
|
916
917
|
instance.poolContext = true;
|
|
@@ -1030,6 +1031,9 @@ export class Browser {
|
|
|
1030
1031
|
locale: langToLocale(options.lang ?? 'en'),
|
|
1031
1032
|
colorScheme: options.colorScheme ?? 'light',
|
|
1032
1033
|
storageState: options.storageState,
|
|
1034
|
+
...(options.extraHttpHeaders && Object.keys(options.extraHttpHeaders).length > 0
|
|
1035
|
+
? { extraHTTPHeaders: options.extraHttpHeaders }
|
|
1036
|
+
: {}),
|
|
1033
1037
|
};
|
|
1034
1038
|
// Dedicated browser process for clip capture. Not pooled because clip
|
|
1035
1039
|
// capture installs context-level init scripts (cursor overlay). Cloud Run
|
|
@@ -5368,7 +5372,13 @@ export class Browser {
|
|
|
5368
5372
|
async setLanguage(lang) {
|
|
5369
5373
|
const context = this.ensureContext();
|
|
5370
5374
|
const page = this.ensurePage();
|
|
5371
|
-
|
|
5375
|
+
// `setExtraHTTPHeaders` REPLACES the header map — merge with the
|
|
5376
|
+
// environment-level auth headers so a SET_LOCALE opcode doesn't strip
|
|
5377
|
+
// them mid-run.
|
|
5378
|
+
await context.setExtraHTTPHeaders({
|
|
5379
|
+
...(this.options.extraHttpHeaders ?? {}),
|
|
5380
|
+
'Accept-Language': lang,
|
|
5381
|
+
});
|
|
5372
5382
|
await page.addInitScript((locale) => {
|
|
5373
5383
|
Object.defineProperty(navigator, 'language', { get: () => locale, configurable: true });
|
|
5374
5384
|
Object.defineProperty(navigator, 'languages', { get: () => [locale], configurable: true });
|
|
@@ -5485,12 +5495,14 @@ export class Browser {
|
|
|
5485
5495
|
return this.context;
|
|
5486
5496
|
}
|
|
5487
5497
|
buildContextOptions() {
|
|
5498
|
+
const extra = this.options.extraHttpHeaders;
|
|
5488
5499
|
return {
|
|
5489
5500
|
viewport: this.options.viewport,
|
|
5490
5501
|
deviceScaleFactor: normalizeDeviceScaleFactor(this.options.deviceScaleFactor),
|
|
5491
5502
|
locale: langToLocale(this.options.lang ?? 'en'),
|
|
5492
5503
|
colorScheme: this.options.colorScheme ?? 'light',
|
|
5493
5504
|
storageState: this.options.storageState,
|
|
5505
|
+
...(extra && Object.keys(extra).length > 0 ? { extraHTTPHeaders: extra } : {}),
|
|
5494
5506
|
};
|
|
5495
5507
|
}
|
|
5496
5508
|
}
|
package/dist/cli-contract.d.ts
CHANGED
|
@@ -114,6 +114,12 @@ export interface ArtifactUploadMetadata {
|
|
|
114
114
|
mimeType: string;
|
|
115
115
|
captureType: "fullpage" | "element";
|
|
116
116
|
captureUrl: string;
|
|
117
|
+
/**
|
|
118
|
+
* Document title captured from the page at screenshot time (Playwright's
|
|
119
|
+
* `page.title()`). Server uses it as the tab title in browser mockups; falls
|
|
120
|
+
* back to `parsed.hostname` when absent (older CLIs do not send it).
|
|
121
|
+
*/
|
|
122
|
+
pageTitle?: string | null;
|
|
117
123
|
lang: string;
|
|
118
124
|
theme: "light" | "dark";
|
|
119
125
|
deviceFrame?: string | null;
|
package/dist/cli-doctor.d.ts
CHANGED
package/dist/cli-doctor.js
CHANGED
|
@@ -148,8 +148,28 @@ async function detectAgent() {
|
|
|
148
148
|
}
|
|
149
149
|
return null;
|
|
150
150
|
}
|
|
151
|
-
async function
|
|
151
|
+
async function parseSkillVersion(skillPath) {
|
|
152
|
+
try {
|
|
153
|
+
const raw = await fs.readFile(skillPath, 'utf8');
|
|
154
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
155
|
+
if (!match)
|
|
156
|
+
return null;
|
|
157
|
+
const yamlText = match[1];
|
|
158
|
+
for (const line of yamlText.split(/\r?\n/)) {
|
|
159
|
+
const m = line.match(/^\s*version\s*:\s*(.+?)\s*$/);
|
|
160
|
+
if (m) {
|
|
161
|
+
return m[1].replace(/^["']|["']$/g, '').trim() || null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function checkSkill(extras, agentOverride) {
|
|
152
171
|
const agent = agentOverride ?? (await detectAgent());
|
|
172
|
+
extras.agentDetected = agent;
|
|
153
173
|
if (!agent) {
|
|
154
174
|
return {
|
|
155
175
|
name: 'AI agent skill',
|
|
@@ -160,12 +180,16 @@ async function checkSkill(agentOverride) {
|
|
|
160
180
|
}
|
|
161
181
|
const skillPath = path.join(process.cwd(), AGENT_PATHS[agent]);
|
|
162
182
|
if (await pathExists(skillPath)) {
|
|
183
|
+
extras.skillPath = AGENT_PATHS[agent];
|
|
184
|
+
extras.skillVersion = await parseSkillVersion(skillPath);
|
|
185
|
+
const versionSuffix = extras.skillVersion ? ` (v${extras.skillVersion})` : '';
|
|
163
186
|
return {
|
|
164
187
|
name: 'AI agent skill',
|
|
165
188
|
status: 'ok',
|
|
166
|
-
message: `${agent}: ${AGENT_PATHS[agent]}`,
|
|
189
|
+
message: `${agent}: ${AGENT_PATHS[agent]}${versionSuffix}`,
|
|
167
190
|
};
|
|
168
191
|
}
|
|
192
|
+
extras.skillPath = AGENT_PATHS[agent];
|
|
169
193
|
return {
|
|
170
194
|
name: 'AI agent skill',
|
|
171
195
|
status: 'warn',
|
|
@@ -181,6 +205,55 @@ async function checkSkill(agentOverride) {
|
|
|
181
205
|
},
|
|
182
206
|
};
|
|
183
207
|
}
|
|
208
|
+
async function checkCliKeyValid() {
|
|
209
|
+
let config = null;
|
|
210
|
+
try {
|
|
211
|
+
config = await readConfig();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
config = null;
|
|
215
|
+
}
|
|
216
|
+
if (!config || !config.apiKey) {
|
|
217
|
+
return {
|
|
218
|
+
name: 'CLI key valid',
|
|
219
|
+
status: 'warn',
|
|
220
|
+
message: 'No CLI key configured locally — skipping remote validation.',
|
|
221
|
+
fixCommand: 'autokap init --cli-key <key>',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetch(`${config.apiBaseUrl}/api/cli/validate`, {
|
|
226
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
227
|
+
});
|
|
228
|
+
if (res.ok) {
|
|
229
|
+
return {
|
|
230
|
+
name: 'CLI key valid',
|
|
231
|
+
status: 'ok',
|
|
232
|
+
message: `Key validated against ${config.apiBaseUrl}.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (res.status === 401 || res.status === 403) {
|
|
236
|
+
return {
|
|
237
|
+
name: 'CLI key valid',
|
|
238
|
+
status: 'fail',
|
|
239
|
+
message: `Server rejected the CLI key (HTTP ${res.status}). It was likely revoked or rotated.`,
|
|
240
|
+
fixCommand: 'autokap init --cli-key <new-key>',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
name: 'CLI key valid',
|
|
245
|
+
status: 'warn',
|
|
246
|
+
message: `Validation endpoint returned HTTP ${res.status}.`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
name: 'CLI key valid',
|
|
252
|
+
status: 'warn',
|
|
253
|
+
message: `Cannot reach validation endpoint: ${err.message}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
184
257
|
async function checkCliVersion(currentVersion) {
|
|
185
258
|
const latest = await getCachedOrFetchLatest();
|
|
186
259
|
if (!latest) {
|
|
@@ -247,28 +320,68 @@ function printCheck(result) {
|
|
|
247
320
|
}
|
|
248
321
|
console.log('');
|
|
249
322
|
}
|
|
323
|
+
function buildJsonReport(results, extras, currentVersion) {
|
|
324
|
+
const ok = results.filter(r => r.status === 'ok').length;
|
|
325
|
+
const warn = results.filter(r => r.status === 'warn').length;
|
|
326
|
+
const fail = results.filter(r => r.status === 'fail').length;
|
|
327
|
+
return {
|
|
328
|
+
cli_version: currentVersion,
|
|
329
|
+
agent_detected: extras.agentDetected,
|
|
330
|
+
skill_path: extras.skillPath,
|
|
331
|
+
skill_version: extras.skillVersion,
|
|
332
|
+
checks: results.map(r => ({
|
|
333
|
+
name: r.name,
|
|
334
|
+
status: r.status,
|
|
335
|
+
message: r.message,
|
|
336
|
+
fixable: Boolean(r.fixFn),
|
|
337
|
+
fix_command: r.fixCommand ?? null,
|
|
338
|
+
})),
|
|
339
|
+
summary: { ok, warn, fail, all_ok: fail === 0 && warn === 0 },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
250
342
|
export async function runDoctor(opts, currentVersion) {
|
|
251
343
|
const agentOverride = opts.agent
|
|
252
344
|
? opts.agent.toLowerCase()
|
|
253
345
|
: undefined;
|
|
346
|
+
const jsonMode = Boolean(opts.json);
|
|
254
347
|
if (agentOverride && !(agentOverride in AGENT_PATHS)) {
|
|
255
|
-
|
|
348
|
+
if (jsonMode) {
|
|
349
|
+
process.stdout.write(JSON.stringify({
|
|
350
|
+
error: `Unknown agent "${opts.agent}". Supported: ${Object.keys(AGENT_PATHS).join(', ')}`,
|
|
351
|
+
}) + '\n');
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
logger.error(`Unknown agent "${opts.agent}". Supported: ${Object.keys(AGENT_PATHS).join(', ')}`);
|
|
355
|
+
}
|
|
256
356
|
process.exit(1);
|
|
257
357
|
}
|
|
258
|
-
|
|
259
|
-
|
|
358
|
+
if (!jsonMode) {
|
|
359
|
+
console.log(chalk.bold('\nautokap doctor'));
|
|
360
|
+
console.log(chalk.gray(`Checking environment for autokap ${currentVersion}...\n`));
|
|
361
|
+
}
|
|
362
|
+
const extras = {
|
|
363
|
+
agentDetected: null,
|
|
364
|
+
skillPath: null,
|
|
365
|
+
skillVersion: null,
|
|
366
|
+
};
|
|
260
367
|
const results = [];
|
|
261
368
|
results.push(checkNodeVersion());
|
|
262
369
|
results.push(await checkChromium());
|
|
263
370
|
results.push(await checkFfmpeg());
|
|
264
371
|
results.push(await checkConfig());
|
|
265
|
-
results.push(await
|
|
372
|
+
results.push(await checkCliKeyValid());
|
|
373
|
+
results.push(await checkSkill(extras, agentOverride));
|
|
266
374
|
results.push(await checkCliVersion(currentVersion));
|
|
375
|
+
const failures = results.filter(r => r.status === 'fail');
|
|
376
|
+
const warnings = results.filter(r => r.status === 'warn');
|
|
377
|
+
if (jsonMode) {
|
|
378
|
+
const report = buildJsonReport(results, extras, currentVersion);
|
|
379
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
380
|
+
process.exit(failures.length > 0 ? 1 : 0);
|
|
381
|
+
}
|
|
267
382
|
for (const r of results) {
|
|
268
383
|
printCheck(r);
|
|
269
384
|
}
|
|
270
|
-
const failures = results.filter(r => r.status === 'fail');
|
|
271
|
-
const warnings = results.filter(r => r.status === 'warn');
|
|
272
385
|
if (opts.fix) {
|
|
273
386
|
const fixable = results.filter(r => r.status !== 'ok' && r.fixFn);
|
|
274
387
|
if (fixable.length === 0) {
|
package/dist/cli-runner.js
CHANGED
|
@@ -25,7 +25,8 @@ import { parseProgram } from './execution-schema.js';
|
|
|
25
25
|
import { buildCursorOverlayScript } from './cursor-overlay-script.js';
|
|
26
26
|
import { CLI_VERSION_HEADER, } from './cli-contract.js';
|
|
27
27
|
import { postProcessClipRecording } from './clip-postprocess.js';
|
|
28
|
-
import { applyDeviceFrame } from './mockup.js';
|
|
28
|
+
import { applyDeviceFrame, seedDeviceConfigs } from './mockup.js';
|
|
29
|
+
import { transformBrowserUrl } from './transform-browser-url.js';
|
|
29
30
|
import { localizeStatusBar } from './status-bar-l10n.js';
|
|
30
31
|
import { logger } from './logger.js';
|
|
31
32
|
import { callLLM } from './llm-provider.js';
|
|
@@ -166,6 +167,11 @@ export async function runCapture(options) {
|
|
|
166
167
|
presetId: options.presetId,
|
|
167
168
|
};
|
|
168
169
|
}
|
|
170
|
+
// Custom device frames live in Supabase (table `device_mockups`) and the
|
|
171
|
+
// end-user CLI does not have the service role key. The server pre-fetches
|
|
172
|
+
// every device referenced by `variants[].deviceFrame` and embeds the rows
|
|
173
|
+
// here; we seed mockup.ts so the export pipeline can apply them locally.
|
|
174
|
+
seedDeviceConfigs(program.deviceConfigs ?? null);
|
|
169
175
|
const runId = randomUUID();
|
|
170
176
|
let videoAudioAssets;
|
|
171
177
|
let videoAudioAssetsByLocale;
|
|
@@ -248,6 +254,7 @@ export async function runCapture(options) {
|
|
|
248
254
|
lang: variant.locale,
|
|
249
255
|
colorScheme: variant.theme,
|
|
250
256
|
storageState: program.preconditions.storageState,
|
|
257
|
+
extraHttpHeaders: program.environmentHttpHeaders,
|
|
251
258
|
};
|
|
252
259
|
let recordingDir;
|
|
253
260
|
let browser;
|
|
@@ -686,6 +693,7 @@ async function uploadResults(config, program, result, runId = randomUUID()) {
|
|
|
686
693
|
healerPatches: sanitizedHealerPatches,
|
|
687
694
|
runResult: strippedRunResult,
|
|
688
695
|
totalDurationMs: result.totalDurationMs,
|
|
696
|
+
detectedAppVersion: result.detectedAppVersion ?? undefined,
|
|
689
697
|
variantSummaries: result.variantResults.map(v => ({
|
|
690
698
|
variantId: v.variantId,
|
|
691
699
|
success: v.success,
|
|
@@ -883,6 +891,9 @@ async function uploadArtifactMultipart(config, program, runId, job, filename) {
|
|
|
883
891
|
formData.append('mimeType', artifact.mimeType);
|
|
884
892
|
formData.append('captureType', artifact.captureType ?? 'fullpage');
|
|
885
893
|
formData.append('captureUrl', artifact.captureUrl ?? program.baseUrl);
|
|
894
|
+
if (artifact.pageTitle) {
|
|
895
|
+
formData.append('pageTitle', artifact.pageTitle);
|
|
896
|
+
}
|
|
886
897
|
formData.append('lang', variantSpec?.locale ?? 'en');
|
|
887
898
|
formData.append('theme', variantSpec?.theme ?? 'light');
|
|
888
899
|
if (variantSpec?.deviceFrame) {
|
|
@@ -960,6 +971,7 @@ async function prepareDirectArtifactUpload(params) {
|
|
|
960
971
|
mimeType: artifact.mimeType,
|
|
961
972
|
captureType: artifact.captureType ?? 'fullpage',
|
|
962
973
|
captureUrl: artifact.captureUrl ?? program.baseUrl,
|
|
974
|
+
pageTitle: artifact.pageTitle ?? null,
|
|
963
975
|
lang: variantSpec?.locale ?? 'en',
|
|
964
976
|
theme: variantSpec?.theme ?? 'light',
|
|
965
977
|
deviceFrame: variantSpec?.deviceFrame ?? null,
|
|
@@ -1038,7 +1050,7 @@ async function prepareScreenshotBufferForDirectUpload(input, metadata, program,
|
|
|
1038
1050
|
?? 2,
|
|
1039
1051
|
showStatusBar: artifactPlan.applyStatusBar ?? false,
|
|
1040
1052
|
statusBar: localizeStatusBar({}, metadata.lang),
|
|
1041
|
-
browserBar: buildCliBrowserBar(metadata.captureUrl, metadata.theme, tabIcon),
|
|
1053
|
+
browserBar: buildCliBrowserBar(metadata.captureUrl, metadata.theme, tabIcon, { publicUrl: program.publicUrl, pageTitle: metadata.pageTitle ?? null }),
|
|
1042
1054
|
});
|
|
1043
1055
|
}
|
|
1044
1056
|
if (artifactPlan?.format?.screenshotFormat === 'jpeg') {
|
|
@@ -1121,14 +1133,16 @@ function normalizeCliDeviceScaleFactor(value) {
|
|
|
1121
1133
|
return null;
|
|
1122
1134
|
return Math.max(0.5, Math.min(4, Number(value)));
|
|
1123
1135
|
}
|
|
1124
|
-
function buildCliBrowserBar(captureUrl, colorScheme, tabIcon) {
|
|
1136
|
+
function buildCliBrowserBar(captureUrl, colorScheme, tabIcon, options = {}) {
|
|
1125
1137
|
if (!captureUrl)
|
|
1126
1138
|
return undefined;
|
|
1139
|
+
const displayUrl = transformBrowserUrl(captureUrl, options.publicUrl);
|
|
1140
|
+
const explicitTitle = options.pageTitle?.trim() || null;
|
|
1127
1141
|
try {
|
|
1128
|
-
const parsed = new URL(
|
|
1142
|
+
const parsed = new URL(displayUrl);
|
|
1129
1143
|
return {
|
|
1130
|
-
url:
|
|
1131
|
-
pageTitle: parsed.hostname,
|
|
1144
|
+
url: displayUrl,
|
|
1145
|
+
pageTitle: explicitTitle ?? parsed.hostname,
|
|
1132
1146
|
colorScheme,
|
|
1133
1147
|
tabIconUrl: tabIcon
|
|
1134
1148
|
? `data:${tabIcon.mimeType};base64,${tabIcon.buffer.toString('base64')}`
|
|
@@ -1137,8 +1151,8 @@ function buildCliBrowserBar(captureUrl, colorScheme, tabIcon) {
|
|
|
1137
1151
|
}
|
|
1138
1152
|
catch {
|
|
1139
1153
|
return {
|
|
1140
|
-
url:
|
|
1141
|
-
pageTitle:
|
|
1154
|
+
url: displayUrl,
|
|
1155
|
+
pageTitle: explicitTitle ?? displayUrl,
|
|
1142
1156
|
colorScheme,
|
|
1143
1157
|
...(tabIcon
|
|
1144
1158
|
? { tabIconUrl: `data:${tabIcon.mimeType};base64,${tabIcon.buffer.toString('base64')}` }
|
package/dist/cli.js
CHANGED
|
@@ -1637,6 +1637,7 @@ program
|
|
|
1637
1637
|
.description('Check environment and dependencies (Node, Chromium, ffmpeg, config, skill, version)')
|
|
1638
1638
|
.option('--fix', 'Attempt to auto-fix detected issues (Chromium install, skill reinstall)', false)
|
|
1639
1639
|
.option('--agent <name>', 'Override agent for skill check: claude, codex, cursor, windsurf, copilot')
|
|
1640
|
+
.option('--json', 'Output a structured JSON report to stdout (for AI assistants and tooling)', false)
|
|
1640
1641
|
.action(async (opts) => {
|
|
1641
1642
|
const { runDoctor } = await import('./cli-doctor.js');
|
|
1642
1643
|
await runDoctor(opts, version);
|
|
@@ -2226,6 +2226,9 @@ export declare const ExecutionProgramSchema: z.ZodObject<{
|
|
|
2226
2226
|
defaultValues: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
2227
2227
|
replaceExisting: z.ZodOptional<z.ZodBoolean>;
|
|
2228
2228
|
}, z.core.$strict>>>;
|
|
2229
|
+
deviceConfigs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
2230
|
+
publicUrl: z.ZodOptional<z.ZodString>;
|
|
2231
|
+
environmentHttpHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
2229
2232
|
}, z.core.$strict>;
|
|
2230
2233
|
export declare const HealerPatchSchema: z.ZodObject<{
|
|
2231
2234
|
opcodeIndex: z.ZodNumber;
|
|
@@ -4923,4 +4926,7 @@ export declare function safeParseProgramResult(data: unknown): z.ZodSafeParseRes
|
|
|
4923
4926
|
defaultValues: Record<string, string>[];
|
|
4924
4927
|
replaceExisting?: boolean | undefined;
|
|
4925
4928
|
}[] | undefined;
|
|
4929
|
+
deviceConfigs?: Record<string, Record<string, unknown>> | undefined;
|
|
4930
|
+
publicUrl?: string | undefined;
|
|
4931
|
+
environmentHttpHeaders?: Record<string, string> | undefined;
|
|
4926
4932
|
}>;
|
package/dist/execution-schema.js
CHANGED
|
@@ -652,6 +652,18 @@ export const ExecutionProgramSchema = z.object({
|
|
|
652
652
|
compiledAt: z.string().datetime(),
|
|
653
653
|
compiledWith: z.string().optional(),
|
|
654
654
|
mockDataGroups: z.array(MockDataGroupSchema).optional(),
|
|
655
|
+
// Server-embedded device frame configs. Shape is governed by the
|
|
656
|
+
// `device_mockups.config` JSON column; we accept anything the server stores
|
|
657
|
+
// and let mockup.ts validate structurally at use time.
|
|
658
|
+
deviceConfigs: z.record(z.string().min(1), z.record(z.string(), z.unknown())).optional(),
|
|
659
|
+
// Project-level public URL used to rewrite the captured origin when
|
|
660
|
+
// decorating browser mockups. Validated only as a non-empty string here;
|
|
661
|
+
// the runtime parses it with `new URL()` and falls back gracefully.
|
|
662
|
+
publicUrl: z.string().min(1).optional(),
|
|
663
|
+
// Auth headers attached to the resolved project environment. Key/value
|
|
664
|
+
// pairs that Playwright will inject as `extraHTTPHeaders` on the
|
|
665
|
+
// BrowserContext so protected staging/preview URLs load successfully.
|
|
666
|
+
environmentHttpHeaders: z.record(z.string().min(1), z.string().min(1)).optional(),
|
|
655
667
|
}).strict().superRefine((value, ctx) => {
|
|
656
668
|
if (value.mediaMode !== value.artifactPlan.mediaMode) {
|
|
657
669
|
ctx.addIssue({
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* preset (natural language) -> ExecutionProgram (typed IR) -> deterministic runtime
|
|
6
6
|
*/
|
|
7
7
|
import type { AKTree, BrowserStorageState, BrowserSessionStorageState, OutscaleConfig, VideoCursorTheme, VideoPageSignals } from './types.js';
|
|
8
|
-
import type { MockupOptions } from './mockup.js';
|
|
8
|
+
import type { DeviceConfig, MockupOptions } from './mockup.js';
|
|
9
|
+
export type { DeviceConfig };
|
|
9
10
|
/** Sentinel value that resolves to the current variant's locale or theme at runtime */
|
|
10
11
|
export declare const VARIANT_PLACEHOLDER: "$variant";
|
|
11
12
|
export declare const OPCODE_KINDS: readonly ["NAVIGATE", "DISMISS_OVERLAYS", "ASSERT_ROUTE", "ASSERT_SURFACE", "CLICK", "TYPE", "PRESS_KEY", "WAIT_FOR", "SLEEP", "SET_LOCALE", "SET_THEME", "SCROLL", "CAPTURE_SCREENSHOT", "BEGIN_CLIP", "END_CLIP", "HOVER", "SELECT_OPTION", "CHECK", "DOUBLE_CLICK", "DRAG", "CLONE_ELEMENT", "INJECT_MOCK_DATA", "REMOVE_ELEMENT", "SET_ATTRIBUTE"];
|
|
@@ -557,6 +558,28 @@ export interface ExecutionProgram {
|
|
|
557
558
|
* Compiled from PresetConfig.mockDataInjection.groups by the server.
|
|
558
559
|
*/
|
|
559
560
|
mockDataGroups?: MockDataGroup[];
|
|
561
|
+
/**
|
|
562
|
+
* Server-embedded device frame configs for every `deviceFrame` referenced by
|
|
563
|
+
* `variants[]`. The CLI seeds its mockup engine with these so it does not
|
|
564
|
+
* need direct Supabase access (end-users do not have the service role key).
|
|
565
|
+
* Keyed by the exact deviceFrame string used in the variants.
|
|
566
|
+
*/
|
|
567
|
+
deviceConfigs?: Record<string, DeviceConfig>;
|
|
568
|
+
/**
|
|
569
|
+
* Project-level public URL used to decorate browser mockups. The CLI
|
|
570
|
+
* substitutes the captured origin (typically a local dev server) with this
|
|
571
|
+
* value via `transformBrowserUrl` before baking it into the browser bar.
|
|
572
|
+
* Server-resolved from `projects.public_url`.
|
|
573
|
+
*/
|
|
574
|
+
publicUrl?: string;
|
|
575
|
+
/**
|
|
576
|
+
* Auth headers attached to the resolved project environment (Bearer token,
|
|
577
|
+
* Vercel protection bypass, x-api-key, etc.). Injected into the Playwright
|
|
578
|
+
* BrowserContext so requests to protected staging/preview URLs go through.
|
|
579
|
+
* Decrypted server-side from `project_environments.auth_headers_encrypted`
|
|
580
|
+
* and embedded in the signed program envelope.
|
|
581
|
+
*/
|
|
582
|
+
environmentHttpHeaders?: Record<string, string>;
|
|
560
583
|
}
|
|
561
584
|
export interface CircuitBreakerConfig {
|
|
562
585
|
/** Max recovery attempts per opcode. Default: 3 */
|
|
@@ -605,6 +628,13 @@ export interface VariantResult {
|
|
|
605
628
|
durationMs: number;
|
|
606
629
|
/** Artifact buffers produced */
|
|
607
630
|
artifacts: ArtifactResult[];
|
|
631
|
+
/**
|
|
632
|
+
* App version detected on the captured page (meta tag, window global, or
|
|
633
|
+
* data attribute). Sent to the server with telemetry so freshness tracking
|
|
634
|
+
* picks up the version live during the capture instead of waiting for the
|
|
635
|
+
* hourly cron.
|
|
636
|
+
*/
|
|
637
|
+
detectedAppVersion?: string | null;
|
|
608
638
|
error?: string;
|
|
609
639
|
}
|
|
610
640
|
export interface ArtifactResult {
|
|
@@ -622,6 +652,8 @@ export interface ArtifactResult {
|
|
|
622
652
|
altText?: string;
|
|
623
653
|
/** Final URL at the time the artifact was produced */
|
|
624
654
|
captureUrl?: string;
|
|
655
|
+
/** Document title at the time the artifact was produced (page.title()) */
|
|
656
|
+
pageTitle?: string;
|
|
625
657
|
/** Step index that produced the artifact */
|
|
626
658
|
stepIndex?: number;
|
|
627
659
|
/** Human-readable label for the artifact */
|
|
@@ -725,6 +757,12 @@ export interface RunResult {
|
|
|
725
757
|
* array for `screenshot` and `clip` modes.
|
|
726
758
|
*/
|
|
727
759
|
opcodeTimings: OpcodeTiming[];
|
|
760
|
+
/**
|
|
761
|
+
* First non-null `detectedAppVersion` from the variants. Used by the server
|
|
762
|
+
* telemetry route to bypass the cron-detected version and stamp the preset
|
|
763
|
+
* with the version actually captured on the page.
|
|
764
|
+
*/
|
|
765
|
+
detectedAppVersion?: string | null;
|
|
728
766
|
error?: string;
|
|
729
767
|
}
|
|
730
768
|
export interface WaitCondition {
|
|
@@ -842,6 +880,19 @@ export interface RuntimeAdapter {
|
|
|
842
880
|
buffer: Buffer;
|
|
843
881
|
mimeType: string;
|
|
844
882
|
} | null>;
|
|
883
|
+
/**
|
|
884
|
+
* Document title of the current page (Playwright's `page.title()`). Captured
|
|
885
|
+
* at screenshot time and stored on the artifact metadata so browser mockups
|
|
886
|
+
* can render the actual page title instead of just the hostname. Optional —
|
|
887
|
+
* adapters that cannot resolve a title should leave this method off.
|
|
888
|
+
*/
|
|
889
|
+
getPageTitle?(): Promise<string | null>;
|
|
890
|
+
/**
|
|
891
|
+
* Read the captured app's version from the live page (meta tag, window
|
|
892
|
+
* global, or data attribute). Mirrors `extractAppVersionFromHtml` server-side
|
|
893
|
+
* and `__AUTOKAP_VERSION__` lookup. Returns null if no marker is present.
|
|
894
|
+
*/
|
|
895
|
+
detectAppVersion?(): Promise<string | null>;
|
|
845
896
|
close(): Promise<void>;
|
|
846
897
|
/** Click an element by semantic target. Falls back to selector if target not found. */
|
|
847
898
|
clickByTarget?(opts: ClickByTargetOptions): Promise<void>;
|
|
@@ -939,4 +990,3 @@ export interface RuntimeAdapter {
|
|
|
939
990
|
selector: string;
|
|
940
991
|
}): Promise<void>;
|
|
941
992
|
}
|
|
942
|
-
export {};
|
package/dist/mockup.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface DeviceFrameDefinition {
|
|
|
11
11
|
height: number;
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
export type DeviceOrientation = 'portrait' | 'landscape';
|
|
15
|
+
export interface OrientationConfigData {
|
|
15
16
|
screen: {
|
|
16
17
|
logicalWidth: number;
|
|
17
18
|
logicalHeight: number;
|
|
@@ -103,6 +104,69 @@ interface OrientationConfigData {
|
|
|
103
104
|
right?: string;
|
|
104
105
|
};
|
|
105
106
|
}
|
|
107
|
+
export interface DeviceConfig {
|
|
108
|
+
id: string;
|
|
109
|
+
name: string;
|
|
110
|
+
category: DeviceCategory;
|
|
111
|
+
platform: string;
|
|
112
|
+
frameOrientation?: 'portrait' | 'landscape';
|
|
113
|
+
supportedOrientations?: ('portrait' | 'landscape')[];
|
|
114
|
+
orientations?: {
|
|
115
|
+
portrait?: OrientationConfigData;
|
|
116
|
+
landscape?: OrientationConfigData;
|
|
117
|
+
};
|
|
118
|
+
/** Row-level frame_url (shared fallback for all orientations) */
|
|
119
|
+
_rowFrameUrl?: string;
|
|
120
|
+
screen: {
|
|
121
|
+
logicalWidth: number;
|
|
122
|
+
logicalHeight: number;
|
|
123
|
+
scale: number;
|
|
124
|
+
cornerRadius: number;
|
|
125
|
+
};
|
|
126
|
+
viewport: {
|
|
127
|
+
width: number;
|
|
128
|
+
height: number;
|
|
129
|
+
};
|
|
130
|
+
safeArea?: {
|
|
131
|
+
top: number;
|
|
132
|
+
bottom: number;
|
|
133
|
+
left?: number;
|
|
134
|
+
right?: number;
|
|
135
|
+
};
|
|
136
|
+
statusBar?: {
|
|
137
|
+
asset: string;
|
|
138
|
+
height: number;
|
|
139
|
+
width: number;
|
|
140
|
+
type?: StatusBarDeviceType;
|
|
141
|
+
layout?: StatusBarLayout;
|
|
142
|
+
};
|
|
143
|
+
homeIndicator?: {
|
|
144
|
+
width: number;
|
|
145
|
+
height: number;
|
|
146
|
+
cornerRadius: number;
|
|
147
|
+
bottomOffset: number;
|
|
148
|
+
};
|
|
149
|
+
frame: {
|
|
150
|
+
type: 'png' | 'svg';
|
|
151
|
+
asset: string;
|
|
152
|
+
width: number;
|
|
153
|
+
height: number;
|
|
154
|
+
screenRect: {
|
|
155
|
+
x: number;
|
|
156
|
+
y: number;
|
|
157
|
+
width: number;
|
|
158
|
+
height: number;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
frameRotation?: number;
|
|
162
|
+
frameBehindContent?: boolean;
|
|
163
|
+
windowBorder?: OrientationConfigData['windowBorder'];
|
|
164
|
+
frameDarkUrl?: string;
|
|
165
|
+
browserBarZones?: OrientationConfigData['browserBarZones'];
|
|
166
|
+
browserStyle?: OrientationConfigData['browserStyle'];
|
|
167
|
+
adminShowStatusBar?: OrientationConfigData['adminShowStatusBar'];
|
|
168
|
+
adminForcedSafeAreaColors?: OrientationConfigData['adminForcedSafeAreaColors'];
|
|
169
|
+
}
|
|
106
170
|
export type MockupOrientation = 'portrait' | 'landscape';
|
|
107
171
|
export interface MockupOptions {
|
|
108
172
|
orientation?: MockupOrientation;
|
|
@@ -184,6 +248,7 @@ export interface ResolvedDeviceFrameDescriptor {
|
|
|
184
248
|
disableOverlays: boolean;
|
|
185
249
|
}
|
|
186
250
|
export declare function invalidateDeviceConfigCache(): void;
|
|
251
|
+
export declare function seedDeviceConfigs(configs: Record<string, DeviceConfig> | null | undefined): void;
|
|
187
252
|
export declare function resolveDeviceFrameDescriptor(id: DeviceFrameId, options?: {
|
|
188
253
|
orientation?: MockupOrientation;
|
|
189
254
|
}): Promise<ResolvedDeviceFrameDescriptor | null>;
|
|
@@ -191,4 +256,3 @@ export declare function rasterizeDeviceFrame(descriptor: ResolvedDeviceFrameDesc
|
|
|
191
256
|
export declare function getDeviceFrames(): Promise<DeviceFrameDefinition[]>;
|
|
192
257
|
export declare function getDeviceFrame(id: DeviceFrameId): Promise<DeviceFrameDefinition | undefined>;
|
|
193
258
|
export declare function applyDeviceFrame(screenshot: Buffer, deviceId: DeviceFrameId, options?: MockupOptions): Promise<Buffer>;
|
|
194
|
-
export {};
|
package/dist/mockup.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { renderStatusBarBuffer } from './status-bar-render.js';
|
|
6
6
|
import { generateBrowserBarSvg } from './browser-bar.js';
|
|
7
7
|
import { computeMockupLayout } from './mockup-html.js';
|
|
8
|
+
import { logger } from './logger.js';
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
10
11
|
const DEVICES_DIR = path.join(__dirname, '..', 'assets', 'devices');
|
|
@@ -144,7 +145,23 @@ function resolveOrientationConfig(config, requestedOrientation) {
|
|
|
144
145
|
adminForcedSafeAreaColors: config.adminForcedSafeAreaColors,
|
|
145
146
|
};
|
|
146
147
|
}
|
|
148
|
+
// Server-provided device configs embedded in signed execution programs. When
|
|
149
|
+
// set, these short-circuit `loadDeviceConfigs` — the CLI does not need direct
|
|
150
|
+
// Supabase access (and end-users do not have the service role key) because the
|
|
151
|
+
// server already vetted every device referenced by the program's variants and
|
|
152
|
+
// inlined the rows.
|
|
153
|
+
let seededDeviceConfigs = null;
|
|
154
|
+
export function seedDeviceConfigs(configs) {
|
|
155
|
+
if (!configs || Object.keys(configs).length === 0) {
|
|
156
|
+
seededDeviceConfigs = null;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
seededDeviceConfigs = new Map(Object.entries(configs));
|
|
160
|
+
}
|
|
147
161
|
async function loadDeviceConfigs() {
|
|
162
|
+
if (seededDeviceConfigs && seededDeviceConfigs.size > 0) {
|
|
163
|
+
return seededDeviceConfigs;
|
|
164
|
+
}
|
|
148
165
|
if (configCache && configCache.expiresAt > Date.now()) {
|
|
149
166
|
return configCache.configs;
|
|
150
167
|
}
|
|
@@ -478,12 +495,12 @@ export async function applyDeviceFrame(screenshot, deviceId, options) {
|
|
|
478
495
|
const isBrowserDevice = config.category === 'browser';
|
|
479
496
|
const os = Math.max(0.5, Math.min(4, opts.outputScale));
|
|
480
497
|
const geometry = computeResolvedFrameGeometry(resolved);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
498
|
+
logger.debug(`[mockup] applyDeviceFrame: id=${deviceId}, category=${config.category}, orientation=${requestedOrientation}`);
|
|
499
|
+
logger.debug(`[mockup] hasOrientationConfig=${!!config.orientations?.[requestedOrientation]}, orientations=${JSON.stringify(Object.keys(config.orientations ?? {}))}`);
|
|
500
|
+
logger.debug(`[mockup] resolved.screen: ${JSON.stringify(resolved.screen)}`);
|
|
501
|
+
logger.debug(`[mockup] resolved.frame: w=${resolved.frame.width} h=${resolved.frame.height} asset="${resolved.frame.asset}" url=${resolved.frameUrl ? 'yes' : 'no'}`);
|
|
502
|
+
logger.debug(`[mockup] resolved.safeArea: ${JSON.stringify(resolved.safeArea)}`);
|
|
503
|
+
logger.debug(`[mockup] scale=${scale}, outputScale=${os}`);
|
|
487
504
|
// Browser devices can work without a frame image
|
|
488
505
|
const hasFrame = !!(resolved.frameUrl || resolved.frame.asset);
|
|
489
506
|
let frameData = null;
|
|
@@ -509,7 +526,7 @@ export async function applyDeviceFrame(screenshot, deviceId, options) {
|
|
|
509
526
|
geo.frameWidth = logicalW;
|
|
510
527
|
geo.frameHeight = logicalH;
|
|
511
528
|
geo.screenRect = { x: 0, y: 0, width: logicalW, height: logicalH };
|
|
512
|
-
|
|
529
|
+
logger.debug(`[mockup] frameless browser: logicalW=${logicalW}, logicalH=${logicalH}, os=${os}, geo=${geo.frameWidth}x${geo.frameHeight}`);
|
|
513
530
|
}
|
|
514
531
|
else if (!hasFrame && geo.frameWidth === 0 && geo.frameHeight === 0) {
|
|
515
532
|
// Non-browser frameless fallback
|
|
@@ -551,21 +568,21 @@ export async function applyDeviceFrame(screenshot, deviceId, options) {
|
|
|
551
568
|
});
|
|
552
569
|
const contentW = layout.contentArea.width;
|
|
553
570
|
const contentH = layout.contentArea.height;
|
|
554
|
-
|
|
555
|
-
|
|
571
|
+
logger.debug(`[mockup] hasFrame=${hasFrame}, geo: fw=${geo.frameWidth} fh=${geo.frameHeight} sr=${JSON.stringify(geo.screenRect)}`);
|
|
572
|
+
logger.debug(`[mockup] layout: container=${layout.containerWidth}x${layout.containerHeight} content=${contentW}x${contentH} contentArea=${JSON.stringify(layout.contentArea)}`);
|
|
556
573
|
// Get incoming screenshot dimensions for logging
|
|
557
574
|
const screenshotMeta = await sharp(screenshot).metadata();
|
|
558
|
-
|
|
575
|
+
logger.debug(`[mockup] input screenshot: ${screenshotMeta.width}x${screenshotMeta.height}`);
|
|
559
576
|
const physicalContentW = Math.round(contentW * os);
|
|
560
577
|
const physicalContentH = Math.round(contentH * os);
|
|
561
|
-
|
|
578
|
+
logger.debug(`[mockup] resize target: ${physicalContentW}x${physicalContentH}`);
|
|
562
579
|
if (screenshotMeta.width
|
|
563
580
|
&& screenshotMeta.height
|
|
564
581
|
&& (Math.abs(screenshotMeta.width - physicalContentW) > 1
|
|
565
582
|
|| Math.abs(screenshotMeta.height - physicalContentH) > 1)) {
|
|
566
583
|
const ratioX = physicalContentW / screenshotMeta.width;
|
|
567
584
|
const ratioY = physicalContentH / screenshotMeta.height;
|
|
568
|
-
|
|
585
|
+
logger.debug(`[mockup] screenshot will be resampled: ` +
|
|
569
586
|
`${screenshotMeta.width}x${screenshotMeta.height} -> ` +
|
|
570
587
|
`${physicalContentW}x${physicalContentH} ` +
|
|
571
588
|
`(x=${ratioX.toFixed(4)}, y=${ratioY.toFixed(4)}). ` +
|
|
@@ -597,7 +614,7 @@ export async function applyDeviceFrame(screenshot, deviceId, options) {
|
|
|
597
614
|
colors.leftColor = sanitizeCssColor(opts.safeAreaLeftColor);
|
|
598
615
|
if (opts.safeAreaRightColor)
|
|
599
616
|
colors.rightColor = sanitizeCssColor(opts.safeAreaRightColor);
|
|
600
|
-
|
|
617
|
+
logger.debug(`[mockup] sampled colors: top=${colors.topColor} bottom=${colors.bottomColor} left=${colors.leftColor} right=${colors.rightColor}`);
|
|
601
618
|
// Determine color scheme: use explicit override if provided, otherwise auto-detect from edge colors.
|
|
602
619
|
// Laptops (MacBook) always use dark menu bar (white text on black background).
|
|
603
620
|
const isLaptop = config.category === 'laptop';
|
|
@@ -664,7 +681,7 @@ export async function applyDeviceFrame(screenshot, deviceId, options) {
|
|
|
664
681
|
const renderW = Math.round(geo.frameWidth);
|
|
665
682
|
const renderH = Math.round(geo.frameHeight);
|
|
666
683
|
const cornerRadius = (resolved.screen?.cornerRadius ?? 0) * scale;
|
|
667
|
-
|
|
684
|
+
logger.debug(`[mockup] composeMockup: ${renderW}x${renderH} @${os}x, showBrowserBar=${isBrowserDevice}, windowBorderWidth=${wbw}`);
|
|
668
685
|
// ── Sharp compositing — layer-by-layer mockup assembly ──
|
|
669
686
|
// All dimensions in physical pixels (logical * os).
|
|
670
687
|
const pw = Math.round(renderW * os);
|
package/dist/opcode-runner.js
CHANGED
|
@@ -147,6 +147,7 @@ export async function executeProgram(program, createAdapter, options = {}) {
|
|
|
147
147
|
const completedVariantResults = variantResults.filter((result) => Boolean(result));
|
|
148
148
|
const aborted = options.abortSignal?.aborted && completedVariantResults.length < program.variants.length;
|
|
149
149
|
const success = !aborted && completedVariantResults.length > 0 && completedVariantResults.every(v => v.success);
|
|
150
|
+
const detectedAppVersion = completedVariantResults.reduce((acc, variantResult) => acc ?? (variantResult.detectedAppVersion ?? null), null);
|
|
150
151
|
return {
|
|
151
152
|
programId: program.presetId,
|
|
152
153
|
success,
|
|
@@ -155,6 +156,7 @@ export async function executeProgram(program, createAdapter, options = {}) {
|
|
|
155
156
|
healerPatches: success ? healerPatches : [], // Only propagate patches on success
|
|
156
157
|
opcodeTimings,
|
|
157
158
|
totalDurationMs: Date.now() - startTime,
|
|
159
|
+
detectedAppVersion,
|
|
158
160
|
error: aborted ? 'aborted' : (success ? undefined : completedVariantResults.find(v => !v.success)?.error),
|
|
159
161
|
};
|
|
160
162
|
}
|
|
@@ -234,12 +236,25 @@ async function executeVariant(program, variant, createAdapter, recoveryChain, te
|
|
|
234
236
|
};
|
|
235
237
|
}
|
|
236
238
|
}
|
|
239
|
+
// Best-effort: read the app version from the live page so the server can
|
|
240
|
+
// stamp the preset with what we actually captured (instead of falling back
|
|
241
|
+
// to whatever the hourly cron last detected).
|
|
242
|
+
let detectedAppVersion = null;
|
|
243
|
+
if (adapter.detectAppVersion) {
|
|
244
|
+
try {
|
|
245
|
+
detectedAppVersion = await adapter.detectAppVersion();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
detectedAppVersion = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
237
251
|
return {
|
|
238
252
|
variantId: variant.id,
|
|
239
253
|
success: true,
|
|
240
254
|
opcodeResults,
|
|
241
255
|
durationMs: Date.now() - startTime,
|
|
242
256
|
artifacts,
|
|
257
|
+
detectedAppVersion,
|
|
243
258
|
};
|
|
244
259
|
}
|
|
245
260
|
catch (err) {
|
|
@@ -693,12 +708,21 @@ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, tele
|
|
|
693
708
|
tabIconMimeType = favicon.mimeType;
|
|
694
709
|
}
|
|
695
710
|
}
|
|
711
|
+
// Capture document title so browser mockups can render the real tab
|
|
712
|
+
// title instead of falling back to the hostname.
|
|
713
|
+
let pageTitle;
|
|
714
|
+
if (adapter.getPageTitle) {
|
|
715
|
+
const resolved = await adapter.getPageTitle();
|
|
716
|
+
if (resolved)
|
|
717
|
+
pageTitle = resolved;
|
|
718
|
+
}
|
|
696
719
|
artifacts.push({
|
|
697
720
|
mediaMode: 'screenshot',
|
|
698
721
|
buffer,
|
|
699
722
|
mimeType: 'image/png',
|
|
700
723
|
captureType: opcode.elementSelector ? 'element' : 'fullpage',
|
|
701
724
|
captureUrl,
|
|
725
|
+
pageTitle,
|
|
702
726
|
dimensions: currentVariant?.viewport,
|
|
703
727
|
captureId: opcode.captureId,
|
|
704
728
|
captureName: opcode.captureName ?? opcode.description,
|
|
@@ -1126,6 +1126,9 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
1126
1126
|
defaultValues: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1127
1127
|
replaceExisting: z.ZodOptional<z.ZodBoolean>;
|
|
1128
1128
|
}, z.core.$strict>>>;
|
|
1129
|
+
deviceConfigs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
1130
|
+
publicUrl: z.ZodOptional<z.ZodString>;
|
|
1131
|
+
environmentHttpHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1129
1132
|
}, z.core.$strict>;
|
|
1130
1133
|
signature: z.ZodString;
|
|
1131
1134
|
meta: z.ZodOptional<z.ZodObject<{
|
package/dist/program-signing.js
CHANGED
|
@@ -102,6 +102,10 @@ export function signExecutionProgramEnvelope(params) {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
export function verifySignedExecutionProgramEnvelope(params) {
|
|
105
|
+
// The Zod schema describes `deviceConfigs` as a loose record (the server is
|
|
106
|
+
// the source of truth for the rich DeviceConfig shape). Cast to the strongly
|
|
107
|
+
// typed envelope at this trust boundary — the signature verification below
|
|
108
|
+
// proves the server produced this payload.
|
|
105
109
|
const envelope = SignedExecutionProgramEnvelopeSchema.parse(params.envelope);
|
|
106
110
|
const payload = {
|
|
107
111
|
signedAt: envelope.signedAt,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform a captured URL for display in the browser bar mockup.
|
|
3
|
+
* 1. Replace the origin with `publicUrl` (if provided).
|
|
4
|
+
* 2. Strip UUID v4 path segments.
|
|
5
|
+
*/
|
|
6
|
+
export declare function transformBrowserUrl(capturedUrl: string, publicUrl?: string | null): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
|
+
/**
|
|
3
|
+
* Transform a captured URL for display in the browser bar mockup.
|
|
4
|
+
* 1. Replace the origin with `publicUrl` (if provided).
|
|
5
|
+
* 2. Strip UUID v4 path segments.
|
|
6
|
+
*/
|
|
7
|
+
export function transformBrowserUrl(capturedUrl, publicUrl) {
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = new URL(capturedUrl);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return capturedUrl;
|
|
14
|
+
}
|
|
15
|
+
if (publicUrl) {
|
|
16
|
+
try {
|
|
17
|
+
const pub = new URL(publicUrl);
|
|
18
|
+
parsed = new URL(`${pub.origin}${parsed.pathname}${parsed.search}${parsed.hash}`);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// invalid publicUrl — skip origin replacement
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const segments = parsed.pathname.split("/").filter((s) => s && !UUID_RE.test(s));
|
|
25
|
+
parsed.pathname = "/" + segments.join("/");
|
|
26
|
+
return parsed.toString().replace(/\/$/, "") || parsed.origin;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=transform-browser-url.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -246,6 +246,17 @@ export interface BrowserOptions {
|
|
|
246
246
|
colorScheme?: 'light' | 'dark';
|
|
247
247
|
/** Optional persisted cookies/localStorage captured during preparation. */
|
|
248
248
|
storageState?: BrowserStorageState;
|
|
249
|
+
/**
|
|
250
|
+
* Extra HTTP headers injected on every navigation. Used to carry the
|
|
251
|
+
* environment-level auth (Bearer token, Vercel protection bypass, x-api-key,
|
|
252
|
+
* etc.) configured on the resolved `project_environments` row.
|
|
253
|
+
*
|
|
254
|
+
* These headers are sent to ALL requests the BrowserContext makes during
|
|
255
|
+
* capture, including cross-origin requests (third-party CDN, analytics).
|
|
256
|
+
* The dashboard surfaces a warning so users only put environment-scoped
|
|
257
|
+
* secrets here.
|
|
258
|
+
*/
|
|
259
|
+
extraHttpHeaders?: Record<string, string>;
|
|
249
260
|
}
|
|
250
261
|
export interface OutscaleConfig {
|
|
251
262
|
/** Uniform padding on all 4 sides (pixels). */
|
|
@@ -1152,6 +1152,9 @@ export declare const VideoIngestPayloadSchema: z.ZodObject<{
|
|
|
1152
1152
|
defaultValues: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1153
1153
|
replaceExisting: z.ZodOptional<z.ZodBoolean>;
|
|
1154
1154
|
}, z.core.$strict>>>;
|
|
1155
|
+
deviceConfigs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
1156
|
+
publicUrl: z.ZodOptional<z.ZodString>;
|
|
1157
|
+
environmentHttpHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1155
1158
|
}, z.core.$strict>;
|
|
1156
1159
|
narration: z.ZodOptional<z.ZodObject<{
|
|
1157
1160
|
voice: z.ZodString;
|
|
@@ -27,6 +27,8 @@ export declare class WebPlaywrightLocal implements RuntimeAdapter {
|
|
|
27
27
|
constructor(browser: Browser, recordingDir?: string | undefined);
|
|
28
28
|
navigate(url: string): Promise<void>;
|
|
29
29
|
getCurrentUrl(): Promise<string>;
|
|
30
|
+
getPageTitle(): Promise<string | null>;
|
|
31
|
+
detectAppVersion(): Promise<string | null>;
|
|
30
32
|
getAKTree(): Promise<AKTree>;
|
|
31
33
|
getPageSignals(): Promise<VideoPageSignals>;
|
|
32
34
|
click(selector: string, options?: ClickOptions): Promise<void>;
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autokap",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "AI-powered CLI tool for capturing clean screenshots of websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -181,6 +181,10 @@
|
|
|
181
181
|
"./program-signing": {
|
|
182
182
|
"types": "./dist/program-signing.d.ts",
|
|
183
183
|
"default": "./dist/program-signing.js"
|
|
184
|
+
},
|
|
185
|
+
"./transform-browser-url": {
|
|
186
|
+
"types": "./dist/transform-browser-url.d.ts",
|
|
187
|
+
"default": "./dist/transform-browser-url.js"
|
|
184
188
|
}
|
|
185
189
|
},
|
|
186
190
|
"bin": {
|