tauri-test-cli 0.6.2 → 0.7.1

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/cli.js CHANGED
@@ -102312,7 +102312,7 @@ var init_esm11 = __esm(() => {
102312
102312
  });
102313
102313
 
102314
102314
  // src/cli.ts
102315
- import { resolve } from "path";
102315
+ import { resolve as resolve2 } from "path";
102316
102316
 
102317
102317
  // src/driver.ts
102318
102318
  import { spawn, execSync } from "child_process";
@@ -120707,7 +120707,9 @@ async function connect(options) {
120707
120707
  }
120708
120708
  async function disconnect() {
120709
120709
  if (browser) {
120710
- await browser.deleteSession();
120710
+ try {
120711
+ await browser.deleteSession();
120712
+ } catch {}
120711
120713
  browser = null;
120712
120714
  }
120713
120715
  stopDriver();
@@ -120905,12 +120907,18 @@ async function waitForInteractive(selector, timeout = 5000) {
120905
120907
  await element.waitForClickable({ timeout });
120906
120908
  }
120907
120909
 
120910
+ // src/commands/utils.ts
120911
+ var xvfbDisplay = null;
120912
+ function getXvfbDisplay() {
120913
+ return xvfbDisplay;
120914
+ }
120915
+
120908
120916
  // src/commands/screenshot.ts
120909
120917
  var HTML2CANVAS_CDN = "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js";
120910
120918
  async function screenshot(options = {}) {
120911
120919
  const browser3 = requireBrowser();
120912
120920
  const autoWait = options.autoWait ?? true;
120913
- const timeout = options.timeout ?? 5000;
120921
+ const timeout = options.timeout ?? 15000;
120914
120922
  if (autoWait) {
120915
120923
  await waitForDomStable();
120916
120924
  }
@@ -120921,12 +120929,21 @@ async function screenshot(options = {}) {
120921
120929
  data2 = await withTimeout(captureWithHtml2Canvas(browser3), timeout, "html2canvas timed out");
120922
120930
  method = "html2canvas";
120923
120931
  } catch (err) {
120924
- console.error(`html2canvas failed: ${err}, trying native...`);
120932
+ console.error(`html2canvas failed: ${err}, trying canvas fallback...`);
120925
120933
  try {
120926
- data2 = await withTimeout(browser3.takeScreenshot(), timeout, `Native screenshot timed out after ${timeout}ms`);
120927
- method = "native";
120928
- } catch (nativeErr) {
120929
- throw new Error(`All screenshot methods failed: ${err}, ${nativeErr}`);
120934
+ data2 = await withTimeout(captureWithCanvas(browser3), timeout, "Canvas screenshot timed out");
120935
+ method = "canvas";
120936
+ } catch (canvasErr) {
120937
+ if (getXvfbDisplay() !== null) {
120938
+ throw new Error(`All screenshot methods failed in Xvfb: html2canvas: ${err}, canvas: ${canvasErr}`);
120939
+ }
120940
+ console.error(`Canvas failed: ${canvasErr}, trying native...`);
120941
+ try {
120942
+ data2 = await withTimeout(browser3.takeScreenshot(), timeout, `Native screenshot timed out after ${timeout}ms`);
120943
+ method = "native";
120944
+ } catch (nativeErr) {
120945
+ throw new Error(`All screenshot methods failed: html2canvas: ${err}, canvas: ${canvasErr}, native: ${nativeErr}`);
120946
+ }
120930
120947
  }
120931
120948
  }
120932
120949
  if (options.output) {
@@ -120993,6 +121010,61 @@ async function captureWithHtml2Canvas(browser3) {
120993
121010
  }
120994
121011
  return base64;
120995
121012
  }
121013
+ async function captureWithCanvas(browser3) {
121014
+ const base64 = await browser3.executeAsync((done) => {
121015
+ try {
121016
+ const w2 = window.innerWidth || 800;
121017
+ const h = window.innerHeight || 600;
121018
+ const canvas = document.createElement("canvas");
121019
+ canvas.width = w2;
121020
+ canvas.height = h;
121021
+ const ctx = canvas.getContext("2d");
121022
+ ctx.fillStyle = "#ffffff";
121023
+ ctx.fillRect(0, 0, w2, h);
121024
+ const serializer = new XMLSerializer;
121025
+ const cloned = document.documentElement.cloneNode(true);
121026
+ const html3 = serializer.serializeToString(cloned);
121027
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w2}" height="${h}">
121028
+ <foreignObject width="100%" height="100%">
121029
+ ${html3}
121030
+ </foreignObject>
121031
+ </svg>`;
121032
+ const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
121033
+ const url2 = URL.createObjectURL(blob);
121034
+ const img = new Image;
121035
+ img.onload = () => {
121036
+ ctx.drawImage(img, 0, 0);
121037
+ URL.revokeObjectURL(url2);
121038
+ const dataUrl = canvas.toDataURL("image/png");
121039
+ done(dataUrl.replace(/^data:image\/png;base64,/, ""));
121040
+ };
121041
+ img.onerror = () => {
121042
+ URL.revokeObjectURL(url2);
121043
+ ctx.fillStyle = "#000000";
121044
+ ctx.font = "16px monospace";
121045
+ const text3 = document.body.innerText || "";
121046
+ const lines = text3.split(`
121047
+ `);
121048
+ for (let i = 0;i < lines.length && i < 40; i++) {
121049
+ ctx.fillText(lines[i], 10, 20 + i * 20);
121050
+ }
121051
+ const dataUrl = canvas.toDataURL("image/png");
121052
+ done(dataUrl.replace(/^data:image\/png;base64,/, ""));
121053
+ };
121054
+ img.src = url2;
121055
+ setTimeout(() => {
121056
+ const dataUrl = canvas.toDataURL("image/png");
121057
+ done(dataUrl.replace(/^data:image\/png;base64,/, ""));
121058
+ }, 4000);
121059
+ } catch (e) {
121060
+ done("");
121061
+ }
121062
+ });
121063
+ if (!base64) {
121064
+ throw new Error("Canvas screenshot capture returned empty");
121065
+ }
121066
+ return base64;
121067
+ }
120996
121068
 
120997
121069
  // src/commands/snapshot.ts
120998
121070
  import { writeFile as writeFile2 } from "fs/promises";
@@ -121250,21 +121322,17 @@ async function waitFor(selector, options = {}) {
121250
121322
  // src/commands/eval.ts
121251
121323
  async function evaluate(script) {
121252
121324
  const browser3 = requireBrowser();
121253
- const wrappedScript = `
121254
- return (function() {
121255
- ${script}
121256
- })();
121257
- `;
121258
121325
  try {
121326
+ const result = await browser3.execute(`return ${script}`);
121327
+ return result;
121328
+ } catch {
121329
+ const wrappedScript = `
121330
+ return (function() {
121331
+ ${script}
121332
+ })();
121333
+ `;
121259
121334
  const result = await browser3.execute(wrappedScript);
121260
121335
  return result;
121261
- } catch (err) {
121262
- try {
121263
- const result = await browser3.execute(`return ${script}`);
121264
- return result;
121265
- } catch {
121266
- throw err;
121267
- }
121268
121336
  }
121269
121337
  }
121270
121338
 
@@ -121278,7 +121346,7 @@ var HTML2CANVAS_CDN2 = "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html
121278
121346
  async function screenshot2(options = {}) {
121279
121347
  const browser3 = requireBrowser();
121280
121348
  const autoWait = options.autoWait ?? true;
121281
- const timeout = options.timeout ?? 5000;
121349
+ const timeout = options.timeout ?? 15000;
121282
121350
  if (autoWait) {
121283
121351
  await waitForDomStable();
121284
121352
  }
@@ -121289,12 +121357,21 @@ async function screenshot2(options = {}) {
121289
121357
  data2 = await withTimeout2(captureWithHtml2Canvas2(browser3), timeout, "html2canvas timed out");
121290
121358
  method = "html2canvas";
121291
121359
  } catch (err) {
121292
- console.error(`html2canvas failed: ${err}, trying native...`);
121360
+ console.error(`html2canvas failed: ${err}, trying canvas fallback...`);
121293
121361
  try {
121294
- data2 = await withTimeout2(browser3.takeScreenshot(), timeout, `Native screenshot timed out after ${timeout}ms`);
121295
- method = "native";
121296
- } catch (nativeErr) {
121297
- throw new Error(`All screenshot methods failed: ${err}, ${nativeErr}`);
121362
+ data2 = await withTimeout2(captureWithCanvas2(browser3), timeout, "Canvas screenshot timed out");
121363
+ method = "canvas";
121364
+ } catch (canvasErr) {
121365
+ if (getXvfbDisplay() !== null) {
121366
+ throw new Error(`All screenshot methods failed in Xvfb: html2canvas: ${err}, canvas: ${canvasErr}`);
121367
+ }
121368
+ console.error(`Canvas failed: ${canvasErr}, trying native...`);
121369
+ try {
121370
+ data2 = await withTimeout2(browser3.takeScreenshot(), timeout, `Native screenshot timed out after ${timeout}ms`);
121371
+ method = "native";
121372
+ } catch (nativeErr) {
121373
+ throw new Error(`All screenshot methods failed: html2canvas: ${err}, canvas: ${canvasErr}, native: ${nativeErr}`);
121374
+ }
121298
121375
  }
121299
121376
  }
121300
121377
  if (options.output) {
@@ -121361,6 +121438,61 @@ async function captureWithHtml2Canvas2(browser3) {
121361
121438
  }
121362
121439
  return base64;
121363
121440
  }
121441
+ async function captureWithCanvas2(browser3) {
121442
+ const base64 = await browser3.executeAsync((done) => {
121443
+ try {
121444
+ const w2 = window.innerWidth || 800;
121445
+ const h = window.innerHeight || 600;
121446
+ const canvas = document.createElement("canvas");
121447
+ canvas.width = w2;
121448
+ canvas.height = h;
121449
+ const ctx = canvas.getContext("2d");
121450
+ ctx.fillStyle = "#ffffff";
121451
+ ctx.fillRect(0, 0, w2, h);
121452
+ const serializer = new XMLSerializer;
121453
+ const cloned = document.documentElement.cloneNode(true);
121454
+ const html3 = serializer.serializeToString(cloned);
121455
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w2}" height="${h}">
121456
+ <foreignObject width="100%" height="100%">
121457
+ ${html3}
121458
+ </foreignObject>
121459
+ </svg>`;
121460
+ const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
121461
+ const url2 = URL.createObjectURL(blob);
121462
+ const img = new Image;
121463
+ img.onload = () => {
121464
+ ctx.drawImage(img, 0, 0);
121465
+ URL.revokeObjectURL(url2);
121466
+ const dataUrl = canvas.toDataURL("image/png");
121467
+ done(dataUrl.replace(/^data:image\/png;base64,/, ""));
121468
+ };
121469
+ img.onerror = () => {
121470
+ URL.revokeObjectURL(url2);
121471
+ ctx.fillStyle = "#000000";
121472
+ ctx.font = "16px monospace";
121473
+ const text3 = document.body.innerText || "";
121474
+ const lines = text3.split(`
121475
+ `);
121476
+ for (let i = 0;i < lines.length && i < 40; i++) {
121477
+ ctx.fillText(lines[i], 10, 20 + i * 20);
121478
+ }
121479
+ const dataUrl = canvas.toDataURL("image/png");
121480
+ done(dataUrl.replace(/^data:image\/png;base64,/, ""));
121481
+ };
121482
+ img.src = url2;
121483
+ setTimeout(() => {
121484
+ const dataUrl = canvas.toDataURL("image/png");
121485
+ done(dataUrl.replace(/^data:image\/png;base64,/, ""));
121486
+ }, 4000);
121487
+ } catch (e) {
121488
+ done("");
121489
+ }
121490
+ });
121491
+ if (!base64) {
121492
+ throw new Error("Canvas screenshot capture returned empty");
121493
+ }
121494
+ return base64;
121495
+ }
121364
121496
 
121365
121497
  // src/commands/snapshot.ts
121366
121498
  import { writeFile as writeFile4 } from "fs/promises";
@@ -121618,21 +121750,17 @@ async function waitFor2(selector, options = {}) {
121618
121750
  // src/commands/eval.ts
121619
121751
  async function evaluate2(script) {
121620
121752
  const browser3 = requireBrowser();
121621
- const wrappedScript = `
121622
- return (function() {
121623
- ${script}
121624
- })();
121625
- `;
121626
121753
  try {
121754
+ const result = await browser3.execute(`return ${script}`);
121755
+ return result;
121756
+ } catch {
121757
+ const wrappedScript = `
121758
+ return (function() {
121759
+ ${script}
121760
+ })();
121761
+ `;
121627
121762
  const result = await browser3.execute(wrappedScript);
121628
121763
  return result;
121629
- } catch (err) {
121630
- try {
121631
- const result = await browser3.execute(`return ${script}`);
121632
- return result;
121633
- } catch {
121634
- throw err;
121635
- }
121636
121764
  }
121637
121765
  }
121638
121766
 
@@ -121657,12 +121785,16 @@ async function executeCommand(cmd, globalAutoWait) {
121657
121785
  console.error(`[${new Date().toISOString()}] activateWindow took ${Date.now() - activateStart}ms`);
121658
121786
  const cmdStart = Date.now();
121659
121787
  let result;
121788
+ if (cmd.cmd === "screenshot" || cmd.cmd === "snapshot") {
121789
+ await injectKeepAlive();
121790
+ }
121660
121791
  switch (cmd.cmd) {
121661
121792
  case "screenshot":
121662
121793
  result = await screenshot2({
121663
121794
  output: cmd.output,
121664
121795
  fullPage: cmd.fullPage,
121665
- autoWait
121796
+ autoWait,
121797
+ timeout: cmd.timeout
121666
121798
  });
121667
121799
  break;
121668
121800
  case "snapshot":
@@ -122131,7 +122263,7 @@ async function waitForDisplay(display, timeoutMs = 1e4) {
122131
122263
  return false;
122132
122264
  }
122133
122265
  var xvfbProcess = null;
122134
- var xvfbDisplay = null;
122266
+ var xvfbDisplay2 = null;
122135
122267
  async function startXvfb() {
122136
122268
  const display = findAvailableDisplay();
122137
122269
  const displayStr = `:${display}`;
@@ -122161,7 +122293,7 @@ async function startXvfb() {
122161
122293
  throw new Error(`Xvfb display ${displayStr} failed to start within timeout`);
122162
122294
  }
122163
122295
  console.error(`Xvfb ready on display ${displayStr}`);
122164
- xvfbDisplay = display;
122296
+ xvfbDisplay2 = display;
122165
122297
  process.env.DISPLAY = displayStr;
122166
122298
  delete process.env.WAYLAND_DISPLAY;
122167
122299
  process.env.GDK_BACKEND = "x11";
@@ -122172,10 +122304,250 @@ function stopXvfb() {
122172
122304
  console.error("Stopping Xvfb...");
122173
122305
  xvfbProcess.kill("SIGTERM");
122174
122306
  xvfbProcess = null;
122175
- xvfbDisplay = null;
122307
+ xvfbDisplay2 = null;
122176
122308
  }
122177
122309
  }
122178
122310
 
122311
+ // src/commands/install-skill.ts
122312
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
122313
+ import { resolve, dirname, join as join3 } from "path";
122314
+ import { homedir as homedir3 } from "os";
122315
+
122316
+ // src/generated/skill-content.ts
122317
+ var SKILL_MD_CONTENT = `---
122318
+ name: tauri-test-cli
122319
+ description: Use when needing to visually verify Tauri app behavior, test UI interactions, take screenshots, or inspect DOM state.
122320
+ ---
122321
+
122322
+ # tauri-test-cli
122323
+
122324
+ Visual testing CLI for Tauri apps. Start server once, send commands anytime.
122325
+
122326
+ ## Step 1: Detect Runtime & Set Command Prefix
122327
+
122328
+ > **Local dependency?** If \`node_modules/tauri-test-cli\` exists in the project, skip Steps 1-2 and use \`npx tauri-test-cli\` (or \`pnpm exec tauri-test-cli\` / \`bunx tauri-test-cli\`) as your command prefix.
122329
+
122330
+ Determine how to invoke tauri-test-cli based on the project:
122331
+
122332
+ \`\`\`bash
122333
+ # Check what's available
122334
+ ls pixi.toml bun.lock package-lock.json pnpm-lock.yaml 2>/dev/null
122335
+ \`\`\`
122336
+
122337
+ Set your command prefix for ALL subsequent commands:
122338
+ - **pixi + bun** (pixi.toml + bun.lock): \`pixi run bunx tauri-test-cli\`
122339
+ - **pixi + npm** (pixi.toml, no bun): \`pixi run npx tauri-test-cli\`
122340
+ - **bun** (bun.lock, no pixi): \`bunx tauri-test-cli\`
122341
+ - **npm** (package-lock.json): \`npx tauri-test-cli\`
122342
+ - **pnpm** (pnpm-lock.yaml): \`pnpm dlx tauri-test-cli\`
122343
+
122344
+ Use this prefix for EVERY command below. Examples below use \`CMD\` as placeholder.
122345
+
122346
+ ## Step 2: Ensure Latest Version
122347
+
122348
+ **bunx and npx cache aggressively.** Always clear all caches before running:
122349
+
122350
+ \`\`\`bash
122351
+ # Clear ALL known cache locations
122352
+ rm -rf /tmp/bunx-*tauri-test-cli* 2>/dev/null # bunx temp cache (primary)
122353
+ rm -rf ~/.bun/install/cache/tauri-test-cli* 2>/dev/null # bun install cache
122354
+ rm -rf ~/.npm/_npx/*/node_modules/tauri-test-cli 2>/dev/null # npx cache
122355
+ \`\`\`
122356
+
122357
+ This ensures you always get the latest published version.
122358
+
122359
+ ## Step 3: Check Dependencies
122360
+
122361
+ \`\`\`bash
122362
+ CMD check-deps
122363
+ \`\`\`
122364
+
122365
+ If tauri-driver is missing:
122366
+ \`\`\`bash
122367
+ CMD setup
122368
+ \`\`\`
122369
+
122370
+ ## Step 4: Find the App Binary
122371
+
122372
+ \`\`\`bash
122373
+ # Search for tauri.conf.json to find the app name
122374
+ find . -name "tauri.conf.json" -not -path "*/node_modules/*" 2>/dev/null
122375
+
122376
+ # Common binary locations:
122377
+ # ./target/debug/app-name
122378
+ # ./src-tauri/target/debug/app-name
122379
+ # ./apps/*/src-tauri/target/debug/app-name
122380
+ \`\`\`
122381
+
122382
+ Read \`tauri.conf.json\` to get the app identifier/name. Check if the binary exists:
122383
+ \`\`\`bash
122384
+ ls ./path/to/target/debug/app-name 2>/dev/null
122385
+ \`\`\`
122386
+
122387
+ If missing, build it:
122388
+ \`\`\`bash
122389
+ # If pixi project:
122390
+ pixi run cargo build --manifest-path ./path/to/src-tauri/Cargo.toml
122391
+ # Otherwise:
122392
+ cd ./path/to/src-tauri && cargo build
122393
+ \`\`\`
122394
+
122395
+ ## Step 5: Check if Dev Server Needed
122396
+
122397
+ Read \`tauri.conf.json\` — if it has a \`devUrl\` (e.g., \`http://localhost:5173\`), the debug binary needs the frontend dev server running.
122398
+
122399
+ \`\`\`bash
122400
+ # 1. Check if devUrl exists
122401
+ grep -o '"devUrl".*' path/to/tauri.conf.json
122402
+
122403
+ # 2. If devUrl found, check if the dev server is already reachable
122404
+ curl -s -o /dev/null -w "%{http_code}" http://localhost:5173
122405
+
122406
+ # 3. If curl fails (connection refused / non-200), start the dev server
122407
+ # Look at package.json for the right dev command
122408
+ cd frontend-dir && npm run dev &
122409
+ sleep 5
122410
+
122411
+ # 4. Verify it's reachable now
122412
+ curl -s -o /dev/null -w "%{http_code}" http://localhost:5173
122413
+ \`\`\`
122414
+
122415
+ If \`build/\` or \`dist/\` exists in the frontend directory, the binary may work without a dev server — but verify by checking the app loads correctly after starting the server.
122416
+
122417
+ ## Step 6: Clean Up & Start Server
122418
+
122419
+ **IMPORTANT on Linux:** Always use \`--xvfb\` flag. This avoids window focus and rendering issues.
122420
+
122421
+ \`\`\`bash
122422
+ # Clean up any stale processes first
122423
+ CMD cleanup
122424
+
122425
+ # Start server (--xvfb on Linux, omit on macOS)
122426
+ CMD server --app ./path/to/binary --xvfb &
122427
+
122428
+ # Wait for server — it can take 15-20 seconds
122429
+ sleep 15
122430
+ CMD status
122431
+ \`\`\`
122432
+
122433
+ If status says "No server running", wait longer:
122434
+ \`\`\`bash
122435
+ sleep 10
122436
+ CMD status
122437
+ \`\`\`
122438
+
122439
+ **DO NOT** try to manually start Xvfb, use the existing Wayland display, or work around \`--xvfb\` failures. If \`--xvfb\` fails, check the error message and report it.
122440
+
122441
+ ## Quick Reference
122442
+
122443
+ | Action | Command |
122444
+ |--------|---------|
122445
+ | Check deps | \`CMD check-deps\` |
122446
+ | Status | \`CMD status\` |
122447
+ | Start server | \`CMD server --app ./path/to/app --xvfb &\` |
122448
+ | Click | \`CMD click "selector"\` |
122449
+ | Type | \`CMD type "selector" "text"\` |
122450
+ | Screenshot | \`CMD screenshot --output /tmp/screen.png\` |
122451
+ | DOM snapshot | \`CMD snapshot --output /tmp/dom.yaml\` |
122452
+ | Wait appear | \`CMD wait "selector" --timeout 3000\` |
122453
+ | Wait gone | \`CMD wait "selector" --gone --timeout 5000\` |
122454
+ | Eval JS | \`CMD eval "document.title"\` |
122455
+ | Stop | \`CMD stop\` |
122456
+ | Cleanup | \`CMD cleanup\` |
122457
+
122458
+ ## After Server Starts
122459
+
122460
+ \`\`\`bash
122461
+ # Always take an initial screenshot to verify the app loaded
122462
+ CMD screenshot --output /tmp/initial.png
122463
+ # Use Read tool to view the screenshot!
122464
+
122465
+ # Run test commands
122466
+ CMD click "button"
122467
+ CMD screenshot --output /tmp/after-click.png
122468
+
122469
+ # When done
122470
+ CMD stop
122471
+ \`\`\`
122472
+
122473
+ ## Troubleshooting
122474
+
122475
+ | Error | Cause | Fix |
122476
+ |-------|-------|-----|
122477
+ | "Connection refused" in screenshot | Frontend dev server not running | Start frontend: \`npm run dev\` |
122478
+ | Blank/white screen | App not loaded yet | Increase \`--wait\` timeout |
122479
+ | "tauri-driver not found" | Missing dependency | Run \`CMD setup\` |
122480
+ | "Maximum sessions" error | Stale processes | Run \`CMD cleanup\` |
122481
+ | "Xvfb display failed to start" | Old CLI version (< 0.6.2) | Clear caches: \`rm -rf /tmp/bunx-*tauri-test-cli* ~/.bun/install/cache/tauri-test-cli*\` and retry |
122482
+ | Server not ready after 15s | Slow app startup | Wait longer, check if dev server is needed |
122483
+
122484
+ ## Screenshot vs Snapshot
122485
+
122486
+ - **Screenshot**: Visual appearance, layout, colors
122487
+ - **DOM snapshot**: Element existence, text content, attributes
122488
+ - **Use snapshot** for canvas elements (screenshots can't capture canvas)
122489
+
122490
+ ## Cleanup (if stuck)
122491
+
122492
+ \`\`\`bash
122493
+ CMD stop
122494
+ CMD cleanup
122495
+ pkill Xvfb 2>/dev/null # Linux only
122496
+ \`\`\`
122497
+ `;
122498
+
122499
+ // src/commands/install-skill.ts
122500
+ function findProjectRoot(cwd) {
122501
+ let dir = resolve(cwd);
122502
+ const root2 = dirname(dir) === dir ? dir : undefined;
122503
+ while (true) {
122504
+ if (existsSync(join3(dir, ".claude"))) {
122505
+ return dir;
122506
+ }
122507
+ const parent2 = dirname(dir);
122508
+ if (parent2 === dir)
122509
+ break;
122510
+ dir = parent2;
122511
+ }
122512
+ return null;
122513
+ }
122514
+ function installSkill(opts = {}) {
122515
+ let targetDir;
122516
+ let mode;
122517
+ if (opts.global && opts.project) {
122518
+ console.error("Error: cannot specify both --global and --project");
122519
+ return { success: false };
122520
+ }
122521
+ if (opts.global) {
122522
+ targetDir = join3(homedir3(), ".claude", "skills", "tauri-test-cli");
122523
+ mode = "global";
122524
+ } else if (opts.project) {
122525
+ targetDir = join3(process.cwd(), ".claude", "skills", "tauri-test-cli");
122526
+ mode = "project";
122527
+ } else {
122528
+ const projectRoot = findProjectRoot(process.cwd());
122529
+ if (projectRoot) {
122530
+ targetDir = join3(projectRoot, ".claude", "skills", "tauri-test-cli");
122531
+ mode = `project (${projectRoot})`;
122532
+ } else {
122533
+ targetDir = join3(homedir3(), ".claude", "skills", "tauri-test-cli");
122534
+ mode = "global";
122535
+ }
122536
+ }
122537
+ const targetPath = join3(targetDir, "SKILL.md");
122538
+ if (existsSync(targetPath)) {
122539
+ const existing = readFileSync2(targetPath, "utf-8");
122540
+ if (existing === SKILL_MD_CONTENT) {
122541
+ console.log(`Skill already up-to-date (${mode}): ${targetPath}`);
122542
+ return { success: true };
122543
+ }
122544
+ }
122545
+ mkdirSync(targetDir, { recursive: true });
122546
+ writeFileSync(targetPath, SKILL_MD_CONTENT, "utf-8");
122547
+ console.log(`Installed skill (${mode}): ${targetPath}`);
122548
+ return { success: true };
122549
+ }
122550
+
122179
122551
  // src/cli.ts
122180
122552
  async function sendToServer(port, cmd) {
122181
122553
  const url2 = `http://127.0.0.1:${port}`;
@@ -122256,6 +122628,9 @@ COMMANDS:
122256
122628
  check-deps
122257
122629
  Check if all required system dependencies are installed
122258
122630
 
122631
+ install-skill [--global | --project]
122632
+ Install Claude Code skill for tauri-test-cli
122633
+
122259
122634
  help
122260
122635
  Show this help message
122261
122636
 
@@ -122375,7 +122750,8 @@ async function executeCommand2(cmd, globalAutoWait) {
122375
122750
  return await screenshot({
122376
122751
  output: cmd.output,
122377
122752
  fullPage: cmd.fullPage,
122378
- autoWait
122753
+ autoWait,
122754
+ timeout: cmd.timeout
122379
122755
  });
122380
122756
  case "snapshot":
122381
122757
  return await snapshot({
@@ -122412,7 +122788,7 @@ async function executeCommand2(cmd, globalAutoWait) {
122412
122788
  }
122413
122789
  return await evaluate(cmd.script);
122414
122790
  case "sleep":
122415
- await new Promise((resolve2) => setTimeout(resolve2, cmd.ms ?? 1000));
122791
+ await new Promise((resolve3) => setTimeout(resolve3, cmd.ms ?? 1000));
122416
122792
  return { slept: cmd.ms ?? 1000 };
122417
122793
  default:
122418
122794
  throw new Error(`Unknown command: ${cmd.cmd}`);
@@ -122484,7 +122860,14 @@ async function main() {
122484
122860
  process.exit(1);
122485
122861
  }
122486
122862
  }
122487
- const appPath = options.app ? resolve(options.app) : undefined;
122863
+ if (command === "install-skill") {
122864
+ const result = installSkill({
122865
+ global: !!options.global,
122866
+ project: !!options.project
122867
+ });
122868
+ process.exit(result.success ? 0 : 1);
122869
+ }
122870
+ const appPath = options.app ? resolve2(options.app) : undefined;
122488
122871
  const jsonOutput = !!options.json;
122489
122872
  const port = options.port ? parseInt(options.port) : 9222;
122490
122873
  const clientModeCommands = ["screenshot", "snapshot", "click", "type", "wait", "eval"];
@@ -122507,7 +122890,8 @@ async function main() {
122507
122890
  cmd = {
122508
122891
  cmd: "screenshot",
122509
122892
  output: options.output,
122510
- fullPage: !!options["full-page"]
122893
+ fullPage: !!options["full-page"],
122894
+ timeout: options.timeout ? parseInt(options.timeout) : undefined
122511
122895
  };
122512
122896
  break;
122513
122897
  case "snapshot":
@@ -122666,7 +123050,8 @@ async function main() {
122666
123050
  result = await screenshot({
122667
123051
  output: options.output,
122668
123052
  fullPage: !!options["full-page"],
122669
- autoWait
123053
+ autoWait,
123054
+ timeout: options.timeout ? parseInt(options.timeout) : undefined
122670
123055
  });
122671
123056
  break;
122672
123057
  case "snapshot":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-test-cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "CLI for testing Tauri applications with screenshot capture, DOM inspection, and user interaction simulation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,8 @@
30
30
  "typecheck": "tsc --noEmit",
31
31
  "test": "bun test",
32
32
  "test:watch": "bun test --watch",
33
- "prepublishOnly": "bun run build && bun test",
33
+ "generate-skill-content": "bun scripts/generate-skill-content.ts",
34
+ "prepublishOnly": "bun run generate-skill-content && bun run build && bun test",
34
35
  "postinstall": "node scripts/postinstall.js"
35
36
  },
36
37
  "dependencies": {
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Reads skills/tauri-test-cli/SKILL.md and generates src/generated/skill-content.ts
4
+ * with the content embedded as a string constant for bundling.
5
+ */
6
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
7
+ import { resolve, dirname } from "path";
8
+
9
+ const ROOT = resolve(dirname(new URL(import.meta.url).pathname), "..");
10
+ const SKILL_PATH = resolve(ROOT, "skills/tauri-test-cli/SKILL.md");
11
+ const OUT_DIR = resolve(ROOT, "src/generated");
12
+ const OUT_PATH = resolve(OUT_DIR, "skill-content.ts");
13
+
14
+ const raw = readFileSync(SKILL_PATH, "utf-8");
15
+
16
+ // Escape for template literal embedding
17
+ const escaped = raw
18
+ .replace(/\\/g, "\\\\")
19
+ .replace(/`/g, "\\`")
20
+ .replace(/\$/g, "\\$");
21
+
22
+ const output = `// AUTO-GENERATED — do not edit. Run: bun scripts/generate-skill-content.ts
23
+ export const SKILL_MD_CONTENT = \`${escaped}\`;
24
+ `;
25
+
26
+ mkdirSync(OUT_DIR, { recursive: true });
27
+ writeFileSync(OUT_PATH, output, "utf-8");
28
+ console.log(`Generated ${OUT_PATH}`);