@veolab/discoverylab 1.4.4 → 1.6.5

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.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +70 -211
  4. package/assets/applab-bundle-icon.png +0 -0
  5. package/assets/icons/icons8-claude-150.png +0 -0
  6. package/assets/icons/icons8-claude-500.png +0 -0
  7. package/dist/{chunk-CUBQRT5L.js → chunk-JAA53ES7.js} +111 -2
  8. package/dist/{chunk-HB3YPWF3.js → chunk-Q7Q3A2ZI.js} +301 -10
  9. package/dist/{chunk-XKX6NBHF.js → chunk-TWRWARU4.js} +52 -2
  10. package/dist/{chunk-2UUMLAVR.js → chunk-V6RREMYD.js} +332 -38
  11. package/dist/cli.js +164 -28
  12. package/dist/export/infographic-template.html +254 -0
  13. package/dist/import-W2JEW254.js +180 -0
  14. package/dist/index.d.ts +30 -6
  15. package/dist/index.html +473 -11
  16. package/dist/index.js +5 -5
  17. package/dist/infographic-GQAHEOAA.js +183 -0
  18. package/dist/mcpb/node_modules/@anthropic-ai/sdk/src/lib/.keep +4 -0
  19. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/better_sqlite3.node.d +1 -0
  20. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/better_sqlite3/src/better_sqlite3.o.d +133 -0
  21. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/deps/locate_sqlite3.stamp.d +1 -0
  22. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o.d +4 -0
  23. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/test_extension/deps/test_extension.o.d +7 -0
  24. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/sqlite3.a.d +1 -0
  25. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/test_extension.node.d +1 -0
  26. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/ba23eeee118cd63e16015df367567cb043fed872.intermediate.d +1 -0
  27. package/dist/{server-QFNKZCOJ.js → server-C2NZM2RV.js} +1 -1
  28. package/dist/{server-OVOACIOJ.js → server-WN6DCCUA.js} +1 -1
  29. package/dist/{setup-6JJYKKBS.js → setup-SMN7FJNZ.js} +5 -2
  30. package/dist/{tools-Q7OZO732.js → tools-VXU3JEQP.js} +6 -4
  31. package/doc/esvp-protocol.md +116 -0
  32. package/package.json +9 -3
  33. package/skills/knowledge-brain/SKILL.md +44 -43
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import "./chunk-R5U7XKVJ.js";
11
11
  import { Command } from "commander";
12
12
  import chalk from "chalk";
13
13
  import open from "open";
14
+ import { fileURLToPath } from "url";
14
15
  var program = new Command();
15
16
  var binName = process.argv[1]?.replace(/.*[\\/]/, "").replace(/\.[^.]+$/, "") === "applab" ? "applab" : "discoverylab";
16
17
  program.name(binName).description("AI-powered app testing & evidence generator - Claude Code Plugin").version(APP_VERSION);
@@ -389,7 +390,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
389
390
  console.log(chalk.cyan("\n DiscoveryLab"));
390
391
  console.log(chalk.gray(" AI-powered app testing & evidence generator\n"));
391
392
  try {
392
- const { startServer } = await import("./server-QFNKZCOJ.js");
393
+ const { startServer } = await import("./server-C2NZM2RV.js");
393
394
  await startServer(port);
394
395
  console.log(chalk.green(` Server running at http://localhost:${port}`));
395
396
  console.log(chalk.gray(" Press Ctrl+C to stop\n"));
@@ -404,7 +405,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
404
405
  program.command("setup").description("Check and configure DiscoveryLab dependencies").action(async () => {
405
406
  console.log(chalk.cyan("\n DiscoveryLab Setup\n"));
406
407
  try {
407
- const { setupStatusTool } = await import("./setup-6JJYKKBS.js");
408
+ const { setupStatusTool } = await import("./setup-SMN7FJNZ.js");
408
409
  const result = await setupStatusTool.handler({});
409
410
  if (result.isError) {
410
411
  console.error(chalk.red(" Setup check failed"));
@@ -454,41 +455,88 @@ program.command("init").description("Initialize DiscoveryLab data directories").
454
455
  process.exit(1);
455
456
  }
456
457
  });
457
- program.command("install").description("Install DiscoveryLab as Claude Code MCP server").action(async () => {
458
- const { homedir } = await import("os");
459
- const { existsSync, readFileSync, writeFileSync } = await import("fs");
460
- const { join } = await import("path");
461
- const claudeConfigPath = join(homedir(), ".claude.json");
458
+ program.command("install").description("Install DiscoveryLab as MCP server for Claude Code and/or Claude Desktop").option("--target <target>", "Installation target: code, desktop, all (default: auto-detect)", "").action(async (opts) => {
459
+ const { homedir, platform } = await import("os");
460
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import("fs");
461
+ const { join, dirname } = await import("path");
462
+ const home = homedir();
463
+ const localMcpEntrypoint = fileURLToPath(new URL("./index.js", import.meta.url));
464
+ const mcpEntry = existsSync(localMcpEntrypoint) ? {
465
+ command: process.execPath,
466
+ args: [localMcpEntrypoint]
467
+ } : {
468
+ command: "npx",
469
+ args: ["-y", "@veolab/discoverylab@latest", "mcp"]
470
+ };
471
+ const targets = {
472
+ code: {
473
+ name: "Claude Code",
474
+ path: join(home, ".claude.json"),
475
+ restart: "Restart Claude Code to activate."
476
+ },
477
+ desktop: {
478
+ name: "Claude Desktop",
479
+ path: platform() === "win32" ? join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
480
+ restart: "Restart Claude Desktop to activate."
481
+ }
482
+ };
483
+ let selectedTargets = [];
484
+ const target = opts.target?.toLowerCase() || "";
485
+ if (target === "code") {
486
+ selectedTargets = ["code"];
487
+ } else if (target === "desktop") {
488
+ selectedTargets = ["desktop"];
489
+ } else if (target === "all") {
490
+ selectedTargets = ["code", "desktop"];
491
+ } else {
492
+ selectedTargets = ["code"];
493
+ const desktopDir = dirname(targets.desktop.path);
494
+ if (existsSync(desktopDir)) {
495
+ selectedTargets.push("desktop");
496
+ }
497
+ }
462
498
  console.log(chalk.cyan("\n Installing DiscoveryLab MCP...\n"));
463
- try {
464
- let config = {};
465
- if (existsSync(claudeConfigPath)) {
466
- const content = readFileSync(claudeConfigPath, "utf-8");
467
- config = JSON.parse(content);
499
+ let installed = 0;
500
+ for (const key of selectedTargets) {
501
+ const t = targets[key];
502
+ try {
503
+ const dir = dirname(t.path);
504
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
505
+ let config = {};
506
+ if (existsSync(t.path)) {
507
+ const content = readFileSync(t.path, "utf-8");
508
+ try {
509
+ config = JSON.parse(content);
510
+ } catch {
511
+ config = {};
512
+ }
513
+ }
514
+ if (!config.mcpServers) config.mcpServers = {};
515
+ config.mcpServers.discoverylab = mcpEntry;
516
+ writeFileSync(t.path, JSON.stringify(config, null, 2));
517
+ console.log(chalk.green(` \u2713 ${t.name} configured`));
518
+ console.log(chalk.gray(` ${t.path}`));
519
+ installed++;
520
+ } catch (error) {
521
+ console.log(chalk.yellow(` \u2717 ${t.name} skipped: ${error instanceof Error ? error.message : String(error)}`));
468
522
  }
469
- if (!config.mcpServers) {
470
- config.mcpServers = {};
523
+ }
524
+ console.log();
525
+ if (installed > 0) {
526
+ for (const key of selectedTargets) {
527
+ console.log(chalk.white(` ${targets[key].restart}`));
471
528
  }
472
- config.mcpServers.discoverylab = {
473
- command: "npx",
474
- args: ["-y", "@veolab/discoverylab@latest", "mcp"]
475
- };
476
- writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
477
- console.log(chalk.green(" \u2713 Added to ~/.claude.json"));
478
- console.log();
479
- console.log(chalk.white(" Restart Claude Code to activate."));
480
529
  console.log(chalk.gray(" Or run: discoverylab serve"));
481
- console.log();
482
- } catch (error) {
483
- console.error(chalk.red(` Failed to install: ${error}`));
484
- process.exit(1);
530
+ } else {
531
+ console.log(chalk.red(" No targets configured."));
485
532
  }
533
+ console.log();
486
534
  });
487
535
  program.command("mcp").description("Run as MCP server (for Claude Code integration)").action(async () => {
488
536
  try {
489
537
  const { getDatabase } = await import("./db-5ECN3O7F.js");
490
538
  getDatabase();
491
- const { mcpServer } = await import("./server-OVOACIOJ.js");
539
+ const { mcpServer } = await import("./server-WN6DCCUA.js");
492
540
  const {
493
541
  uiTools,
494
542
  projectTools,
@@ -502,7 +550,7 @@ program.command("mcp").description("Run as MCP server (for Claude Code integrati
502
550
  taskHubTools,
503
551
  esvpTools,
504
552
  knowledgeTools
505
- } = await import("./tools-Q7OZO732.js");
553
+ } = await import("./tools-VXU3JEQP.js");
506
554
  mcpServer.registerTools([
507
555
  ...uiTools,
508
556
  ...projectTools,
@@ -540,6 +588,94 @@ program.command("info").description("Show version and configuration info").actio
540
588
  console.log();
541
589
  }
542
590
  });
591
+ program.command("export").description("Export project in various formats").argument("<project-id>", "Project ID or slug").option("--format <format>", "Export format (infographic, applab, esvp)", "infographic").option("--output <path>", "Custom output path").option("--open", "Open file after generation").option("--compress", "Force image compression").option("--no-baseline", "Omit baseline info").action(async (projectId, opts) => {
592
+ try {
593
+ if (opts.format === "infographic") {
594
+ console.log(chalk.cyan(`
595
+ Exporting infographic for: ${projectId}
596
+ `));
597
+ const { join: pathJoin } = await import("path");
598
+ const { getDatabase, projects, frames: framesTable, FRAMES_DIR, EXPORTS_DIR, PROJECTS_DIR } = await import("./db-5ECN3O7F.js");
599
+ const { eq } = await import("drizzle-orm");
600
+ const { collectFrameImages, buildInfographicData, generateInfographicHtml } = await import("./infographic-GQAHEOAA.js");
601
+ const db = getDatabase();
602
+ const allProjects = await db.select().from(projects);
603
+ const project = allProjects.find((p) => p.id === projectId || p.id.startsWith(projectId) || p.name.toLowerCase().includes(projectId.toLowerCase()));
604
+ if (!project) {
605
+ console.log(chalk.red(` Project not found: ${projectId}`));
606
+ console.log(chalk.gray(" Available projects:"));
607
+ for (const p of allProjects.slice(0, 10)) {
608
+ console.log(chalk.gray(` ${p.id.slice(0, 12)} - ${p.marketingTitle || p.name}`));
609
+ }
610
+ return;
611
+ }
612
+ console.log(chalk.green(` \u2714 Found project: ${project.marketingTitle || project.name}`));
613
+ const dbFrames = await db.select().from(framesTable).where(eq(framesTable.projectId, project.id)).orderBy(framesTable.frameNumber).limit(20);
614
+ let frameFiles;
615
+ let frameOcr;
616
+ if (dbFrames.length > 0) {
617
+ frameFiles = dbFrames.map((f) => f.imagePath);
618
+ frameOcr = dbFrames;
619
+ } else {
620
+ frameFiles = collectFrameImages(pathJoin(FRAMES_DIR, project.id), project.videoPath, PROJECTS_DIR, project.id);
621
+ frameOcr = frameFiles.map(() => ({ ocrText: null }));
622
+ }
623
+ console.log(chalk.green(` \u2714 ${frameFiles.length} frames found`));
624
+ if (frameFiles.length === 0) {
625
+ console.log(chalk.red(" No frames found. Run analyzer first."));
626
+ return;
627
+ }
628
+ const data = buildInfographicData(project, frameFiles, frameOcr);
629
+ console.log(chalk.green(` \u2714 ${project.aiSummary ? "AI analysis loaded" : "No analysis (basic labels)"}`));
630
+ const slug = (project.marketingTitle || project.name || project.id).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
631
+ const outputPath = opts.output ? pathJoin(opts.output, `${slug}-infographic.html`) : pathJoin(EXPORTS_DIR, `${slug}-infographic.html`);
632
+ const result = generateInfographicHtml(data, outputPath);
633
+ if (result.success) {
634
+ const sizeKb = ((result.size || 0) / 1024).toFixed(1);
635
+ console.log(chalk.green(` \u2714 Exported: ${result.outputPath} (${sizeKb}KB, ${result.frameCount} frames)`));
636
+ if (opts.open) {
637
+ const { exec } = await import("child_process");
638
+ exec(`open "${result.outputPath}"`);
639
+ }
640
+ } else {
641
+ console.log(chalk.red(` Export failed: ${result.error}`));
642
+ }
643
+ } else {
644
+ console.log(chalk.yellow(` Format "${opts.format}" - use the web UI for applab/esvp exports.`));
645
+ }
646
+ } catch (error) {
647
+ console.log(chalk.red(` Export failed: ${error instanceof Error ? error.message : String(error)}`));
648
+ }
649
+ });
650
+ program.command("import").description("Import a shared .applab project bundle").argument("<file>", "Path to .applab file").action(async (file) => {
651
+ try {
652
+ const { resolve } = await import("path");
653
+ const filePath = resolve(file);
654
+ console.log(chalk.cyan(`
655
+ Importing: ${filePath}
656
+ `));
657
+ const { getDatabase, projects, frames: framesTable, DATA_DIR, FRAMES_DIR, PROJECTS_DIR } = await import("./db-5ECN3O7F.js");
658
+ const { importApplabBundle } = await import("./import-W2JEW254.js");
659
+ const db = getDatabase();
660
+ const result = await importApplabBundle(filePath, db, { projects, frames: framesTable }, {
661
+ dataDir: DATA_DIR,
662
+ framesDir: FRAMES_DIR,
663
+ projectsDir: PROJECTS_DIR
664
+ });
665
+ if (result.success) {
666
+ console.log(chalk.green(` \u2714 Imported: ${result.projectName}`));
667
+ console.log(chalk.green(` \u2714 ${result.frameCount} frames`));
668
+ console.log(chalk.gray(` ID: ${result.projectId}
669
+ `));
670
+ } else {
671
+ console.log(chalk.red(` Import failed: ${result.error}
672
+ `));
673
+ }
674
+ } catch (error) {
675
+ console.log(chalk.red(` Import failed: ${error instanceof Error ? error.message : String(error)}
676
+ `));
677
+ }
678
+ });
543
679
  program.parse();
544
680
  if (!process.argv.slice(2).length) {
545
681
  program.outputHelp();
@@ -0,0 +1,254 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>__TITLE__</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ :root { --accent: #6366f1; --bg: #0f0f17; --bg2: #16161f; --bg3: #1e1e2a; --text: #e4e4e7; --muted: #71717a; --ok: #22c55e; --warn: #eab308; }
10
+ body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; min-height: 100vh; overflow-x: hidden; }
11
+
12
+ /* Header */
13
+ .header { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; border-bottom: 1px solid rgba(255,255,255,0.05); }
14
+ .header-left { display: flex; align-items: center; gap: 12px; }
15
+ .header-title { font-size: 16px; font-weight: 700; }
16
+ .header-platform { font-size: 10px; padding: 2px 8px; background: rgba(99,102,241,0.12); color: var(--accent); border-radius: 10px; }
17
+ .header-date { font-size: 11px; color: var(--muted); }
18
+
19
+ /* Step chips */
20
+ .chips { display: flex; gap: 4px; padding: 10px 24px; overflow-x: auto; scrollbar-width: none; }
21
+ .chips::-webkit-scrollbar { display: none; }
22
+ .chip { font-size: 10px; padding: 4px 12px; border-radius: 16px; background: var(--bg2); border: 1px solid rgba(255,255,255,0.06); cursor: pointer; white-space: nowrap; transition: all 0.25s; color: var(--muted); font-weight: 500; }
23
+ .chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
24
+ .chip:hover:not(.active) { border-color: rgba(255,255,255,0.15); }
25
+
26
+ /* Main layout */
27
+ .main { display: grid; grid-template-columns: 1fr 300px; height: calc(100vh - 100px); overflow: hidden; }
28
+ @media (max-width: 768px) { .main { grid-template-columns: 1fr; } .panel { border-left: none; border-top: 1px solid rgba(255,255,255,0.05); max-height: 200px; } }
29
+
30
+ /* Viewer */
31
+ .viewer { display: flex; align-items: center; justify-content: center; position: relative; padding: 20px; }
32
+ .device-area { position: relative; display: flex; align-items: center; }
33
+ .phone { width: 320px; background: #000; border-radius: 28px; overflow: hidden; box-shadow: 0 16px 48px rgba(0,0,0,0.5); border: 1.5px solid rgba(255,255,255,0.06); position: relative; }
34
+ .phone img { width: 100%; display: block; transition: opacity 0.35s ease; }
35
+ .phone img.out { opacity: 0; position: absolute; top: 0; left: 0; }
36
+ .phone img.in { opacity: 1; position: relative; }
37
+
38
+ /* Side annotations (appear when paused) */
39
+ .side-annots { position: absolute; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 8px; opacity: 0; transition: opacity 0.35s; pointer-events: none; width: 160px; }
40
+ .side-annots.show { opacity: 1; pointer-events: auto; }
41
+ .side-annots.left { right: calc(100% + 16px); }
42
+ .side-annots.right { left: calc(100% + 16px); }
43
+ .side-annot { padding: 8px 10px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; position: relative; }
44
+ .side-annot::before { content: ''; position: absolute; top: 50%; width: 12px; height: 1px; background: rgba(255,255,255,0.15); }
45
+ .side-annots.left .side-annot::before { right: -13px; }
46
+ .side-annots.right .side-annot::before { left: -13px; }
47
+ .side-annot-title { font-size: 10px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 5px; }
48
+ .side-annot-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
49
+ .side-annot-desc { font-size: 9px; color: var(--muted); margin-top: 2px; line-height: 1.4; }
50
+
51
+ /* Controls */
52
+ .controls { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 10px; background: rgba(0,0,0,0.6); backdrop-filter: blur(8px); padding: 6px 14px; border-radius: 20px; }
53
+ .play-btn { width: 28px; height: 28px; border-radius: 50%; background: transparent; border: 1.5px solid rgba(255,255,255,0.3); color: var(--text); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
54
+ .play-btn:hover { border-color: var(--accent); color: var(--accent); }
55
+ .dots { display: flex; gap: 5px; }
56
+ .dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.15); cursor: pointer; transition: all 0.25s; }
57
+ .dot.active { background: var(--accent); width: 16px; border-radius: 3px; }
58
+ .status-text { font-size: 9px; color: rgba(255,255,255,0.35); }
59
+
60
+ /* Panel */
61
+ .panel { padding: 16px; border-left: 1px solid rgba(255,255,255,0.05); overflow-y: auto; background: var(--bg2); display: flex; flex-direction: column; gap: 16px; }
62
+ .panel-section { }
63
+ .panel-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted); margin-bottom: 6px; }
64
+ .step-number { font-size: 11px; color: var(--accent); font-weight: 700; margin-bottom: 2px; }
65
+ .step-title { font-size: 14px; font-weight: 600; line-height: 1.3; }
66
+ .step-desc { font-size: 12px; color: var(--muted); line-height: 1.5; margin-top: 4px; }
67
+
68
+ /* Annotation cards in panel */
69
+ .annot-cards { display: flex; flex-direction: column; gap: 6px; }
70
+ .annot-card { padding: 8px 10px; background: var(--bg3); border-radius: 8px; border-left: 3px solid var(--accent); cursor: pointer; transition: all 0.2s; }
71
+ .annot-card:hover { background: rgba(99,102,241,0.08); }
72
+ .annot-card-title { font-size: 11px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
73
+ .annot-card-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
74
+ .annot-card-desc { font-size: 10px; color: var(--muted); margin-top: 2px; line-height: 1.4; }
75
+
76
+ .baseline-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; padding: 3px 10px; border-radius: 12px; background: rgba(113,113,122,0.1); color: var(--muted); }
77
+ .baseline-badge.ok { background: rgba(34,197,94,0.1); color: var(--ok); }
78
+ .baseline-badge.warn { background: rgba(234,179,8,0.1); color: var(--warn); }
79
+ .bl-dot { width: 5px; height: 5px; border-radius: 50%; }
80
+
81
+ .footer { text-align: center; padding: 6px; font-size: 8px; color: rgba(255,255,255,0.12); }
82
+
83
+ /* Transitions */
84
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
85
+ .fade-in { animation: fadeIn 0.3s ease forwards; }
86
+ </style>
87
+ </head>
88
+ <body>
89
+ <div class="header">
90
+ <div class="header-left">
91
+ <div class="header-title" id="title"></div>
92
+ <div class="header-platform" id="platform"></div>
93
+ </div>
94
+ <div class="header-date" id="date"></div>
95
+ </div>
96
+ <div class="chips" id="chips"></div>
97
+ <div class="main">
98
+ <div class="viewer">
99
+ <div class="device-area" id="deviceArea" style="position: relative;">
100
+ <div class="side-annots left" id="annotsLeft"></div>
101
+ <div class="phone" id="phone"></div>
102
+ <div class="side-annots right" id="annotsRight"></div>
103
+ </div>
104
+ <div class="controls">
105
+ <button class="play-btn" id="playBtn">
106
+ <svg id="playIco" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><polygon points="6 3 20 12 6 21"/></svg>
107
+ <svg id="pauseIco" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" style="display:none"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
108
+ </button>
109
+ <div class="dots" id="dots"></div>
110
+ <div class="status-text" id="statusText">playing</div>
111
+ </div>
112
+ </div>
113
+ <div class="panel">
114
+ <div class="panel-section">
115
+ <div class="panel-label">Step</div>
116
+ <div class="step-number" id="stepNum"></div>
117
+ <div class="step-title" id="stepTitle"></div>
118
+ <div class="step-desc" id="stepDesc"></div>
119
+ </div>
120
+ <div class="panel-section">
121
+ <div class="panel-label">Annotations</div>
122
+ <div class="annot-cards" id="annotCards"></div>
123
+ </div>
124
+ <div class="panel-section">
125
+ <div class="panel-label">Baseline</div>
126
+ <div class="baseline-badge" id="blBadge"><div class="bl-dot"></div><span></span></div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ <div class="footer">Generated by DiscoveryLab</div>
131
+
132
+ <script>
133
+ const D = window.FLOW_DATA || { name: 'Flow', frames: [] };
134
+ const F = D.frames || [];
135
+ let cur = 0, playing = true, timer = null;
136
+
137
+ document.getElementById('title').textContent = D.name || 'App Flow';
138
+ document.getElementById('platform').textContent = D.platform || '';
139
+ document.getElementById('date').textContent = D.recorded_at ? new Date(D.recorded_at).toLocaleDateString() : '';
140
+
141
+ // Create images
142
+ const phone = document.getElementById('phone');
143
+ F.forEach((f, i) => {
144
+ const img = document.createElement('img');
145
+ img.src = f.base64;
146
+ img.className = i === 0 ? 'in' : 'out';
147
+ img.dataset.i = i;
148
+ phone.appendChild(img);
149
+ });
150
+
151
+ // Chips
152
+ const chips = document.getElementById('chips');
153
+ F.forEach((f, i) => {
154
+ const c = document.createElement('div');
155
+ c.className = `chip ${i===0?'active':''}`;
156
+ c.textContent = `${i+1}. ${f.step_name}`;
157
+ c.onclick = () => { go(i); stop(); };
158
+ chips.appendChild(c);
159
+ });
160
+
161
+ // Dots
162
+ const dots = document.getElementById('dots');
163
+ F.forEach((_, i) => {
164
+ const d = document.createElement('div');
165
+ d.className = `dot ${i===0?'active':''}`;
166
+ d.onclick = () => { go(i); stop(); };
167
+ dots.appendChild(d);
168
+ });
169
+
170
+ function go(i) {
171
+ cur = i;
172
+ phone.querySelectorAll('img').forEach((img, j) => { img.className = j===i?'in':'out'; });
173
+ chips.querySelectorAll('.chip').forEach((c, j) => c.classList.toggle('active', j===i));
174
+ dots.querySelectorAll('.dot').forEach((d, j) => d.classList.toggle('active', j===i));
175
+ updatePanel();
176
+ updateAnnotations();
177
+ }
178
+
179
+ function updatePanel() {
180
+ const f = F[cur]; if (!f) return;
181
+ document.getElementById('stepNum').textContent = `${cur+1} of ${F.length}`;
182
+ document.getElementById('stepTitle').textContent = f.step_name;
183
+ document.getElementById('stepDesc').textContent = f.description;
184
+
185
+ // Annotation cards
186
+ const cards = document.getElementById('annotCards');
187
+ cards.innerHTML = '';
188
+ (f.hotspots || []).forEach(h => {
189
+ const card = document.createElement('div');
190
+ card.className = 'annot-card fade-in';
191
+ card.style.borderLeftColor = h.color;
192
+ card.innerHTML = `<div class="annot-card-title"><div class="annot-card-dot" style="background:${h.color}"></div>${h.title}</div><div class="annot-card-desc">${h.description}</div>`;
193
+ cards.appendChild(card);
194
+ });
195
+
196
+ // Baseline
197
+ const bl = document.getElementById('blBadge');
198
+ const s = f.baseline_status || 'not_validated';
199
+ bl.className = `baseline-badge ${s==='ok'?'ok':s==='changed'?'warn':''}`;
200
+ bl.querySelector('.bl-dot').style.background = s==='ok'?'var(--ok)':s==='changed'?'var(--warn)':'var(--muted)';
201
+ bl.querySelector('span').textContent = s==='ok'?'Validated':s==='changed'?'Changed':'No baseline';
202
+ }
203
+
204
+ function updateAnnotations() {
205
+ const left = document.getElementById('annotsLeft');
206
+ const right = document.getElementById('annotsRight');
207
+ left.innerHTML = ''; right.innerHTML = '';
208
+
209
+ if (playing) {
210
+ left.classList.remove('show'); right.classList.remove('show');
211
+ return;
212
+ }
213
+ left.classList.add('show'); right.classList.add('show');
214
+
215
+ const f = F[cur]; if (!f?.hotspots?.length) return;
216
+
217
+ f.hotspots.forEach((h, j) => {
218
+ const container = j % 2 === 0 ? left : right;
219
+ const el = document.createElement('div');
220
+ el.className = 'side-annot fade-in';
221
+ el.style.animationDelay = (j * 0.1) + 's';
222
+ el.innerHTML = `
223
+ <div class="side-annot-title"><div class="side-annot-dot" style="background:${h.color}"></div>${h.label}</div>
224
+ ${h.description !== h.label ? `<div class="side-annot-desc">${h.description.slice(0, 60)}</div>` : ''}
225
+ `;
226
+ container.appendChild(el);
227
+ });
228
+ }
229
+
230
+ function play() {
231
+ playing = true;
232
+ document.getElementById('playIco').style.display = 'none';
233
+ document.getElementById('pauseIco').style.display = '';
234
+ document.getElementById('statusText').textContent = 'playing';
235
+ document.getElementById('annotsLeft').classList.remove('show');
236
+ document.getElementById('annotsRight').classList.remove('show');
237
+ timer = setInterval(() => { cur = (cur+1)%F.length; go(cur); }, 2200);
238
+ }
239
+
240
+ function stop() {
241
+ playing = false; clearInterval(timer);
242
+ document.getElementById('playIco').style.display = '';
243
+ document.getElementById('pauseIco').style.display = 'none';
244
+ document.getElementById('statusText').textContent = 'paused';
245
+ updateAnnotations();
246
+ }
247
+
248
+ document.getElementById('playBtn').onclick = () => playing ? stop() : play();
249
+
250
+ updatePanel();
251
+ play();
252
+ </script>
253
+ </body>
254
+ </html>
@@ -0,0 +1,180 @@
1
+ import "./chunk-R5U7XKVJ.js";
2
+
3
+ // src/core/export/import.ts
4
+ import { existsSync, mkdirSync, readFileSync, cpSync, rmSync, readdirSync, statSync } from "fs";
5
+ import { join, basename } from "path";
6
+ import { execSync } from "child_process";
7
+ import { randomUUID } from "crypto";
8
+ function extractZip(zipPath, targetDir) {
9
+ mkdirSync(targetDir, { recursive: true });
10
+ if (process.platform === "darwin") {
11
+ try {
12
+ execSync(`ditto -xk "${zipPath}" "${targetDir}"`, { stdio: "pipe" });
13
+ return;
14
+ } catch {
15
+ }
16
+ }
17
+ execSync(`unzip -qo "${zipPath}" -d "${targetDir}"`, { stdio: "pipe" });
18
+ }
19
+ function findBundleRoot(extractDir) {
20
+ const entries = readdirSync(extractDir);
21
+ if (entries.length === 1) {
22
+ const candidate = join(extractDir, entries[0]);
23
+ if (statSync(candidate).isDirectory()) return candidate;
24
+ }
25
+ if (entries.includes("manifest.json")) return extractDir;
26
+ for (const entry of entries) {
27
+ const dir = join(extractDir, entry);
28
+ if (statSync(dir).isDirectory() && existsSync(join(dir, "manifest.json"))) {
29
+ return dir;
30
+ }
31
+ }
32
+ return extractDir;
33
+ }
34
+ async function importApplabBundle(zipPath, db, schema, paths) {
35
+ if (!existsSync(zipPath)) {
36
+ return { success: false, error: `File not found: ${zipPath}` };
37
+ }
38
+ const tempDir = join(paths.dataDir, ".import-temp-" + Date.now());
39
+ try {
40
+ extractZip(zipPath, tempDir);
41
+ const bundleRoot = findBundleRoot(tempDir);
42
+ const manifestPath = join(bundleRoot, "manifest.json");
43
+ if (!existsSync(manifestPath)) {
44
+ return { success: false, error: "Invalid .applab file: manifest.json not found" };
45
+ }
46
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
47
+ let projectData = {};
48
+ const projectJsonPath = join(bundleRoot, "metadata", "project.json");
49
+ if (existsSync(projectJsonPath)) {
50
+ projectData = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
51
+ }
52
+ const projectId = manifest.id || projectData.id || randomUUID();
53
+ const projectName = manifest.name || projectData.name || basename(zipPath, ".applab");
54
+ const { eq } = await import("drizzle-orm");
55
+ const existing = await db.select().from(schema.projects).where(eq(schema.projects.id, projectId)).limit(1);
56
+ if (existing.length > 0) {
57
+ return { success: false, error: `Project already exists: ${projectName} (${projectId}). Delete it first to re-import.` };
58
+ }
59
+ let frameCount = 0;
60
+ const framesSourceDir = join(bundleRoot, "frames");
61
+ const framesTargetDir = join(paths.framesDir, projectId);
62
+ if (existsSync(framesSourceDir)) {
63
+ mkdirSync(framesTargetDir, { recursive: true });
64
+ cpSync(framesSourceDir, framesTargetDir, { recursive: true });
65
+ frameCount = readdirSync(framesTargetDir).filter((f) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f)).length;
66
+ }
67
+ const projectTargetDir = join(paths.projectsDir, projectId);
68
+ mkdirSync(projectTargetDir, { recursive: true });
69
+ const mediaDir = join(bundleRoot, "media");
70
+ if (existsSync(mediaDir)) {
71
+ cpSync(mediaDir, projectTargetDir, { recursive: true });
72
+ }
73
+ const recordingDir = join(bundleRoot, "recording");
74
+ if (existsSync(recordingDir)) {
75
+ cpSync(recordingDir, projectTargetDir, { recursive: true });
76
+ }
77
+ let aiSummary = null;
78
+ let ocrText = null;
79
+ const aiPath = join(bundleRoot, "analysis", "app-intelligence.md");
80
+ const ocrPath = join(bundleRoot, "analysis", "ocr.txt");
81
+ if (existsSync(aiPath)) aiSummary = readFileSync(aiPath, "utf-8");
82
+ if (existsSync(ocrPath)) ocrText = readFileSync(ocrPath, "utf-8");
83
+ let taskHubLinks = null;
84
+ const linksPath = join(bundleRoot, "taskhub", "links.json");
85
+ if (existsSync(linksPath)) {
86
+ taskHubLinks = readFileSync(linksPath, "utf-8");
87
+ }
88
+ let videoPath = null;
89
+ const videoExts = [".mp4", ".mov", ".webm"];
90
+ for (const ext of videoExts) {
91
+ const files = readdirSync(projectTargetDir).filter((f) => f.endsWith(ext));
92
+ if (files.length > 0) {
93
+ videoPath = join(projectTargetDir, files[0]);
94
+ break;
95
+ }
96
+ }
97
+ if (!videoPath) videoPath = projectTargetDir;
98
+ let thumbnailPath = null;
99
+ if (frameCount > 0) {
100
+ const firstFrame = readdirSync(framesTargetDir).filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f)).sort()[0];
101
+ if (firstFrame) thumbnailPath = join(framesTargetDir, firstFrame);
102
+ }
103
+ const now = /* @__PURE__ */ new Date();
104
+ await db.insert(schema.projects).values({
105
+ id: projectId,
106
+ name: projectName,
107
+ marketingTitle: projectData.marketingTitle || projectName,
108
+ marketingDescription: projectData.marketingDescription || null,
109
+ videoPath,
110
+ thumbnailPath,
111
+ platform: manifest.platform || projectData.platform || null,
112
+ aiSummary,
113
+ ocrText,
114
+ ocrEngine: projectData.ocrEngine || null,
115
+ ocrConfidence: projectData.ocrConfidence || null,
116
+ frameCount,
117
+ duration: projectData.duration || null,
118
+ manualNotes: projectData.manualNotes || null,
119
+ tags: projectData.tags || null,
120
+ linkedTicket: projectData.linkedTicket || null,
121
+ linkedJiraUrl: projectData.linkedJiraUrl || null,
122
+ linkedNotionUrl: projectData.linkedNotionUrl || null,
123
+ linkedFigmaUrl: projectData.linkedFigmaUrl || null,
124
+ taskHubLinks,
125
+ taskRequirements: projectData.taskRequirements || null,
126
+ taskTestMap: projectData.taskTestMap || null,
127
+ status: aiSummary ? "analyzed" : "draft",
128
+ createdAt: projectData.createdAt ? new Date(projectData.createdAt) : now,
129
+ updatedAt: now
130
+ });
131
+ if (frameCount > 0) {
132
+ const frameFiles = readdirSync(framesTargetDir).filter((f) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f)).sort();
133
+ let frameAnalysis = {};
134
+ const framesJsonPath = join(bundleRoot, "analysis", "frames.json");
135
+ if (existsSync(framesJsonPath)) {
136
+ try {
137
+ frameAnalysis = JSON.parse(readFileSync(framesJsonPath, "utf-8"));
138
+ } catch {
139
+ }
140
+ }
141
+ for (let i = 0; i < frameFiles.length; i++) {
142
+ const frameId = `${projectId}-frame-${i}`;
143
+ const framePath = join(framesTargetDir, frameFiles[i]);
144
+ const frameOcr = frameAnalysis[`frame-${i}`]?.ocrText || null;
145
+ await db.insert(schema.frames).values({
146
+ id: frameId,
147
+ projectId,
148
+ frameNumber: i,
149
+ timestamp: i,
150
+ // approximate
151
+ imagePath: framePath,
152
+ ocrText: frameOcr,
153
+ isKeyFrame: i === 0,
154
+ createdAt: now
155
+ });
156
+ }
157
+ }
158
+ return {
159
+ success: true,
160
+ projectId,
161
+ projectName,
162
+ frameCount
163
+ };
164
+ } catch (error) {
165
+ return {
166
+ success: false,
167
+ error: error instanceof Error ? error.message : String(error)
168
+ };
169
+ } finally {
170
+ if (existsSync(tempDir)) {
171
+ try {
172
+ rmSync(tempDir, { recursive: true, force: true });
173
+ } catch {
174
+ }
175
+ }
176
+ }
177
+ }
178
+ export {
179
+ importApplabBundle
180
+ };