@ytspar/sweetlink 1.18.0 → 1.19.0

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.
@@ -1875,6 +1875,43 @@ const COMMAND_HELP = {
1875
1875
  pnpm sweetlink snapshot -i
1876
1876
  pnpm sweetlink click @e3
1877
1877
  pnpm sweetlink record stop`,
1878
+ sim: ` sim <ios|android> <command...>
1879
+ Record iOS Simulator or Android Emulator screen while running a command.
1880
+ Wraps \`xcrun simctl io booted recordVideo\` (iOS) or
1881
+ \`adb shell screenrecord\` (Android), writing an .mp4 of what was on
1882
+ screen during your XCUITest / Espresso / fastlane / appium run.
1883
+
1884
+ Options:
1885
+ --output <path> .mp4 path (default: .sweetlink/sim/<label>-<stamp>.mp4)
1886
+ --label <text> Embedded in filename
1887
+ --device <name|udid> Pick a specific simulator/emulator (default: first booted)
1888
+ --time-limit <sec> Android only — caps screen recording (max 180)
1889
+ --ignore-exit Don't propagate the recorded command's exit code
1890
+
1891
+ Requirements:
1892
+ iOS: Xcode + a booted Simulator (Simulator.app)
1893
+ Android: Android Platform Tools (\`adb\`) + a running emulator
1894
+
1895
+ Examples:
1896
+ pnpm sweetlink sim ios "fastlane scan" --device "iPhone 15"
1897
+ pnpm sweetlink sim android "./gradlew connectedAndroidTest"`,
1898
+ term: ` term <command...>
1899
+ Record a shell command's stdout/stderr into asciicast v2 + a self-contained
1900
+ HTML player. Captures real timing; the player has play/pause, 0.1×–4×
1901
+ speed, seek bar, and ANSI colour rendering.
1902
+
1903
+ Options:
1904
+ --output <path> .cast file path (default: .sweetlink/term/<label>-<stamp>.cast)
1905
+ --label <text> Label embedded in the .cast title + filename
1906
+ --shell <path> Shell to invoke the command in (default: /bin/sh)
1907
+ --cols <n> Reported terminal width (default: 120)
1908
+ --rows <n> Reported terminal height (default: 30)
1909
+ --ignore-exit Don't propagate the recorded command's exit code
1910
+
1911
+ Examples:
1912
+ pnpm sweetlink term "pytest -v" --label api-tests
1913
+ pnpm sweetlink term "go test ./..." --label go-tests
1914
+ pnpm sweetlink term "make build" --output .sweetlink/term/build.cast`,
1878
1915
  sessions: ` sessions [list|open]
1879
1916
  List or open all recorded sessions in this project.
1880
1917
 
@@ -2047,6 +2084,8 @@ if (hasFlag('--output-schema')) {
2047
2084
  'fill',
2048
2085
  'console',
2049
2086
  'record',
2087
+ 'term',
2088
+ 'sim',
2050
2089
  'sessions',
2051
2090
  'proof',
2052
2091
  'report',
@@ -2711,6 +2750,130 @@ async function handleStatusCommand() {
2711
2750
  }
2712
2751
  break;
2713
2752
  }
2753
+ case 'sim': {
2754
+ // Record iOS Simulator or Android Emulator screen while running a command.
2755
+ // Example: sweetlink sim ios "fastlane scan" --device "iPhone 15"
2756
+ const platform = args[1];
2757
+ if (platform !== 'ios' && platform !== 'android') {
2758
+ console.error('[Sweetlink] Usage: sweetlink sim <ios|android> "<command>" [--output path] [--device <name>]');
2759
+ process.exit(1);
2760
+ }
2761
+ const flagsWithValues = new Set(['--output', '--label', '--device', '--time-limit']);
2762
+ const positional = [];
2763
+ for (let i = 2; i < args.length; i++) {
2764
+ const a = args[i];
2765
+ if (a.startsWith('--')) {
2766
+ if (flagsWithValues.has(a))
2767
+ i++;
2768
+ continue;
2769
+ }
2770
+ positional.push(a);
2771
+ }
2772
+ const command = positional.join(' ').trim();
2773
+ if (!command) {
2774
+ console.error(`[Sweetlink] Error: sim ${platform} requires a command. Example: sweetlink sim ${platform} "fastlane scan"`);
2775
+ process.exit(1);
2776
+ }
2777
+ const label = getArg('--label');
2778
+ const labelSlug = label
2779
+ ? label.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 40)
2780
+ : `sim-${platform}`;
2781
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
2782
+ const defaultDir = path.join(findProjectRoot(), '.sweetlink', 'sim');
2783
+ const output = getArg('--output') ?? path.join(defaultDir, `${labelSlug}-${stamp}.mp4`);
2784
+ ensureDir(output);
2785
+ const device = getArg('--device');
2786
+ console.log(`[Sweetlink] Recording ${platform} simulator: ${command}`);
2787
+ let recResult;
2788
+ if (platform === 'ios') {
2789
+ const { recordIosSimulator } = await import('../simulator/ios.js');
2790
+ recResult = await recordIosSimulator({ command, output, device });
2791
+ }
2792
+ else {
2793
+ const { recordAndroidEmulator } = await import('../simulator/android.js');
2794
+ const tl = getArg('--time-limit');
2795
+ recResult = await recordAndroidEmulator({
2796
+ command, output, device,
2797
+ timeLimit: tl ? parseInt(tl, 10) : undefined,
2798
+ });
2799
+ }
2800
+ let sizeKb = '?';
2801
+ try {
2802
+ sizeKb = String(Math.round(fs.statSync(output).size / 1024));
2803
+ }
2804
+ catch { /* file may not exist if recordingClosed is false */ }
2805
+ console.log(`[Sweetlink] ${recResult.recordingClosed ? '✓' : '⚠'} ${getRelativePath(output)} · ` +
2806
+ `${recResult.durationSec.toFixed(1)}s · ${sizeKb}KB · ${recResult.device} · exit=${recResult.exitCode}` +
2807
+ (recResult.recordingClosed ? '' : ' (recording was force-killed; mp4 may be incomplete)'));
2808
+ result = {
2809
+ path: output,
2810
+ device: recResult.device,
2811
+ durationSec: recResult.durationSec,
2812
+ exitCode: recResult.exitCode,
2813
+ recordingClosed: recResult.recordingClosed,
2814
+ };
2815
+ if (recResult.exitCode !== 0 && !hasFlag('--ignore-exit')) {
2816
+ process.exit(recResult.exitCode);
2817
+ }
2818
+ break;
2819
+ }
2820
+ case 'term': {
2821
+ // Record a shell command's stdout/stderr into asciicast v2 + HTML player.
2822
+ // Example: sweetlink term "pytest -v" --label api-tests
2823
+ const flagsWithValues = new Set(['--output', '--label', '--shell', '--cols', '--rows']);
2824
+ const positional = [];
2825
+ for (let i = 1; i < args.length; i++) {
2826
+ const a = args[i];
2827
+ if (a.startsWith('--')) {
2828
+ if (flagsWithValues.has(a))
2829
+ i++;
2830
+ continue;
2831
+ }
2832
+ positional.push(a);
2833
+ }
2834
+ const command = positional.join(' ').trim();
2835
+ if (!command) {
2836
+ console.error('[Sweetlink] Error: term requires a command. Example: sweetlink term "pytest tests/"');
2837
+ process.exit(1);
2838
+ }
2839
+ const label = getArg('--label');
2840
+ const labelSlug = label
2841
+ ? label.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 40)
2842
+ : 'term';
2843
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
2844
+ const defaultDir = path.join(findProjectRoot(), '.sweetlink', 'term');
2845
+ const output = getArg('--output') ?? path.join(defaultDir, `${labelSlug}-${stamp}.cast`);
2846
+ ensureDir(output);
2847
+ console.log(`[Sweetlink] Recording terminal: ${command}`);
2848
+ const { captureTerminal } = await import('../term/recorder.js');
2849
+ const { generatePlayer } = await import('../term/player.js');
2850
+ const cap = await captureTerminal({
2851
+ command,
2852
+ output,
2853
+ label,
2854
+ shell: getArg('--shell'),
2855
+ cols: getArg('--cols') ? parseInt(getArg('--cols'), 10) : undefined,
2856
+ rows: getArg('--rows') ? parseInt(getArg('--rows'), 10) : undefined,
2857
+ });
2858
+ const playerPath = await generatePlayer({ castPath: output });
2859
+ console.log(`[Sweetlink] ✓ ${getRelativePath(output)} · ${cap.durationSec.toFixed(1)}s · ` +
2860
+ `${cap.events} events · ${(cap.bytes / 1024).toFixed(0)}KB · exit=${cap.exitCode}`);
2861
+ console.log(`[Sweetlink] ▶ ${getRelativePath(playerPath)}`);
2862
+ result = {
2863
+ castPath: output,
2864
+ playerPath,
2865
+ durationSec: cap.durationSec,
2866
+ bytes: cap.bytes,
2867
+ events: cap.events,
2868
+ exitCode: cap.exitCode,
2869
+ };
2870
+ // Propagate the recorded command's exit code by default so CI fails
2871
+ // when the wrapped tests fail.
2872
+ if (cap.exitCode !== 0 && !hasFlag('--ignore-exit')) {
2873
+ process.exit(cap.exitCode);
2874
+ }
2875
+ break;
2876
+ }
2714
2877
  case 'sessions': {
2715
2878
  const sub = args[1];
2716
2879
  const projRoot = findProjectRoot();