@veolab/discoverylab 1.3.1 → 1.3.3
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/chunk-7EDIUVIO.js +4304 -0
- package/dist/{chunk-4L76GPRC.js → chunk-AHVBE25Y.js} +23 -17
- package/dist/chunk-HGWEHWKJ.js +94 -0
- package/dist/{chunk-VRM42PML.js → chunk-LXSWDEXV.js} +276 -56
- package/dist/{chunk-N6JJ2RGV.js → chunk-ZLHIHMSL.js} +1 -1
- package/dist/cli.js +26 -26
- package/dist/{esvp-GSISVXLC.js → esvp-KVOWYW6G.js} +2 -1
- package/dist/{esvp-mobile-GC7MAGMI.js → esvp-mobile-GZ5EMYPG.js} +3 -2
- package/dist/index.d.ts +13 -17
- package/dist/index.html +149 -29
- package/dist/index.js +6 -6
- package/dist/{server-FO3UVUZU.js → server-T5X6GGOO.js} +5 -5
- package/dist/templates/bundle/bundle.js +8 -4
- package/dist/templates/bundle/public/mockup-android-galaxy.png +0 -0
- package/dist/{tools-OCRMOQ4U.js → tools-YGM5HRIB.js} +4 -4
- package/package.json +2 -2
- package/dist/chunk-3QRQEDWR.js +0 -1690
- package/dist/chunk-GAKEFJ5T.js +0 -481
|
@@ -4,20 +4,9 @@ import {
|
|
|
4
4
|
createTextResult
|
|
5
5
|
} from "./chunk-XKX6NBHF.js";
|
|
6
6
|
import {
|
|
7
|
-
createLoginFlow,
|
|
8
|
-
createNavigationTestFlow,
|
|
9
|
-
createOnboardingFlow,
|
|
10
|
-
generateMaestroFlow,
|
|
11
|
-
getAdbCommand,
|
|
12
7
|
getAvailableTemplates,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
isTemplatesInstalled,
|
|
16
|
-
listMaestroDevices,
|
|
17
|
-
runMaestroTest,
|
|
18
|
-
runMaestroWithCapture,
|
|
19
|
-
startMaestroStudio
|
|
20
|
-
} from "./chunk-3QRQEDWR.js";
|
|
8
|
+
isTemplatesInstalled
|
|
9
|
+
} from "./chunk-HGWEHWKJ.js";
|
|
21
10
|
import {
|
|
22
11
|
createFormSubmissionScript,
|
|
23
12
|
createLoginScript,
|
|
@@ -35,12 +24,18 @@ import {
|
|
|
35
24
|
buildAppLabNetworkProfile
|
|
36
25
|
} from "./chunk-LB3RNE3O.js";
|
|
37
26
|
import {
|
|
27
|
+
LOCAL_ESVP_SERVER_URL,
|
|
38
28
|
attachESVPNetworkTrace,
|
|
39
29
|
captureESVPCheckpoint,
|
|
40
30
|
clearESVPNetwork,
|
|
41
31
|
configureESVPNetwork,
|
|
42
32
|
createESVPSession,
|
|
33
|
+
createLoginFlow,
|
|
34
|
+
createNavigationTestFlow,
|
|
35
|
+
createOnboardingFlow,
|
|
43
36
|
finishESVPSession,
|
|
37
|
+
generateMaestroFlow,
|
|
38
|
+
getAdbCommand,
|
|
44
39
|
getESVPArtifactContent,
|
|
45
40
|
getESVPConnection,
|
|
46
41
|
getESVPHealth,
|
|
@@ -48,15 +43,21 @@ import {
|
|
|
48
43
|
getESVPSession,
|
|
49
44
|
getESVPSessionNetwork,
|
|
50
45
|
getESVPTranscript,
|
|
46
|
+
getMaestroVersion,
|
|
51
47
|
inspectESVPSession,
|
|
48
|
+
isMaestroInstalled,
|
|
52
49
|
listESVPArtifacts,
|
|
53
50
|
listESVPDevices,
|
|
54
51
|
listESVPSessions,
|
|
52
|
+
listMaestroDevices,
|
|
55
53
|
replayESVPSession,
|
|
56
54
|
runESVPActions,
|
|
57
55
|
runESVPPreflight,
|
|
56
|
+
runMaestroTest,
|
|
57
|
+
runMaestroWithCapture,
|
|
58
|
+
startMaestroStudio,
|
|
58
59
|
validateESVPReplay
|
|
59
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-7EDIUVIO.js";
|
|
60
61
|
import {
|
|
61
62
|
DATA_DIR,
|
|
62
63
|
EXPORTS_DIR,
|
|
@@ -6452,9 +6453,14 @@ function resolveProjectRecordingReplaySessionId(session) {
|
|
|
6452
6453
|
}
|
|
6453
6454
|
function resolveProjectRecordingESVPServerUrl(session) {
|
|
6454
6455
|
const esvp = resolveProjectRecordingESVPState(session);
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6456
|
+
const serverUrl = typeof esvp?.serverUrl === "string" ? esvp.serverUrl.trim().replace(/\/+$/, "") : "";
|
|
6457
|
+
if (serverUrl === LOCAL_ESVP_SERVER_URL) {
|
|
6458
|
+
return LOCAL_ESVP_SERVER_URL;
|
|
6459
|
+
}
|
|
6460
|
+
if (!serverUrl && esvp?.connectionMode === "local") {
|
|
6461
|
+
return LOCAL_ESVP_SERVER_URL;
|
|
6462
|
+
}
|
|
6463
|
+
return void 0;
|
|
6458
6464
|
}
|
|
6459
6465
|
function resolveAppLabBaseUrl(appLabUrl) {
|
|
6460
6466
|
const raw = String(appLabUrl || process.env.DISCOVERYLAB_APP_URL || "http://127.0.0.1:3847").trim();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TEMPLATES_DIR
|
|
3
|
+
} from "./chunk-VVIOB362.js";
|
|
4
|
+
|
|
5
|
+
// src/core/templates/loader.ts
|
|
6
|
+
import { existsSync, readFileSync } from "fs";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
var MANIFEST_FILE = "manifest.json";
|
|
10
|
+
var BUNDLE_DIR = "bundle";
|
|
11
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
var BUNDLED_TEMPLATES_DIR_CANDIDATES = [
|
|
13
|
+
join(__dirname, "templates"),
|
|
14
|
+
join(__dirname, "..", "templates"),
|
|
15
|
+
join(__dirname, "..", "..", "templates")
|
|
16
|
+
];
|
|
17
|
+
var cachedManifest = null;
|
|
18
|
+
var cachedTemplatesDir = null;
|
|
19
|
+
var cachedAt = 0;
|
|
20
|
+
var CACHE_TTL = 3e4;
|
|
21
|
+
function resolveTemplatesDir() {
|
|
22
|
+
const explicitOverride = process.env.DISCOVERYLAB_TEMPLATE_DIR?.trim() || process.env.DISCOVERYLAB_TEMPLATE_SOURCE_DIR?.trim();
|
|
23
|
+
if (explicitOverride) {
|
|
24
|
+
const overrideManifest = join(explicitOverride, MANIFEST_FILE);
|
|
25
|
+
const overrideBundle = join(explicitOverride, BUNDLE_DIR);
|
|
26
|
+
if (existsSync(overrideManifest) && existsSync(overrideBundle)) {
|
|
27
|
+
return explicitOverride;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const candidate of BUNDLED_TEMPLATES_DIR_CANDIDATES) {
|
|
31
|
+
const bundledManifest = join(candidate, MANIFEST_FILE);
|
|
32
|
+
const bundledBundle = join(candidate, BUNDLE_DIR);
|
|
33
|
+
if (existsSync(bundledManifest) && existsSync(bundledBundle)) {
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const localManifest = join(TEMPLATES_DIR, MANIFEST_FILE);
|
|
38
|
+
const localBundle = join(TEMPLATES_DIR, BUNDLE_DIR);
|
|
39
|
+
if (existsSync(localManifest) && existsSync(localBundle)) {
|
|
40
|
+
return TEMPLATES_DIR;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function loadManifest() {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
if (cachedManifest && now - cachedAt < CACHE_TTL) {
|
|
47
|
+
return cachedManifest;
|
|
48
|
+
}
|
|
49
|
+
const dir = resolveTemplatesDir();
|
|
50
|
+
if (!dir) {
|
|
51
|
+
cachedManifest = null;
|
|
52
|
+
cachedTemplatesDir = null;
|
|
53
|
+
cachedAt = now;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const raw = readFileSync(join(dir, MANIFEST_FILE), "utf-8");
|
|
58
|
+
const manifest = JSON.parse(raw);
|
|
59
|
+
cachedManifest = manifest;
|
|
60
|
+
cachedTemplatesDir = dir;
|
|
61
|
+
cachedAt = now;
|
|
62
|
+
return manifest;
|
|
63
|
+
} catch {
|
|
64
|
+
cachedManifest = null;
|
|
65
|
+
cachedTemplatesDir = null;
|
|
66
|
+
cachedAt = now;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function isTemplatesInstalled() {
|
|
71
|
+
return loadManifest() !== null;
|
|
72
|
+
}
|
|
73
|
+
function getAvailableTemplates() {
|
|
74
|
+
const manifest = loadManifest();
|
|
75
|
+
return manifest?.templates ?? [];
|
|
76
|
+
}
|
|
77
|
+
function getTemplate(id) {
|
|
78
|
+
const templates = getAvailableTemplates();
|
|
79
|
+
return templates.find((t) => t.id === id) ?? null;
|
|
80
|
+
}
|
|
81
|
+
function getBundlePath() {
|
|
82
|
+
loadManifest();
|
|
83
|
+
if (!cachedTemplatesDir) return null;
|
|
84
|
+
const bundlePath = join(cachedTemplatesDir, BUNDLE_DIR);
|
|
85
|
+
if (!existsSync(bundlePath)) return null;
|
|
86
|
+
return bundlePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
isTemplatesInstalled,
|
|
91
|
+
getAvailableTemplates,
|
|
92
|
+
getTemplate,
|
|
93
|
+
getBundlePath
|
|
94
|
+
};
|
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
findAndroidSdkPath,
|
|
3
|
-
getAdbCommand,
|
|
4
2
|
getAvailableTemplates,
|
|
5
3
|
getBundlePath,
|
|
6
|
-
getEmulatorPath,
|
|
7
|
-
getMaestroRecorder,
|
|
8
4
|
getTemplate,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
isTemplatesInstalled,
|
|
12
|
-
killZombieMaestroProcesses,
|
|
13
|
-
listConnectedAndroidDevices,
|
|
14
|
-
listMaestroDevices,
|
|
15
|
-
parseMaestroActionsFromYaml,
|
|
16
|
-
resolveAndroidDeviceSerial,
|
|
17
|
-
runMaestroTest,
|
|
18
|
-
tapViaIdb
|
|
19
|
-
} from "./chunk-3QRQEDWR.js";
|
|
20
|
-
import {
|
|
21
|
-
APP_VERSION
|
|
22
|
-
} from "./chunk-6EGBXRDK.js";
|
|
5
|
+
isTemplatesInstalled
|
|
6
|
+
} from "./chunk-HGWEHWKJ.js";
|
|
23
7
|
import {
|
|
24
8
|
runPlaywrightTest
|
|
25
9
|
} from "./chunk-FIL7IWEL.js";
|
|
@@ -37,20 +21,37 @@ import {
|
|
|
37
21
|
resolveLocalAppHttpTraceCollectorById,
|
|
38
22
|
startLocalAppHttpTraceCollector,
|
|
39
23
|
validateMaestroRecordingWithESVP
|
|
40
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-ZLHIHMSL.js";
|
|
41
25
|
import {
|
|
42
26
|
buildAppLabNetworkProfile
|
|
43
27
|
} from "./chunk-LB3RNE3O.js";
|
|
44
28
|
import {
|
|
29
|
+
LOCAL_ESVP_SERVER_URL,
|
|
45
30
|
attachESVPNetworkTrace,
|
|
46
31
|
configureESVPNetwork,
|
|
47
32
|
createESVPSession,
|
|
33
|
+
findAndroidSdkPath,
|
|
34
|
+
getAdbCommand,
|
|
48
35
|
getESVPReplayConsistency,
|
|
36
|
+
getEmulatorPath,
|
|
37
|
+
getMaestroRecorder,
|
|
49
38
|
inspectESVPSession,
|
|
39
|
+
isIdbInstalled,
|
|
40
|
+
isMaestroInstalled,
|
|
41
|
+
killZombieMaestroProcesses,
|
|
42
|
+
listConnectedAndroidDevices,
|
|
43
|
+
listMaestroDevices,
|
|
44
|
+
parseMaestroActionsFromYaml,
|
|
50
45
|
replayESVPSession,
|
|
46
|
+
resolveAndroidDeviceSerial,
|
|
51
47
|
runESVPActions,
|
|
48
|
+
runMaestroTest,
|
|
49
|
+
tapViaIdb,
|
|
52
50
|
validateESVPReplay
|
|
53
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-7EDIUVIO.js";
|
|
52
|
+
import {
|
|
53
|
+
APP_VERSION
|
|
54
|
+
} from "./chunk-6EGBXRDK.js";
|
|
54
55
|
import {
|
|
55
56
|
redactQuotedStringsInText,
|
|
56
57
|
redactSensitiveTestInput
|
|
@@ -4589,9 +4590,20 @@ function resolveProjectESVPSessionId(esvp) {
|
|
|
4589
4590
|
return validationId || networkId || directId || null;
|
|
4590
4591
|
}
|
|
4591
4592
|
function resolveProjectESVPServerUrl(esvp) {
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4593
|
+
return normalizePersistedLocalESVPServerUrl(esvp?.serverUrl, esvp?.connectionMode);
|
|
4594
|
+
}
|
|
4595
|
+
function normalizePersistedLocalESVPServerUrl(serverUrl, connectionMode) {
|
|
4596
|
+
const normalized = typeof serverUrl === "string" ? serverUrl.trim().replace(/\/+$/, "") : "";
|
|
4597
|
+
if (normalized === LOCAL_ESVP_SERVER_URL) {
|
|
4598
|
+
return LOCAL_ESVP_SERVER_URL;
|
|
4599
|
+
}
|
|
4600
|
+
if (!normalized && connectionMode === "local") {
|
|
4601
|
+
return LOCAL_ESVP_SERVER_URL;
|
|
4602
|
+
}
|
|
4603
|
+
return void 0;
|
|
4604
|
+
}
|
|
4605
|
+
function resolvePersistedLocalESVPServerUrl(serverUrl, esvp) {
|
|
4606
|
+
return normalizePersistedLocalESVPServerUrl(serverUrl, "local") || resolveProjectESVPServerUrl(esvp) || LOCAL_ESVP_SERVER_URL;
|
|
4595
4607
|
}
|
|
4596
4608
|
function isESVPReplayValidationSupported(result) {
|
|
4597
4609
|
if (!result) return true;
|
|
@@ -6433,7 +6445,7 @@ app.post("/api/testing/mobile/recordings/:id/esvp/validate", async (c) => {
|
|
|
6433
6445
|
let autoSynced = false;
|
|
6434
6446
|
if (result.networkEntries.length === 0 && result.supported && result.sourceSessionId && ((result.networkState?.managed_proxy?.entry_count ?? 0) > 0 || (result.networkState?.trace_count ?? 0) > 0)) {
|
|
6435
6447
|
try {
|
|
6436
|
-
const { collectESVPSessionNetworkData: collectESVPSessionNetworkData2 } = await import("./esvp-mobile-
|
|
6448
|
+
const { collectESVPSessionNetworkData: collectESVPSessionNetworkData2 } = await import("./esvp-mobile-GZ5EMYPG.js");
|
|
6437
6449
|
const deferred = await collectESVPSessionNetworkData2(result.sourceSessionId, serverUrl);
|
|
6438
6450
|
if (deferred.networkEntries.length > 0) {
|
|
6439
6451
|
session.networkEntries = deferred.networkEntries;
|
|
@@ -6517,7 +6529,7 @@ app.post("/api/testing/mobile/recordings/:id/esvp/replay", async (c) => {
|
|
|
6517
6529
|
session.esvp = {
|
|
6518
6530
|
...esvp || {},
|
|
6519
6531
|
currentSessionId: sourceSessionId,
|
|
6520
|
-
serverUrl:
|
|
6532
|
+
serverUrl: resolvePersistedLocalESVPServerUrl(serverUrl, esvp),
|
|
6521
6533
|
executor,
|
|
6522
6534
|
validation: {
|
|
6523
6535
|
...existingValidation,
|
|
@@ -6660,7 +6672,10 @@ app.post("/api/testing/mobile/recordings/:id/esvp/network/start", async (c) => {
|
|
|
6660
6672
|
...session.esvp && typeof session.esvp === "object" ? session.esvp : {},
|
|
6661
6673
|
currentSessionId: sourceSessionId,
|
|
6662
6674
|
connectionMode: typeof session?.esvp?.connectionMode === "string" ? session.esvp.connectionMode : "local",
|
|
6663
|
-
serverUrl:
|
|
6675
|
+
serverUrl: resolvePersistedLocalESVPServerUrl(
|
|
6676
|
+
serverUrl,
|
|
6677
|
+
session?.esvp && typeof session.esvp === "object" ? session.esvp : null
|
|
6678
|
+
),
|
|
6664
6679
|
executor,
|
|
6665
6680
|
network: {
|
|
6666
6681
|
...existingNetwork || {},
|
|
@@ -9938,11 +9953,11 @@ app.post("/api/templates/props", async (c) => {
|
|
|
9938
9953
|
if (!project) {
|
|
9939
9954
|
return c.json({ error: "Project not found" }, 404);
|
|
9940
9955
|
}
|
|
9941
|
-
const
|
|
9942
|
-
if (!
|
|
9956
|
+
const templateState = getTemplateProjectState(project);
|
|
9957
|
+
if (!templateState) {
|
|
9943
9958
|
return c.json({ error: "Project has no video to render" }, 400);
|
|
9944
9959
|
}
|
|
9945
|
-
return c.json(
|
|
9960
|
+
return c.json(templateState);
|
|
9946
9961
|
} catch (error) {
|
|
9947
9962
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
9948
9963
|
return c.json({ error: message }, 500);
|
|
@@ -10000,26 +10015,39 @@ app.put("/api/projects/:id/template-content", async (c) => {
|
|
|
10000
10015
|
try {
|
|
10001
10016
|
const id = c.req.param("id");
|
|
10002
10017
|
const body = await c.req.json();
|
|
10003
|
-
const { title, titleLines, terminalTabs, showcaseMode } = body;
|
|
10018
|
+
const { title, titleLines, terminalTabs, showcaseMode, deviceMockup } = body;
|
|
10004
10019
|
const db = getDatabase();
|
|
10005
10020
|
const project = db.select().from(projects).where(eq(projects.id, id)).get();
|
|
10006
10021
|
if (!project) {
|
|
10007
10022
|
return c.json({ error: "Project not found" }, 404);
|
|
10008
10023
|
}
|
|
10024
|
+
const defaultTitle = sanitizeTemplateTitle(
|
|
10025
|
+
extractFirstSentence(project.aiSummary || project.name || "App Recording"),
|
|
10026
|
+
"App Recording"
|
|
10027
|
+
);
|
|
10028
|
+
const sanitizedTitle = sanitizeTemplateTitle(title, defaultTitle);
|
|
10029
|
+
const sanitizedTitleLines = sanitizeTemplateTitleLines(titleLines, sanitizedTitle);
|
|
10030
|
+
const sanitizedTerminalTabs = sanitizeTemplateTerminalTabs(terminalTabs);
|
|
10031
|
+
const sanitizedShowcaseMode = showcaseMode === "artistic" || showcaseMode === "terminal" ? showcaseMode : void 0;
|
|
10032
|
+
const platform = project.platform === "ios" || project.platform === "android" || project.platform === "web" ? project.platform : "web";
|
|
10033
|
+
const availableAndroidMockups = listAndroidDeviceMockupIds();
|
|
10034
|
+
const sanitizedDeviceMockup = platform === "android" ? resolveAndroidDeviceMockup(deviceMockup, availableAndroidMockups) : void 0;
|
|
10035
|
+
const savedContent = {
|
|
10036
|
+
title: sanitizedTitle,
|
|
10037
|
+
titleLines: sanitizedTitleLines,
|
|
10038
|
+
terminalTabs: sanitizedTerminalTabs,
|
|
10039
|
+
showcaseMode: sanitizedShowcaseMode,
|
|
10040
|
+
deviceMockup: sanitizedDeviceMockup,
|
|
10041
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10042
|
+
};
|
|
10009
10043
|
const { writeFileSync: writeFileSync5, mkdirSync: mkdirSync6 } = await import("fs");
|
|
10010
10044
|
const projectDir = join5(PROJECTS_DIR, id);
|
|
10011
10045
|
if (!existsSync5(projectDir)) {
|
|
10012
10046
|
mkdirSync6(projectDir, { recursive: true });
|
|
10013
10047
|
}
|
|
10014
10048
|
const contentPath = join5(projectDir, "template-content.json");
|
|
10015
|
-
writeFileSync5(contentPath, JSON.stringify(
|
|
10016
|
-
|
|
10017
|
-
titleLines,
|
|
10018
|
-
terminalTabs,
|
|
10019
|
-
showcaseMode,
|
|
10020
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10021
|
-
}));
|
|
10022
|
-
return c.json({ success: true, message: "Template content saved" });
|
|
10049
|
+
writeFileSync5(contentPath, JSON.stringify(savedContent));
|
|
10050
|
+
return c.json({ success: true, message: "Template content saved", content: savedContent });
|
|
10023
10051
|
} catch (error) {
|
|
10024
10052
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
10025
10053
|
return c.json({ error: message }, 500);
|
|
@@ -10040,10 +10068,14 @@ app.post("/api/templates/render", async (c) => {
|
|
|
10040
10068
|
if (!project) {
|
|
10041
10069
|
return c.json({ error: "Project not found" }, 404);
|
|
10042
10070
|
}
|
|
10043
|
-
const
|
|
10044
|
-
if (!
|
|
10071
|
+
const templateState = getTemplateProjectState(project);
|
|
10072
|
+
if (!templateState) {
|
|
10045
10073
|
return c.json({ error: "Project has no video to render" }, 400);
|
|
10046
10074
|
}
|
|
10075
|
+
if (!templateState.eligibility.templatesAllowed) {
|
|
10076
|
+
return c.json({ error: templateState.eligibility.reason || `Templates are limited to videos up to ${TEMPLATE_MAX_DURATION_SECONDS} seconds.` }, 400);
|
|
10077
|
+
}
|
|
10078
|
+
const { props } = templateState;
|
|
10047
10079
|
const job = await startRender(projectId, templateId, props, (progress) => {
|
|
10048
10080
|
broadcastToClients({
|
|
10049
10081
|
type: "templateRenderProgress",
|
|
@@ -10139,29 +10171,27 @@ app.get("/api/settings/template-preference", async (c) => {
|
|
|
10139
10171
|
}
|
|
10140
10172
|
});
|
|
10141
10173
|
function assembleTemplateProps(project) {
|
|
10142
|
-
const resolvedVideoPath =
|
|
10143
|
-
if (!resolvedVideoPath
|
|
10174
|
+
const resolvedVideoPath = resolveTemplateVideoPath(project);
|
|
10175
|
+
if (!resolvedVideoPath) {
|
|
10144
10176
|
return null;
|
|
10145
10177
|
}
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
} catch {
|
|
10151
|
-
return null;
|
|
10152
|
-
}
|
|
10153
|
-
const defaultTitle = extractFirstSentence(project.aiSummary || project.name || "App Recording");
|
|
10178
|
+
const defaultTitle = sanitizeTemplateTitle(
|
|
10179
|
+
extractFirstSentence(project.aiSummary || project.name || "App Recording"),
|
|
10180
|
+
"App Recording"
|
|
10181
|
+
);
|
|
10154
10182
|
const subtitle = project.name || void 0;
|
|
10155
10183
|
const editedContent = loadEditedTemplateContent(project.id);
|
|
10184
|
+
const availableAndroidMockups = listAndroidDeviceMockupIds();
|
|
10156
10185
|
let title = defaultTitle;
|
|
10157
10186
|
let titleLines;
|
|
10158
10187
|
let terminalTabs = [];
|
|
10159
10188
|
let hasNetworkData = false;
|
|
10160
10189
|
let showcaseMode;
|
|
10190
|
+
let deviceMockup;
|
|
10161
10191
|
if (editedContent) {
|
|
10162
|
-
title = editedContent.title || defaultTitle;
|
|
10163
|
-
titleLines = editedContent.titleLines;
|
|
10164
|
-
terminalTabs = editedContent.terminalTabs
|
|
10192
|
+
title = sanitizeTemplateTitle(editedContent.title || defaultTitle, defaultTitle);
|
|
10193
|
+
titleLines = sanitizeTemplateTitleLines(editedContent.titleLines, title);
|
|
10194
|
+
terminalTabs = sanitizeTemplateTerminalTabs(editedContent.terminalTabs);
|
|
10165
10195
|
hasNetworkData = terminalTabs.length > 0;
|
|
10166
10196
|
showcaseMode = editedContent.showcaseMode;
|
|
10167
10197
|
} else {
|
|
@@ -10171,8 +10201,14 @@ function assembleTemplateProps(project) {
|
|
|
10171
10201
|
terminalTabs = groupNetworkIntoTabs(networkEntries);
|
|
10172
10202
|
}
|
|
10173
10203
|
}
|
|
10174
|
-
const videoDuration = project.duration || 0;
|
|
10175
10204
|
const platform = project.platform === "ios" || project.platform === "android" || project.platform === "web" ? project.platform : "web";
|
|
10205
|
+
if (platform === "android") {
|
|
10206
|
+
deviceMockup = resolveAndroidDeviceMockup(editedContent?.deviceMockup, availableAndroidMockups);
|
|
10207
|
+
}
|
|
10208
|
+
if (!titleLines && (!showcaseMode || showcaseMode === "artistic") && !hasNetworkData) {
|
|
10209
|
+
titleLines = splitTemplateTitleIntoLines(title);
|
|
10210
|
+
}
|
|
10211
|
+
const videoDuration = getActualTemplateVideoDuration(project, resolvedVideoPath);
|
|
10176
10212
|
const videoUrl = `http://localhost:${currentServerPort}/api/file?path=${encodeURIComponent(resolvedVideoPath)}`;
|
|
10177
10213
|
return {
|
|
10178
10214
|
videoUrl,
|
|
@@ -10183,7 +10219,32 @@ function assembleTemplateProps(project) {
|
|
|
10183
10219
|
subtitle,
|
|
10184
10220
|
terminalTabs,
|
|
10185
10221
|
hasNetworkData,
|
|
10186
|
-
showcaseMode
|
|
10222
|
+
showcaseMode,
|
|
10223
|
+
deviceMockup
|
|
10224
|
+
};
|
|
10225
|
+
}
|
|
10226
|
+
var TEMPLATE_MAX_DURATION_SECONDS = 60;
|
|
10227
|
+
var DEFAULT_ANDROID_DEVICE_MOCKUP = "mockup-android-galaxy.png";
|
|
10228
|
+
var ANDROID_DEVICE_MOCKUP_FALLBACKS = [
|
|
10229
|
+
DEFAULT_ANDROID_DEVICE_MOCKUP,
|
|
10230
|
+
"mockup-android.png",
|
|
10231
|
+
"mockup-android-google-pixel-9-pro.png"
|
|
10232
|
+
];
|
|
10233
|
+
function getTemplateProjectState(project) {
|
|
10234
|
+
const props = assembleTemplateProps(project);
|
|
10235
|
+
if (!props) {
|
|
10236
|
+
return null;
|
|
10237
|
+
}
|
|
10238
|
+
const eligibility = buildTemplateEligibility(props.videoDuration);
|
|
10239
|
+
const androidDeviceMockups = listAndroidDeviceMockupIds().map((id) => ({
|
|
10240
|
+
id,
|
|
10241
|
+
label: formatAndroidDeviceMockupLabel(id)
|
|
10242
|
+
}));
|
|
10243
|
+
return {
|
|
10244
|
+
props,
|
|
10245
|
+
eligibility,
|
|
10246
|
+
androidDeviceMockups,
|
|
10247
|
+
defaultAndroidDeviceMockup: resolveAndroidDeviceMockup(void 0, androidDeviceMockups.map((option) => option.id))
|
|
10187
10248
|
};
|
|
10188
10249
|
}
|
|
10189
10250
|
function extractFirstSentence(text) {
|
|
@@ -10192,6 +10253,164 @@ function extractFirstSentence(text) {
|
|
|
10192
10253
|
if (match) return match[0].trim();
|
|
10193
10254
|
return text.substring(0, 80).trim();
|
|
10194
10255
|
}
|
|
10256
|
+
function resolveTemplateVideoPath(project) {
|
|
10257
|
+
const resolvedVideoPath = resolveVideoPath(project.videoPath);
|
|
10258
|
+
if (!resolvedVideoPath || !existsSync5(resolvedVideoPath)) {
|
|
10259
|
+
return null;
|
|
10260
|
+
}
|
|
10261
|
+
try {
|
|
10262
|
+
if (statSync2(resolvedVideoPath).isDirectory()) {
|
|
10263
|
+
return null;
|
|
10264
|
+
}
|
|
10265
|
+
} catch {
|
|
10266
|
+
return null;
|
|
10267
|
+
}
|
|
10268
|
+
return resolvedVideoPath;
|
|
10269
|
+
}
|
|
10270
|
+
function getActualTemplateVideoDuration(project, resolvedVideoPath) {
|
|
10271
|
+
const probedDuration = probeVideoDurationSeconds(resolvedVideoPath);
|
|
10272
|
+
if (probedDuration && probedDuration > 0) {
|
|
10273
|
+
return probedDuration;
|
|
10274
|
+
}
|
|
10275
|
+
const projectDuration = Number(project?.duration);
|
|
10276
|
+
if (Number.isFinite(projectDuration) && projectDuration > 0) {
|
|
10277
|
+
return projectDuration;
|
|
10278
|
+
}
|
|
10279
|
+
return 0;
|
|
10280
|
+
}
|
|
10281
|
+
function probeVideoDurationSeconds(filePath) {
|
|
10282
|
+
try {
|
|
10283
|
+
if (!existsSync5(filePath)) return null;
|
|
10284
|
+
const output = execSync3(
|
|
10285
|
+
`ffprobe -v quiet -print_format json -show_format "${filePath}"`,
|
|
10286
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
10287
|
+
);
|
|
10288
|
+
const data = JSON.parse(output);
|
|
10289
|
+
const duration = parseFloat(data?.format?.duration || "0");
|
|
10290
|
+
return Number.isFinite(duration) && duration > 0 ? duration : null;
|
|
10291
|
+
} catch {
|
|
10292
|
+
return null;
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
function buildTemplateEligibility(actualDurationSeconds) {
|
|
10296
|
+
const templatesAllowed = isTemplateDurationAllowed(actualDurationSeconds);
|
|
10297
|
+
return {
|
|
10298
|
+
templatesAllowed,
|
|
10299
|
+
maxTemplateDurationSeconds: TEMPLATE_MAX_DURATION_SECONDS,
|
|
10300
|
+
actualDurationSeconds,
|
|
10301
|
+
reason: templatesAllowed || actualDurationSeconds <= 0 ? void 0 : `Templates are limited to videos up to ${TEMPLATE_MAX_DURATION_SECONDS} seconds. This recording is ${formatTemplateDuration(actualDurationSeconds)}.`
|
|
10302
|
+
};
|
|
10303
|
+
}
|
|
10304
|
+
function isTemplateDurationAllowed(actualDurationSeconds) {
|
|
10305
|
+
if (!Number.isFinite(actualDurationSeconds) || actualDurationSeconds <= 0) {
|
|
10306
|
+
return true;
|
|
10307
|
+
}
|
|
10308
|
+
return Math.round(actualDurationSeconds * 1e3) <= TEMPLATE_MAX_DURATION_SECONDS * 1e3;
|
|
10309
|
+
}
|
|
10310
|
+
function formatTemplateDuration(actualDurationSeconds) {
|
|
10311
|
+
if (!Number.isFinite(actualDurationSeconds) || actualDurationSeconds <= 0) {
|
|
10312
|
+
return "unknown duration";
|
|
10313
|
+
}
|
|
10314
|
+
const totalSeconds = Math.max(1, Math.round(actualDurationSeconds));
|
|
10315
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
10316
|
+
const seconds = totalSeconds % 60;
|
|
10317
|
+
return minutes > 0 ? `${minutes}m ${seconds}s` : `${totalSeconds}s`;
|
|
10318
|
+
}
|
|
10319
|
+
function sanitizeTemplateTitle(value, fallback = "App Recording") {
|
|
10320
|
+
const normalized = normalizeTemplateTitle(value);
|
|
10321
|
+
if (normalized) return normalized;
|
|
10322
|
+
const safeFallback = normalizeTemplateTitle(fallback);
|
|
10323
|
+
return safeFallback || "App Recording";
|
|
10324
|
+
}
|
|
10325
|
+
function normalizeTemplateTitle(value) {
|
|
10326
|
+
if (typeof value !== "string") return "";
|
|
10327
|
+
const withoutMarkdown = value.replace(/[#*_`~>\-[\]{}()<>\\/|]+/g, " ").replace(/&/g, " and ").replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
10328
|
+
if (!withoutMarkdown) return "";
|
|
10329
|
+
const limitedWords = withoutMarkdown.split(" ").filter(Boolean).slice(0, 7).join(" ");
|
|
10330
|
+
if (!limitedWords) return "";
|
|
10331
|
+
if (limitedWords.length <= 48) return limitedWords;
|
|
10332
|
+
const truncated = limitedWords.slice(0, 48).trim();
|
|
10333
|
+
return truncated.replace(/\s+\S*$/, "").trim() || truncated;
|
|
10334
|
+
}
|
|
10335
|
+
function sanitizeTemplateTitleLines(value, fallbackTitle) {
|
|
10336
|
+
if (!Array.isArray(value)) {
|
|
10337
|
+
return void 0;
|
|
10338
|
+
}
|
|
10339
|
+
const cleaned = value.map((line) => sanitizeTemplateTitle(line, "")).filter(Boolean).slice(0, 4);
|
|
10340
|
+
if (cleaned.length > 0) {
|
|
10341
|
+
return cleaned;
|
|
10342
|
+
}
|
|
10343
|
+
return splitTemplateTitleIntoLines(fallbackTitle);
|
|
10344
|
+
}
|
|
10345
|
+
function splitTemplateTitleIntoLines(title) {
|
|
10346
|
+
const words = title.split(" ").filter(Boolean);
|
|
10347
|
+
if (words.length === 0) return void 0;
|
|
10348
|
+
if (words.length <= 2) return [title];
|
|
10349
|
+
const lines = [];
|
|
10350
|
+
for (let index = 0; index < words.length && lines.length < 4; index += 2) {
|
|
10351
|
+
lines.push(words.slice(index, index + 2).join(" "));
|
|
10352
|
+
}
|
|
10353
|
+
return lines;
|
|
10354
|
+
}
|
|
10355
|
+
function sanitizeTemplateTerminalTabs(value) {
|
|
10356
|
+
if (!Array.isArray(value)) return [];
|
|
10357
|
+
return value.map((tab) => {
|
|
10358
|
+
if (!tab || typeof tab !== "object") return null;
|
|
10359
|
+
const record = tab;
|
|
10360
|
+
const label = typeof record.label === "string" ? record.label.trim() : "";
|
|
10361
|
+
const methodFromLabel = label.split(" ")[0] || "GET";
|
|
10362
|
+
const routeFromLabel = label.split(" ").slice(1).join(" ") || "/";
|
|
10363
|
+
const method = typeof record.method === "string" && record.method.trim() ? record.method.trim().toUpperCase() : methodFromLabel.toUpperCase();
|
|
10364
|
+
const route = typeof record.route === "string" && record.route.trim() ? record.route.trim() : routeFromLabel;
|
|
10365
|
+
const content = typeof record.content === "string" ? record.content : "";
|
|
10366
|
+
if (!label && !content.trim()) return null;
|
|
10367
|
+
return {
|
|
10368
|
+
label: label || `${method} ${route}`.trim(),
|
|
10369
|
+
method: method || "GET",
|
|
10370
|
+
route: route || "/",
|
|
10371
|
+
content
|
|
10372
|
+
};
|
|
10373
|
+
}).filter((tab) => !!tab);
|
|
10374
|
+
}
|
|
10375
|
+
function listAndroidDeviceMockupIds() {
|
|
10376
|
+
const bundlePath = getBundlePath();
|
|
10377
|
+
if (!bundlePath) {
|
|
10378
|
+
return [...ANDROID_DEVICE_MOCKUP_FALLBACKS];
|
|
10379
|
+
}
|
|
10380
|
+
const publicDir = join5(bundlePath, "public");
|
|
10381
|
+
if (!existsSync5(publicDir)) {
|
|
10382
|
+
return [...ANDROID_DEVICE_MOCKUP_FALLBACKS];
|
|
10383
|
+
}
|
|
10384
|
+
const files = readdirSync2(publicDir).filter((file) => /^mockup-android.*\.png$/i.test(file));
|
|
10385
|
+
const unique = new Set(files.length > 0 ? files : ANDROID_DEVICE_MOCKUP_FALLBACKS);
|
|
10386
|
+
return [...unique].sort((left, right) => {
|
|
10387
|
+
const leftIndex = ANDROID_DEVICE_MOCKUP_FALLBACKS.indexOf(left);
|
|
10388
|
+
const rightIndex = ANDROID_DEVICE_MOCKUP_FALLBACKS.indexOf(right);
|
|
10389
|
+
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
10390
|
+
if (leftIndex === -1) return 1;
|
|
10391
|
+
if (rightIndex === -1) return -1;
|
|
10392
|
+
return leftIndex - rightIndex;
|
|
10393
|
+
}
|
|
10394
|
+
return left.localeCompare(right);
|
|
10395
|
+
});
|
|
10396
|
+
}
|
|
10397
|
+
function resolveAndroidDeviceMockup(requested, available) {
|
|
10398
|
+
const candidate = typeof requested === "string" ? requested.trim() : "";
|
|
10399
|
+
if (candidate && available.includes(candidate)) {
|
|
10400
|
+
return candidate;
|
|
10401
|
+
}
|
|
10402
|
+
for (const fallback of ANDROID_DEVICE_MOCKUP_FALLBACKS) {
|
|
10403
|
+
if (available.includes(fallback)) {
|
|
10404
|
+
return fallback;
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
return available[0] || DEFAULT_ANDROID_DEVICE_MOCKUP;
|
|
10408
|
+
}
|
|
10409
|
+
function formatAndroidDeviceMockupLabel(filename) {
|
|
10410
|
+
const base = filename.replace(/^mockup-android-?/i, "").replace(/\.png$/i, "");
|
|
10411
|
+
if (!base) return "Android";
|
|
10412
|
+
return base.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
10413
|
+
}
|
|
10195
10414
|
function isTunnelLikeNetworkEntry(entry) {
|
|
10196
10415
|
if (!entry || typeof entry !== "object") return false;
|
|
10197
10416
|
const method = typeof entry.method === "string" ? entry.method.toUpperCase() : "";
|
|
@@ -10331,7 +10550,8 @@ function loadEditedTemplateContent(projectId) {
|
|
|
10331
10550
|
title: data.title,
|
|
10332
10551
|
titleLines: data.titleLines,
|
|
10333
10552
|
terminalTabs: data.terminalTabs,
|
|
10334
|
-
showcaseMode: data.showcaseMode
|
|
10553
|
+
showcaseMode: data.showcaseMode,
|
|
10554
|
+
deviceMockup: data.deviceMockup
|
|
10335
10555
|
};
|
|
10336
10556
|
} catch {
|
|
10337
10557
|
return null;
|