@supatest/cli 0.0.4 → 0.0.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 (69) hide show
  1. package/dist/commands/login.js +392 -0
  2. package/dist/commands/setup.js +234 -0
  3. package/dist/config.js +29 -0
  4. package/dist/core/agent.js +259 -0
  5. package/dist/index.js +154 -6586
  6. package/dist/modes/headless.js +117 -0
  7. package/dist/modes/interactive.js +418 -0
  8. package/dist/presenters/composite.js +32 -0
  9. package/dist/presenters/console.js +163 -0
  10. package/dist/presenters/react.js +217 -0
  11. package/dist/presenters/types.js +1 -0
  12. package/dist/presenters/web.js +78 -0
  13. package/dist/prompts/builder.js +181 -0
  14. package/dist/prompts/fixer.js +148 -0
  15. package/dist/prompts/index.js +3 -0
  16. package/dist/prompts/planner.js +70 -0
  17. package/dist/services/api-client.js +244 -0
  18. package/dist/services/event-streamer.js +130 -0
  19. package/dist/types.js +1 -0
  20. package/dist/ui/App.js +322 -0
  21. package/dist/ui/components/AuthBanner.js +24 -0
  22. package/dist/ui/components/AuthDialog.js +32 -0
  23. package/dist/ui/components/Banner.js +12 -0
  24. package/dist/ui/components/ExpandableSection.js +17 -0
  25. package/dist/ui/components/Header.js +51 -0
  26. package/dist/ui/components/HelpMenu.js +89 -0
  27. package/dist/ui/components/InputPrompt.js +286 -0
  28. package/dist/ui/components/MessageList.js +42 -0
  29. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  30. package/dist/ui/components/Scrollable.js +103 -0
  31. package/dist/ui/components/SessionSelector.js +196 -0
  32. package/dist/ui/components/StatusBar.js +34 -0
  33. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  34. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  35. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  36. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  37. package/dist/ui/components/messages/TodoMessage.js +44 -0
  38. package/dist/ui/components/messages/ToolMessage.js +218 -0
  39. package/dist/ui/components/messages/UserMessage.js +14 -0
  40. package/dist/ui/contexts/KeypressContext.js +527 -0
  41. package/dist/ui/contexts/MouseContext.js +98 -0
  42. package/dist/ui/contexts/SessionContext.js +129 -0
  43. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  44. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  45. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  46. package/dist/ui/hooks/useFocus.js +50 -0
  47. package/dist/ui/hooks/useKeypress.js +26 -0
  48. package/dist/ui/hooks/useModeToggle.js +25 -0
  49. package/dist/ui/types/auth.js +13 -0
  50. package/dist/ui/utils/file-completion.js +56 -0
  51. package/dist/ui/utils/input.js +50 -0
  52. package/dist/ui/utils/markdown.js +376 -0
  53. package/dist/ui/utils/mouse.js +189 -0
  54. package/dist/ui/utils/theme.js +59 -0
  55. package/dist/utils/banner.js +9 -0
  56. package/dist/utils/encryption.js +71 -0
  57. package/dist/utils/events.js +36 -0
  58. package/dist/utils/keychain-storage.js +120 -0
  59. package/dist/utils/logger.js +209 -0
  60. package/dist/utils/node-version.js +89 -0
  61. package/dist/utils/plan-file.js +75 -0
  62. package/dist/utils/project-instructions.js +23 -0
  63. package/dist/utils/rich-logger.js +208 -0
  64. package/dist/utils/stdin.js +25 -0
  65. package/dist/utils/stdio.js +80 -0
  66. package/dist/utils/summary.js +94 -0
  67. package/dist/utils/token-storage.js +242 -0
  68. package/dist/version.js +6 -0
  69. package/package.json +3 -4
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Session Context
3
+ * Manages the state of the interactive session including messages, todos, and agent status
4
+ */
5
+ import React, { createContext, useCallback, useContext, useState } from "react";
6
+ const SessionContext = createContext(null);
7
+ export const SessionProvider = ({ children, }) => {
8
+ const [messages, setMessages] = useState([]);
9
+ const [todos, setTodos] = useState([]);
10
+ const [stats, setStats] = useState({
11
+ filesModified: new Set(),
12
+ commandsRun: [],
13
+ iterations: 0,
14
+ startTime: Date.now(),
15
+ });
16
+ const [isAgentRunning, setIsAgentRunning] = useState(false);
17
+ const [shouldInterruptAgent, setShouldInterruptAgent] = useState(false);
18
+ const [sessionId, setSessionId] = useState();
19
+ const [webUrl, setWebUrl] = useState();
20
+ const [agentMode, setAgentMode] = useState('build');
21
+ const [planFilePath, setPlanFilePath] = useState();
22
+ const [allToolsExpanded, setAllToolsExpanded] = useState(true);
23
+ const addMessage = useCallback((message) => {
24
+ // Tools that support expand/collapse
25
+ const expandableTools = ["Bash", "BashOutput", "Command Output"];
26
+ const isExpandableTool = message.type === "tool" && expandableTools.includes(message.toolName || "");
27
+ const newMessage = {
28
+ ...message,
29
+ id: `${Date.now()}-${Math.random()}`,
30
+ timestamp: Date.now(),
31
+ // Set isExpanded based on current allToolsExpanded state for expandable tools
32
+ isExpanded: isExpandableTool ? allToolsExpanded : message.isExpanded,
33
+ };
34
+ setMessages((prev) => [...prev, newMessage]);
35
+ }, [allToolsExpanded]);
36
+ const updateLastMessage = useCallback((updates) => {
37
+ setMessages((prev) => {
38
+ if (prev.length === 0)
39
+ return prev;
40
+ const last = prev[prev.length - 1];
41
+ return [...prev.slice(0, -1), { ...last, ...updates }];
42
+ });
43
+ }, []);
44
+ const updateMessageById = useCallback((id, updates) => {
45
+ setMessages((prev) => {
46
+ const index = prev.findIndex((msg) => msg.id === id);
47
+ if (index === -1)
48
+ return prev;
49
+ const updated = [...prev];
50
+ updated[index] = { ...updated[index], ...updates };
51
+ return updated;
52
+ });
53
+ }, []);
54
+ const updateMessageByToolId = useCallback((toolId, updates) => {
55
+ setMessages((prev) => {
56
+ const index = prev.findIndex((msg) => msg.toolUseId === toolId);
57
+ if (index === -1)
58
+ return prev;
59
+ const updated = [...prev];
60
+ updated[index] = { ...updated[index], ...updates };
61
+ return updated;
62
+ });
63
+ }, []);
64
+ const clearMessages = useCallback(() => {
65
+ setMessages([]);
66
+ setTodos([]);
67
+ setStats({
68
+ filesModified: new Set(),
69
+ commandsRun: [],
70
+ iterations: 0,
71
+ startTime: Date.now(),
72
+ });
73
+ // Clear the terminal screen (similar to Gemini CLI behavior)
74
+ console.clear();
75
+ }, []);
76
+ const loadMessages = useCallback((newMessages) => {
77
+ setMessages(newMessages);
78
+ }, []);
79
+ const updateStats = useCallback((updates) => {
80
+ setStats((prev) => ({ ...prev, ...updates }));
81
+ }, []);
82
+ const toggleAllToolOutputs = useCallback(() => {
83
+ setAllToolsExpanded((prev) => {
84
+ const newValue = !prev;
85
+ // Update only Bash tool messages to match the new expanded state
86
+ // Other tools (Grep, Glob, Read, etc.) don't have expandable output
87
+ const expandableTools = ["Bash", "BashOutput", "Command Output"];
88
+ setMessages((msgs) => msgs.map((msg) => msg.type === "tool" && msg.toolResult && expandableTools.includes(msg.toolName || "")
89
+ ? { ...msg, isExpanded: newValue }
90
+ : msg));
91
+ return newValue;
92
+ });
93
+ }, []);
94
+ const value = {
95
+ messages,
96
+ addMessage,
97
+ updateLastMessage,
98
+ updateMessageById,
99
+ updateMessageByToolId,
100
+ clearMessages,
101
+ loadMessages,
102
+ toggleAllToolOutputs,
103
+ allToolsExpanded,
104
+ todos,
105
+ setTodos,
106
+ stats,
107
+ updateStats,
108
+ isAgentRunning,
109
+ setIsAgentRunning,
110
+ shouldInterruptAgent,
111
+ setShouldInterruptAgent,
112
+ sessionId,
113
+ setSessionId,
114
+ webUrl,
115
+ setWebUrl,
116
+ agentMode,
117
+ setAgentMode,
118
+ planFilePath,
119
+ setPlanFilePath,
120
+ };
121
+ return (React.createElement(SessionContext.Provider, { value: value }, children));
122
+ };
123
+ export const useSession = () => {
124
+ const context = useContext(SessionContext);
125
+ if (!context) {
126
+ throw new Error("useSession must be used within SessionProvider");
127
+ }
128
+ return context;
129
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Animated scrollbar hook
3
+ * Provides a thin, animated scrollbar that fades in/out on interaction
4
+ */
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ export const useAnimatedScrollbar = (isFocused, scrollBy, options) => {
7
+ const [scrollbarColor, setScrollbarColor] = useState(options.unfocusedColor);
8
+ const colorRef = useRef(scrollbarColor);
9
+ colorRef.current = scrollbarColor;
10
+ const animationFrame = useRef(null);
11
+ const timeout = useRef(null);
12
+ const isAnimatingRef = useRef(false);
13
+ const cleanup = useCallback(() => {
14
+ if (animationFrame.current) {
15
+ clearInterval(animationFrame.current);
16
+ animationFrame.current = null;
17
+ }
18
+ if (timeout.current) {
19
+ clearTimeout(timeout.current);
20
+ timeout.current = null;
21
+ }
22
+ isAnimatingRef.current = false;
23
+ }, []);
24
+ const interpolateColor = useCallback((color1, color2, progress) => {
25
+ // Simple color interpolation
26
+ return progress < 0.5 ? color1 : color2;
27
+ }, []);
28
+ const flashScrollbar = useCallback(() => {
29
+ cleanup();
30
+ isAnimatingRef.current = true;
31
+ const fadeInDuration = 200;
32
+ const visibleDuration = 1000;
33
+ const fadeOutDuration = 300;
34
+ const focusedColor = options.focusedColor;
35
+ const unfocusedColor = options.unfocusedColor;
36
+ const startColor = colorRef.current;
37
+ // Phase 1: Fade In
38
+ let start = Date.now();
39
+ const animateFadeIn = () => {
40
+ const elapsed = Date.now() - start;
41
+ const progress = Math.max(0, Math.min(elapsed / fadeInDuration, 1));
42
+ setScrollbarColor(interpolateColor(startColor, focusedColor, progress));
43
+ if (progress === 1) {
44
+ if (animationFrame.current) {
45
+ clearInterval(animationFrame.current);
46
+ animationFrame.current = null;
47
+ }
48
+ // Phase 2: Wait
49
+ timeout.current = setTimeout(() => {
50
+ // Phase 3: Fade Out
51
+ start = Date.now();
52
+ const animateFadeOut = () => {
53
+ const elapsed = Date.now() - start;
54
+ const progress = Math.max(0, Math.min(elapsed / fadeOutDuration, 1));
55
+ setScrollbarColor(interpolateColor(focusedColor, unfocusedColor, progress));
56
+ if (progress === 1) {
57
+ cleanup();
58
+ }
59
+ };
60
+ animationFrame.current = setInterval(animateFadeOut, 33);
61
+ }, visibleDuration);
62
+ }
63
+ };
64
+ animationFrame.current = setInterval(animateFadeIn, 33);
65
+ }, [cleanup, interpolateColor, options.focusedColor, options.unfocusedColor]);
66
+ const wasFocused = useRef(isFocused);
67
+ useEffect(() => {
68
+ if (isFocused && !wasFocused.current) {
69
+ flashScrollbar();
70
+ }
71
+ else if (!isFocused && wasFocused.current) {
72
+ cleanup();
73
+ setScrollbarColor(options.unfocusedColor);
74
+ }
75
+ wasFocused.current = isFocused;
76
+ return cleanup;
77
+ }, [isFocused, flashScrollbar, cleanup, options.unfocusedColor]);
78
+ const scrollByWithAnimation = useCallback((delta) => {
79
+ scrollBy(delta);
80
+ flashScrollbar();
81
+ }, [scrollBy, flashScrollbar]);
82
+ return { scrollbarColor, flashScrollbar, scrollByWithAnimation };
83
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Batched scroll hook
3
+ * Batches scroll updates to prevent jittery behavior
4
+ */
5
+ import { useCallback, useRef } from "react";
6
+ export const useBatchedScroll = (scrollTop) => {
7
+ const pendingScrollTopRef = useRef(null);
8
+ const currentScrollTopRef = useRef(scrollTop);
9
+ // Update current scroll top whenever it changes
10
+ currentScrollTopRef.current = scrollTop;
11
+ const getScrollTop = useCallback(() => {
12
+ return pendingScrollTopRef.current ?? currentScrollTopRef.current;
13
+ }, []);
14
+ const setPendingScrollTop = useCallback((value) => {
15
+ pendingScrollTopRef.current = value;
16
+ // Clear pending after a microtask
17
+ setTimeout(() => {
18
+ pendingScrollTopRef.current = null;
19
+ }, 0);
20
+ }, []);
21
+ return { getScrollTop, setPendingScrollTop };
22
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { useEffect } from 'react';
7
+ import { writeToStdout } from '../../utils/stdio';
8
+ const ENABLE_BRACKETED_PASTE = '\x1b[?2004h';
9
+ const DISABLE_BRACKETED_PASTE = '\x1b[?2004l';
10
+ /**
11
+ * Enables and disables bracketed paste mode in the terminal.
12
+ *
13
+ * This hook ensures that bracketed paste mode is enabled when the component
14
+ * mounts and disabled when it unmounts or when the process exits.
15
+ *
16
+ * Bracketed paste mode wraps pasted text in special escape sequences
17
+ * ([200~ and [201~) so we can distinguish between typed and pasted text.
18
+ */
19
+ export const useBracketedPaste = () => {
20
+ useEffect(() => {
21
+ writeToStdout(ENABLE_BRACKETED_PASTE);
22
+ const cleanup = () => {
23
+ writeToStdout(DISABLE_BRACKETED_PASTE);
24
+ };
25
+ process.on('exit', cleanup);
26
+ return () => {
27
+ cleanup();
28
+ process.off('exit', cleanup);
29
+ };
30
+ }, []);
31
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { useStdin, useStdout } from 'ink';
7
+ import { useEffect, useState } from 'react';
8
+ import { useKeypress } from './useKeypress.js';
9
+ // ANSI escape codes to enable/disable terminal focus reporting
10
+ export const ENABLE_FOCUS_REPORTING = '\x1b[?1004h';
11
+ export const DISABLE_FOCUS_REPORTING = '\x1b[?1004l';
12
+ // ANSI escape codes for focus events
13
+ export const FOCUS_IN = '\x1b[I';
14
+ export const FOCUS_OUT = '\x1b[O';
15
+ export const useFocus = () => {
16
+ const { stdin } = useStdin();
17
+ const { stdout } = useStdout();
18
+ const [isFocused, setIsFocused] = useState(true);
19
+ useEffect(() => {
20
+ const handleData = (data) => {
21
+ const sequence = data.toString();
22
+ const lastFocusIn = sequence.lastIndexOf(FOCUS_IN);
23
+ const lastFocusOut = sequence.lastIndexOf(FOCUS_OUT);
24
+ if (lastFocusIn > lastFocusOut) {
25
+ setIsFocused(true);
26
+ }
27
+ else if (lastFocusOut > lastFocusIn) {
28
+ setIsFocused(false);
29
+ }
30
+ };
31
+ // Enable focus reporting
32
+ stdout?.write(ENABLE_FOCUS_REPORTING);
33
+ stdin?.on('data', handleData);
34
+ return () => {
35
+ // Disable focus reporting on cleanup
36
+ stdout?.write(DISABLE_FOCUS_REPORTING);
37
+ stdin?.removeListener('data', handleData);
38
+ };
39
+ }, [stdin, stdout]);
40
+ useKeypress((_) => {
41
+ if (!isFocused) {
42
+ // If the user has typed a key, and we cannot possibly be focused out.
43
+ // This is a workaround for some tmux use cases. It is still useful to
44
+ // listen for the true FOCUS_IN event as well as that will update the
45
+ // focus state earlier than waiting for a keypress.
46
+ setIsFocused(true);
47
+ }
48
+ }, { isActive: true });
49
+ return isFocused;
50
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { useEffect } from 'react';
7
+ import { useKeypressContext } from '../contexts/KeypressContext';
8
+ /**
9
+ * A hook that listens for keypress events from stdin.
10
+ *
11
+ * @param onKeypress - The callback function to execute on each keypress.
12
+ * @param options - Options to control the hook's behavior.
13
+ * @param options.isActive - Whether the hook should be actively listening for input.
14
+ */
15
+ export function useKeypress(onKeypress, { isActive }) {
16
+ const { subscribe, unsubscribe } = useKeypressContext();
17
+ useEffect(() => {
18
+ if (!isActive) {
19
+ return;
20
+ }
21
+ subscribe(onKeypress);
22
+ return () => {
23
+ unsubscribe(onKeypress);
24
+ };
25
+ }, [isActive, onKeypress, subscribe, unsubscribe]);
26
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Hook for toggling between plan and build modes
3
+ * Uses Shift+Tab keyboard shortcut (like Claude Code)
4
+ */
5
+ import { useEffect } from "react";
6
+ import { useKeypressContext } from "../contexts/KeypressContext.js";
7
+ import { useSession } from "../contexts/SessionContext.js";
8
+ export function useModeToggle() {
9
+ const { subscribe, unsubscribe } = useKeypressContext();
10
+ const { agentMode, setAgentMode, isAgentRunning } = useSession();
11
+ useEffect(() => {
12
+ const handleKeypress = (key) => {
13
+ // Shift+Tab to toggle mode (like Claude Code)
14
+ // Only allow toggling when agent is not running
15
+ if (key.name === "tab" && key.shift && !isAgentRunning) {
16
+ const newMode = agentMode === "plan" ? "build" : "plan";
17
+ setAgentMode(newMode);
18
+ // Mode indicator is shown below the input prompt (no message needed)
19
+ }
20
+ };
21
+ subscribe(handleKeypress);
22
+ return () => unsubscribe(handleKeypress);
23
+ }, [agentMode, isAgentRunning, setAgentMode, subscribe, unsubscribe]);
24
+ return { agentMode };
25
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Auth types for Supatest CLI
3
+ *
4
+ * Based on Gemini CLI (Apache 2.0 License)
5
+ * https://github.com/google-gemini/gemini-cli
6
+ * Copyright 2025 Google LLC
7
+ */
8
+ export var AuthState;
9
+ (function (AuthState) {
10
+ AuthState["Unauthenticated"] = "unauthenticated";
11
+ AuthState["Authenticating"] = "authenticating";
12
+ AuthState["Authenticated"] = "authenticated";
13
+ })(AuthState || (AuthState = {}));
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const IGNORED_DIRS = new Set([
4
+ 'node_modules',
5
+ '.git',
6
+ '.turbo',
7
+ 'dist',
8
+ 'build',
9
+ 'coverage',
10
+ '.next',
11
+ '.cache'
12
+ ]);
13
+ const IGNORED_EXTENSIONS = new Set([
14
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg',
15
+ '.woff', '.woff2', '.ttf', '.eot',
16
+ '.mp4', '.webm', '.mp3', '.wav',
17
+ '.zip', '.tar', '.gz', '.7z', '.rar',
18
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
19
+ '.exe', '.dll', '.so', '.dylib', '.bin',
20
+ '.map', '.lock', '.tsbuildinfo'
21
+ ]);
22
+ function shouldIgnore(name, isDir) {
23
+ if (name.startsWith('.'))
24
+ return true; // Ignore dotfiles by default for now
25
+ if (isDir)
26
+ return IGNORED_DIRS.has(name);
27
+ const ext = path.extname(name).toLowerCase();
28
+ return IGNORED_EXTENSIONS.has(ext);
29
+ }
30
+ export function getFiles(rootDir = process.cwd(), maxDepth = 3) {
31
+ const files = [];
32
+ function scan(dir, depth) {
33
+ if (depth > maxDepth)
34
+ return;
35
+ try {
36
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ if (shouldIgnore(entry.name, entry.isDirectory()))
39
+ continue;
40
+ const fullPath = path.join(dir, entry.name);
41
+ const relativePath = path.relative(rootDir, fullPath);
42
+ if (entry.isDirectory()) {
43
+ scan(fullPath, depth + 1);
44
+ }
45
+ else {
46
+ files.push(relativePath);
47
+ }
48
+ }
49
+ }
50
+ catch (error) {
51
+ // Ignore access errors
52
+ }
53
+ }
54
+ scan(rootDir, 0);
55
+ return files;
56
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export const ESC = '\u001B';
7
+ export const SGR_EVENT_PREFIX = `${ESC}[<`;
8
+ export const X11_EVENT_PREFIX = `${ESC}[M`;
9
+ // eslint-disable-next-line no-control-regex
10
+ export const SGR_MOUSE_REGEX = /^\x1b\[<(\d+);(\d+);(\d+)([mM])/; // SGR mouse events
11
+ // X11 is ESC [ M followed by 3 bytes.
12
+ // eslint-disable-next-line no-control-regex
13
+ export const X11_MOUSE_REGEX = /^\x1b\[M([\s\S]{3})/;
14
+ export function couldBeSGRMouseSequence(buffer) {
15
+ if (buffer.length === 0)
16
+ return true;
17
+ // Check if buffer is a prefix of a mouse sequence starter
18
+ if (SGR_EVENT_PREFIX.startsWith(buffer))
19
+ return true;
20
+ // Check if buffer is a mouse sequence prefix
21
+ if (buffer.startsWith(SGR_EVENT_PREFIX))
22
+ return true;
23
+ return false;
24
+ }
25
+ export function couldBeMouseSequence(buffer) {
26
+ if (buffer.length === 0)
27
+ return true;
28
+ // Check SGR prefix
29
+ if (SGR_EVENT_PREFIX.startsWith(buffer) ||
30
+ buffer.startsWith(SGR_EVENT_PREFIX))
31
+ return true;
32
+ // Check X11 prefix
33
+ if (X11_EVENT_PREFIX.startsWith(buffer) ||
34
+ buffer.startsWith(X11_EVENT_PREFIX))
35
+ return true;
36
+ return false;
37
+ }
38
+ /**
39
+ * Checks if the buffer *starts* with a complete mouse sequence.
40
+ * Returns the length of the sequence if matched, or 0 if not.
41
+ */
42
+ export function getMouseSequenceLength(buffer) {
43
+ const sgrMatch = buffer.match(SGR_MOUSE_REGEX);
44
+ if (sgrMatch)
45
+ return sgrMatch[0].length;
46
+ const x11Match = buffer.match(X11_MOUSE_REGEX);
47
+ if (x11Match)
48
+ return x11Match[0].length;
49
+ return 0;
50
+ }