@bugbug-io/cli 13.39.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.
@@ -0,0 +1,2410 @@
1
+ import {
2
+ BaseSuiteRunDetails,
3
+ BaseTestRunDetails,
4
+ DEFAULT_RESERVED_TERMINAL_ROWS,
5
+ EscProvider,
6
+ MenuList,
7
+ ROUTES,
8
+ SelectableOption,
9
+ SelectionPrefix,
10
+ ShortcutProvider,
11
+ Status,
12
+ ViewLayout,
13
+ VirtualizedList,
14
+ analytics,
15
+ defaultJunitReportPath,
16
+ detectFormat,
17
+ exportProject,
18
+ exportTest,
19
+ formatCwd,
20
+ formatDuration,
21
+ formatErrorMessage,
22
+ formatRunStatus,
23
+ getCliConfig,
24
+ getIdentity,
25
+ getProjectById,
26
+ getSuite,
27
+ getSuiteDetails,
28
+ getSuiteRunDetails,
29
+ getSuiteRunTestRuns,
30
+ getTest,
31
+ getTestDetails,
32
+ getTestRunDetails,
33
+ getTestRunJunitReport,
34
+ getVersion,
35
+ hasProjectConfigFile,
36
+ importProject,
37
+ importTest,
38
+ initProject,
39
+ isAuthenticationError,
40
+ isAuthorizationError,
41
+ listProfiles,
42
+ listProjects,
43
+ listSuiteRuns,
44
+ listTestRunDebugArtifacts,
45
+ listTestRuns,
46
+ loadSuiteTableResources,
47
+ loadTestTableResources,
48
+ login,
49
+ logout,
50
+ openExternalUrl,
51
+ runSuite,
52
+ runTest,
53
+ switchProject,
54
+ updateSdkConfig,
55
+ useEscHandler,
56
+ useShortcutBlocker,
57
+ useShortcutHandler,
58
+ useTheme,
59
+ userCwd,
60
+ writeReportXml
61
+ } from "./chunk-NTNHB6R6.js";
62
+
63
+ // src/app/NavigatorApp.tsx
64
+ import { useState as useState17 } from "react";
65
+ import { RouterProvider } from "react-router";
66
+
67
+ // src/app/contexts/navigatorSession/NavigatorSessionContext.tsx
68
+ import { createContext, useCallback, useContext, useState } from "react";
69
+ import { jsx } from "react/jsx-runtime";
70
+ var applyAuthState = (config, authState) => ({
71
+ ...config,
72
+ ...authState.token !== void 0 ? { token: authState.token } : {},
73
+ ...authState.projectId !== void 0 ? { projectId: authState.projectId } : {}
74
+ });
75
+ var getAuthStatus = (config, authState) => ({
76
+ kind: config.token && config.projectId ? "ready" : "prompt",
77
+ token: config.token,
78
+ projectId: config.projectId,
79
+ isUserToken: authState.isUserToken === true,
80
+ apiUrl: config.apiUrl
81
+ });
82
+ var syncInitialSdkConfig = () => {
83
+ const config = getCliConfig();
84
+ if (config.token) {
85
+ updateSdkConfig({
86
+ apiToken: config.token,
87
+ ...config.projectId ? { projectId: config.projectId } : {}
88
+ });
89
+ }
90
+ return {};
91
+ };
92
+ var NavigatorSessionContext = createContext(null);
93
+ var NavigatorSessionProvider = ({ children }) => {
94
+ const [authState, setAuthState] = useState(
95
+ () => syncInitialSdkConfig()
96
+ );
97
+ const [authError, setAuthError] = useState(null);
98
+ const config = applyAuthState(getCliConfig(), authState);
99
+ const auth = getAuthStatus(config, authState);
100
+ const completeSession = useCallback(
101
+ (token, projectId, options = {}) => {
102
+ try {
103
+ const trimmedToken = token.trim();
104
+ if (!trimmedToken) throw new Error("Token must not be empty.");
105
+ const trimmedProjectId = projectId.trim();
106
+ if (!trimmedProjectId) throw new Error("Project id must not be empty.");
107
+ const nextAuthState = {
108
+ token: trimmedToken,
109
+ projectId: trimmedProjectId,
110
+ isUserToken: options.isUserToken === true
111
+ };
112
+ updateSdkConfig({ apiToken: nextAuthState.token, projectId: nextAuthState.projectId });
113
+ analytics.refreshWithToken(trimmedToken);
114
+ analytics.trackEvent("cli_session_configured", { success: true });
115
+ if (config.projectId !== trimmedProjectId) {
116
+ analytics.trackEvent("cli_session_project_switched", { success: true });
117
+ }
118
+ analytics.trackEvent("cli_session_started");
119
+ setAuthState(nextAuthState);
120
+ setAuthError(null);
121
+ } catch (err) {
122
+ analytics.trackEvent("cli_session_configured", { success: false });
123
+ setAuthError(err instanceof Error ? err.message : String(err));
124
+ }
125
+ },
126
+ [config.projectId]
127
+ );
128
+ const clearSession = useCallback(() => {
129
+ setAuthState({});
130
+ setAuthError(null);
131
+ }, []);
132
+ const setSessionProject = useCallback((projectId) => {
133
+ try {
134
+ const trimmedProjectId = projectId.trim();
135
+ if (!trimmedProjectId) throw new Error("Project id must not be empty.");
136
+ updateSdkConfig({ projectId: trimmedProjectId });
137
+ setAuthState((current) => ({ ...current, projectId: trimmedProjectId }));
138
+ setAuthError(null);
139
+ analytics.trackEvent("cli_session_project_switched", { success: true });
140
+ } catch (err) {
141
+ analytics.trackEvent("cli_session_project_switched", { success: false });
142
+ setAuthError(err instanceof Error ? err.message : String(err));
143
+ }
144
+ }, []);
145
+ return /* @__PURE__ */ jsx(
146
+ NavigatorSessionContext.Provider,
147
+ {
148
+ value: { auth, config, authError, completeSession, clearSession, setSessionProject },
149
+ children
150
+ }
151
+ );
152
+ };
153
+ var useNavigatorSession = () => {
154
+ const ctx = useContext(NavigatorSessionContext);
155
+ if (!ctx) {
156
+ throw new Error("useNavigatorSession must be used within a NavigatorSessionProvider.");
157
+ }
158
+ return ctx;
159
+ };
160
+
161
+ // src/app/router.tsx
162
+ import { createMemoryRouter, createRoutesFromElements, Route } from "react-router";
163
+
164
+ // src/app/Navigator.tsx
165
+ import { Box as Box4, useApp, useInput } from "ink";
166
+ import { useState as useState4 } from "react";
167
+ import { Outlet, useLocation as useLocation3, useNavigate as useNavigate2 } from "react-router";
168
+
169
+ // src/app/components/AuthGate/AuthGate.tsx
170
+ import { useEffect as useEffect2 } from "react";
171
+ import { useLocation, useNavigate } from "react-router";
172
+
173
+ // src/app/hooks/useConnectionStatus.ts
174
+ import { useEffect, useState as useState2 } from "react";
175
+ var PING_INTERVAL_MS = 15e3;
176
+ var getConnectionStatus = async () => {
177
+ try {
178
+ const identity = await getIdentity();
179
+ return { state: "ok", message: "API connectivity successful", identity };
180
+ } catch (err) {
181
+ if (isAuthenticationError(err) || isAuthorizationError(err)) {
182
+ return { state: "unauthenticated", message: "API token is invalid or missing" };
183
+ }
184
+ return {
185
+ state: "error",
186
+ message: err instanceof Error ? err.message : "Unknown error"
187
+ };
188
+ }
189
+ };
190
+ var useConnectionStatus = (enabled) => {
191
+ const [status, setStatus] = useState2({ state: "checking" });
192
+ useEffect(() => {
193
+ if (!enabled) {
194
+ return void 0;
195
+ }
196
+ let cancelled = false;
197
+ const ping = async () => {
198
+ const nextStatus = await getConnectionStatus();
199
+ if (!cancelled) setStatus(nextStatus);
200
+ };
201
+ ping();
202
+ const interval = setInterval(() => {
203
+ ping();
204
+ }, PING_INTERVAL_MS);
205
+ return () => {
206
+ cancelled = true;
207
+ clearInterval(interval);
208
+ };
209
+ }, [enabled]);
210
+ return status;
211
+ };
212
+
213
+ // src/app/components/AuthGate/AuthGate.tsx
214
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
215
+ var INIT_ROUTE = "/init";
216
+ var AuthGate = ({ children }) => {
217
+ const { auth } = useNavigatorSession();
218
+ const location = useLocation();
219
+ const navigate = useNavigate();
220
+ const onInitRoute = location.pathname === INIT_ROUTE;
221
+ const hasConfiguredAuth = auth.kind === "ready";
222
+ const connectionStatus = useConnectionStatus(hasConfiguredAuth);
223
+ const authCheckPending = hasConfiguredAuth && connectionStatus.state === "checking";
224
+ const authRejected = hasConfiguredAuth && connectionStatus.state === "unauthenticated";
225
+ const authenticated = hasConfiguredAuth && !authRejected;
226
+ useEffect2(() => {
227
+ if (!authCheckPending && !authenticated && !onInitRoute) {
228
+ navigate(INIT_ROUTE, {
229
+ state: {
230
+ redirectUrl: location.pathname,
231
+ onlyThisSession: true,
232
+ ...authRejected ? { ignoreConfiguredAuth: true } : {}
233
+ },
234
+ replace: true
235
+ });
236
+ }
237
+ }, [authCheckPending, authenticated, authRejected, onInitRoute, location.pathname, navigate]);
238
+ if (authCheckPending) return null;
239
+ if (authenticated) return /* @__PURE__ */ jsx2(Fragment, { children: children(true) });
240
+ if (onInitRoute) return /* @__PURE__ */ jsx2(Fragment, { children: children(false) });
241
+ return null;
242
+ };
243
+
244
+ // src/app/components/Header/Header.tsx
245
+ import { Box, Text } from "ink";
246
+
247
+ // src/app/hooks/useFeatureQuery.ts
248
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
249
+ var useFeatureQuery = (fn, deps) => {
250
+ const [data, setData] = useState3(void 0);
251
+ const [isFetching, setIsFetching] = useState3(true);
252
+ const [error, setError] = useState3(void 0);
253
+ const [refreshIndex, setRefreshIndex] = useState3(0);
254
+ useEffect3(() => {
255
+ let cancelled = false;
256
+ setIsFetching(true);
257
+ setError(void 0);
258
+ const run = async () => {
259
+ try {
260
+ const result = await fn();
261
+ if (!cancelled) setData(result);
262
+ } catch (err) {
263
+ if (!cancelled) setError(formatErrorMessage(err));
264
+ } finally {
265
+ if (!cancelled) setIsFetching(false);
266
+ }
267
+ };
268
+ run().catch((err) => {
269
+ if (!cancelled) {
270
+ setError(formatErrorMessage(err));
271
+ setIsFetching(false);
272
+ }
273
+ });
274
+ return () => {
275
+ cancelled = true;
276
+ };
277
+ }, [refreshIndex, ...deps]);
278
+ const refetch = useCallback2(() => {
279
+ setRefreshIndex((index) => index + 1);
280
+ }, []);
281
+ return {
282
+ data,
283
+ isLoading: isFetching && data === void 0,
284
+ isFetching,
285
+ isError: error !== void 0,
286
+ error,
287
+ refetch
288
+ };
289
+ };
290
+
291
+ // src/app/hooks/useCurrentProject.ts
292
+ var useCurrentProject = (enabled) => {
293
+ const { auth } = useNavigatorSession();
294
+ const projectId = enabled ? auth.projectId : void 0;
295
+ const query = useFeatureQuery(
296
+ () => projectId ? getProjectById(projectId) : Promise.resolve(null),
297
+ [projectId]
298
+ );
299
+ return query.data ?? void 0;
300
+ };
301
+
302
+ // src/app/components/Header/Header.constants.ts
303
+ var VERSION = getVersion();
304
+ var BUGBUG_LOGO_ASCII = [
305
+ "\u2584 \u2584 ",
306
+ "\u2588\u2588\u2588 \u2588\u2588\u2588 ",
307
+ "\u2580\u2580\u2580 \u2580\u2580\u2580 ",
308
+ "\u2584\u2584 \u2584\u2584 ",
309
+ " \u2580\u2580\u2580\u2580 "
310
+ ].join("\n");
311
+ var STATUS_LABEL = {
312
+ checking: "Connecting...",
313
+ ok: "Authenticated",
314
+ unauthenticated: "Unauthenticated",
315
+ error: "Disconnected"
316
+ };
317
+ var STATUS_COLOR = {
318
+ checking: "yellow",
319
+ ok: void 0,
320
+ unauthenticated: "yellow",
321
+ error: "red"
322
+ };
323
+
324
+ // src/app/components/Header/Header.tsx
325
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
326
+ var Header = ({ authenticated }) => {
327
+ const { theme } = useTheme();
328
+ const status = useConnectionStatus(authenticated);
329
+ const project = useCurrentProject(authenticated);
330
+ const identity = authenticated ? status.identity : void 0;
331
+ const state = authenticated ? status.state : "unauthenticated";
332
+ const showDetail = authenticated && (status.state === "error" || status.state === "unauthenticated") && !!status.message;
333
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
334
+ /* @__PURE__ */ jsx3(Box, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text, { color: theme.colors.brand, children: BUGBUG_LOGO_ASCII }) }),
335
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
336
+ /* @__PURE__ */ jsxs(Text, { children: [
337
+ /* @__PURE__ */ jsx3(Text, { bold: true, children: "BugBug" }),
338
+ " ",
339
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
340
+ "v",
341
+ VERSION
342
+ ] })
343
+ ] }),
344
+ /* @__PURE__ */ jsx3(Text, { dimColor: true, children: "Your interactive quality buddy" }),
345
+ /* @__PURE__ */ jsxs(Text, { children: [
346
+ /* @__PURE__ */ jsx3(Text, { dimColor: true, children: "Status: " }),
347
+ /* @__PURE__ */ jsx3(Text, { color: STATUS_COLOR[state], dimColor: state === "ok", children: STATUS_LABEL[state] }),
348
+ authenticated && identity && /* @__PURE__ */ jsx3(Text, { dimColor: true, children: identity.tokenType === "user" ? ` (logged in as ${identity.name})` : ` (via ${identity.tokenType} token)` }),
349
+ showDetail ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
350
+ " (",
351
+ status.message,
352
+ ")"
353
+ ] }) : null
354
+ ] }),
355
+ authenticated && project ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
356
+ "Project: ",
357
+ project.name ?? "Untitled project",
358
+ " at ",
359
+ project.organizationName
360
+ ] }) : /* @__PURE__ */ jsx3(Text, { dimColor: true, children: " " }),
361
+ /* @__PURE__ */ jsx3(Text, { dimColor: true, children: formatCwd(userCwd()) })
362
+ ] })
363
+ ] });
364
+ };
365
+
366
+ // src/features/tui/components/Breadcrumb/Breadcrumb.tsx
367
+ import { Box as Box2, Text as Text2 } from "ink";
368
+ import { useLocation as useLocation2 } from "react-router";
369
+
370
+ // src/app/routeLabels.ts
371
+ var CRUMB_LABELS = {
372
+ "/tests": "Tests",
373
+ "/tests/list": "List",
374
+ "/tests/import": "Import",
375
+ "/tests/run": "Run",
376
+ "/tests/:testId": "Details",
377
+ "/tests/:testId/run": "Run",
378
+ "/tests/:testId/lastRun": "Show last run",
379
+ "/tests/:testId/export": "Export",
380
+ "/tests/:testId/open": "Open in BugBug",
381
+ "/testruns": "Test runs",
382
+ "/testruns/list": "List",
383
+ "/testruns/:runId": "Details",
384
+ "/testruns/:runId/debugArtifacts": "Show run artifacts",
385
+ "/testruns/:runId/junitReport": "Download JUnit report",
386
+ "/testruns/:runId/open": "Open in BugBug",
387
+ "/suites": "Suites",
388
+ "/suites/list": "List",
389
+ "/suites/run": "Run",
390
+ "/suites/:suiteId": "Details",
391
+ "/suites/:suiteId/run": "Run",
392
+ "/suites/:suiteId/lastRun": "Show last run",
393
+ "/suites/:suiteId/open": "Open in BugBug",
394
+ "/suiteruns": "Suite runs",
395
+ "/suiteruns/list": "List",
396
+ "/suiteruns/:runId": "Details",
397
+ "/suiteruns/:runId/showTestRuns": "Show test runs",
398
+ "/suiteruns/:runId/open": "Open in BugBug",
399
+ "/profiles": "Profiles",
400
+ "/profiles/list": "List",
401
+ "/project": "Project",
402
+ "/project/export": "Export",
403
+ "/project/import": "Import",
404
+ "/settings": "Settings",
405
+ "/settings/setSessionToken": "Set session token",
406
+ "/logout": "Logout"
407
+ };
408
+
409
+ // src/features/tui/tui.utils.ts
410
+ var ROOT_LABEL = "Main menu";
411
+ var normalizePath = (pathname) => {
412
+ const trimmed = pathname.replace(/\/+$/, "");
413
+ return trimmed === "" ? "/" : trimmed;
414
+ };
415
+ var pathSegments = (pathname) => normalizePath(pathname).split("/").filter(Boolean);
416
+ var matchesPattern = (pattern, pathname) => {
417
+ const patternSegments = pathSegments(pattern);
418
+ const currentSegments = pathSegments(pathname);
419
+ if (patternSegments.length !== currentSegments.length) return false;
420
+ return patternSegments.every(
421
+ (segment, index) => segment.startsWith(":") || segment === currentSegments[index]
422
+ );
423
+ };
424
+ var findLabel = (pathname, labels) => {
425
+ const normalizedPath = normalizePath(pathname);
426
+ const exact = labels[normalizedPath];
427
+ if (exact) return exact;
428
+ const wildcard = Object.entries(labels).find(
429
+ ([pattern]) => matchesPattern(pattern, normalizedPath)
430
+ );
431
+ return wildcard?.[1];
432
+ };
433
+ var buildBreadcrumb = (pathname, labels) => {
434
+ const parts = [ROOT_LABEL];
435
+ const segments = pathSegments(pathname);
436
+ for (let index = 0; index < segments.length; index += 1) {
437
+ const prefix = `/${segments.slice(0, index + 1).join("/")}`;
438
+ const label = findLabel(prefix, labels);
439
+ if (label && parts[parts.length - 1] !== label) parts.push(label);
440
+ }
441
+ return parts.join(" \u203A ");
442
+ };
443
+
444
+ // src/features/tui/components/Breadcrumb/Breadcrumb.tsx
445
+ import { jsx as jsx4 } from "react/jsx-runtime";
446
+ var Breadcrumb = () => {
447
+ const { pathname } = useLocation2();
448
+ return /* @__PURE__ */ jsx4(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text2, { bold: true, color: "cyan", children: buildBreadcrumb(pathname, CRUMB_LABELS) }) });
449
+ };
450
+
451
+ // src/features/tui/components/ShortcutsGrid/ShortcutsGrid.tsx
452
+ import { Box as Box3, Text as Text3 } from "ink";
453
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
454
+ var GLOBAL_SHORTCUTS = [
455
+ { shortcut: "t", label: "Tests" },
456
+ { shortcut: "r", label: "Test runs" },
457
+ { shortcut: "s", label: "Suites" },
458
+ { shortcut: "u", label: "Suite runs" },
459
+ { shortcut: "f", label: "Profiles" },
460
+ { shortcut: "p", label: "Project" },
461
+ { shortcut: ",", label: "Settings" },
462
+ { shortcut: "Esc", label: "Back" },
463
+ { shortcut: "q", label: "Close at main menu" },
464
+ { shortcut: "Ctrl-C", label: "Exit" }
465
+ ];
466
+ var ACTION_SHORTCUTS = [
467
+ { shortcut: "Enter", label: "Select" },
468
+ { shortcut: "/", label: "Search" },
469
+ { shortcut: "l", label: "List or logs" },
470
+ { shortcut: "r", label: "Run" },
471
+ { shortcut: "i", label: "Import" },
472
+ { shortcut: "e", label: "Export" },
473
+ { shortcut: "o", label: "Open in BugBug" },
474
+ { shortcut: "h", label: "Last run" },
475
+ { shortcut: "a", label: "Artifacts" },
476
+ { shortcut: "t", label: "Suite run tests" }
477
+ ];
478
+ var ShortcutColumn = ({ title, items }) => /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginRight: 4, children: [
479
+ /* @__PURE__ */ jsx5(Text3, { bold: true, children: title }),
480
+ items.map(({ shortcut, label }) => /* @__PURE__ */ jsxs2(Text3, { children: [
481
+ /* @__PURE__ */ jsx5(Text3, { color: "cyan", children: shortcut.padEnd(7) }),
482
+ label
483
+ ] }, `${shortcut}-${label}`))
484
+ ] });
485
+ var ShortcutsGrid = () => /* @__PURE__ */ jsxs2(Box3, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
486
+ /* @__PURE__ */ jsx5(ShortcutColumn, { title: "Global", items: GLOBAL_SHORTCUTS }),
487
+ /* @__PURE__ */ jsx5(ShortcutColumn, { title: "Actions", items: ACTION_SHORTCUTS })
488
+ ] });
489
+
490
+ // src/features/tui/ShortcutController.constants.ts
491
+ var GLOBAL_SHORTCUT_PRIORITY = -100;
492
+ var HELP_SHORTCUT_PRIORITY = 100;
493
+
494
+ // src/app/Navigator.tsx
495
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
496
+ var Navigator = () => {
497
+ const [shortcutsVisible, setShortcutsVisible] = useState4(false);
498
+ return /* @__PURE__ */ jsx6(ShortcutProvider, { children: /* @__PURE__ */ jsx6(
499
+ NavigatorShell,
500
+ {
501
+ shortcutsVisible,
502
+ onToggleShortcuts: () => setShortcutsVisible((visible) => !visible)
503
+ }
504
+ ) });
505
+ };
506
+ var NavigatorShell = ({
507
+ shortcutsVisible,
508
+ onToggleShortcuts
509
+ }) => {
510
+ const { exit } = useApp();
511
+ const navigate = useNavigate2();
512
+ const location = useLocation3();
513
+ const atRoot = location.pathname === ROUTES.root();
514
+ useInput((input, key) => {
515
+ if (key.ctrl && input === "c") {
516
+ exit();
517
+ return;
518
+ }
519
+ if (atRoot && (input === "q" || input === "Q")) {
520
+ exit();
521
+ }
522
+ });
523
+ useShortcutHandler(
524
+ ({ input }) => {
525
+ if (input !== "?") return false;
526
+ onToggleShortcuts();
527
+ return true;
528
+ },
529
+ { priority: HELP_SHORTCUT_PRIORITY }
530
+ );
531
+ useShortcutHandler(
532
+ ({ input }) => {
533
+ const route = {
534
+ t: "/tests",
535
+ r: ROUTES.testRuns.list(),
536
+ s: "/suites",
537
+ u: ROUTES.suiteRuns.list(),
538
+ p: "/project",
539
+ ",": "/settings"
540
+ }[input];
541
+ if (!route) return false;
542
+ navigate(route);
543
+ return true;
544
+ },
545
+ { priority: GLOBAL_SHORTCUT_PRIORITY }
546
+ );
547
+ return /* @__PURE__ */ jsx6(Box4, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx6(AuthGate, { children: (authenticated) => /* @__PURE__ */ jsxs3(
548
+ EscProvider,
549
+ {
550
+ canGoBack: !atRoot,
551
+ onUnhandled: () => {
552
+ if (!atRoot) navigate(-1);
553
+ },
554
+ children: [
555
+ /* @__PURE__ */ jsx6(Header, { authenticated }),
556
+ authenticated && /* @__PURE__ */ jsx6(Breadcrumb, {}),
557
+ /* @__PURE__ */ jsx6(Outlet, {}),
558
+ shortcutsVisible && /* @__PURE__ */ jsx6(ShortcutsGrid, {})
559
+ ]
560
+ }
561
+ ) }) });
562
+ };
563
+
564
+ // src/app/views/logout/Logout.tsx
565
+ import { StatusMessage } from "@inkjs/ui";
566
+ import { useApp as useApp2 } from "ink";
567
+ import { useEffect as useEffect4, useState as useState5 } from "react";
568
+ import { jsx as jsx7 } from "react/jsx-runtime";
569
+ var Logout = () => {
570
+ const { exit } = useApp2();
571
+ const [error, setError] = useState5(null);
572
+ useEffect4(() => {
573
+ let cleared;
574
+ try {
575
+ ({ cleared } = logout());
576
+ } catch (err) {
577
+ setError(err instanceof Error ? err.message : String(err));
578
+ return;
579
+ }
580
+ process.stdout.write(cleared ? "Logged out.\n" : "Already logged out.\n");
581
+ exit();
582
+ }, [exit]);
583
+ return /* @__PURE__ */ jsx7(ViewLayout, { title: "Logout", hintsHidden: true, children: error && /* @__PURE__ */ jsx7(StatusMessage, { variant: "error", children: `Error: ${error}` }) });
584
+ };
585
+
586
+ // src/app/hooks/useIsUserToken.ts
587
+ var useIsUserToken = () => {
588
+ const { auth } = useNavigatorSession();
589
+ const needsIdentity = auth.kind === "ready" && !auth.isUserToken;
590
+ const query = useFeatureQuery(
591
+ async () => needsIdentity ? (await getIdentity()).tokenType : null,
592
+ [needsIdentity, auth.token]
593
+ );
594
+ return auth.isUserToken || query.data === "user";
595
+ };
596
+
597
+ // src/app/views/MainMenu.tsx
598
+ import { jsx as jsx8 } from "react/jsx-runtime";
599
+ var MainMenu = () => {
600
+ const isUserToken = useIsUserToken();
601
+ const canSwitchProject = !hasProjectConfigFile();
602
+ const options = [
603
+ { label: "Tests", shortcut: "t", description: "Run, list, import tests", to: "/tests" },
604
+ {
605
+ label: "Test runs",
606
+ shortcut: "r",
607
+ description: "Browse recent test runs",
608
+ to: ROUTES.testRuns.root()
609
+ },
610
+ {
611
+ label: "Suites",
612
+ shortcut: "s",
613
+ description: "Run and list test suites",
614
+ to: "/suites"
615
+ },
616
+ {
617
+ label: "Suite runs",
618
+ shortcut: "u",
619
+ description: "Browse recent suite runs",
620
+ to: ROUTES.suiteRuns.root()
621
+ },
622
+ {
623
+ label: "Profiles",
624
+ shortcut: "f",
625
+ description: "Manage profiles",
626
+ to: "/profiles"
627
+ },
628
+ {
629
+ label: "Current project",
630
+ shortcut: "p",
631
+ description: "Import or export current project",
632
+ to: "/project"
633
+ },
634
+ ...canSwitchProject ? [
635
+ {
636
+ label: "Switch project",
637
+ shortcut: "w",
638
+ description: "Pick another project for this session",
639
+ to: "/project/switch"
640
+ }
641
+ ] : [],
642
+ ...isUserToken ? [
643
+ {
644
+ label: "Logout",
645
+ shortcut: "l",
646
+ description: "Clear the current CLI session",
647
+ to: "/logout"
648
+ }
649
+ ] : []
650
+ ];
651
+ return /* @__PURE__ */ jsx8(ViewLayout, { children: /* @__PURE__ */ jsx8(MenuList, { options }) });
652
+ };
653
+
654
+ // src/app/hooks/useProfiles.ts
655
+ var useProfilesList = () => useFeatureQuery(() => listProfiles(), []);
656
+
657
+ // src/features/profiles/profiles.utils.ts
658
+ var profileColumns = [
659
+ { key: "name", label: "Name", width: 36 },
660
+ { key: "default", label: "Default", width: 10 }
661
+ ];
662
+ var getProfileRow = (profile) => ({
663
+ key: profile.id,
664
+ cells: {
665
+ name: profile.name,
666
+ default: profile.isDefault ? "yes" : "no"
667
+ },
668
+ value: profile
669
+ });
670
+
671
+ // src/features/tui/components/BaseResourceListScreen/BaseResourceListScreen.tsx
672
+ import { Spinner, StatusMessage as StatusMessage2 } from "@inkjs/ui";
673
+ import { Box as Box7 } from "ink";
674
+ import { useState as useState6 } from "react";
675
+
676
+ // src/features/tui/components/TableList/TableList.tsx
677
+ import { Box as Box6, Text as Text5 } from "ink";
678
+
679
+ // src/features/tui/components/TableList/TableList.utils.ts
680
+ var fitCell = (value, width) => {
681
+ const text = value === null || value === void 0 || value === "" ? "-" : String(value);
682
+ const visible = text.length > width ? `${text.slice(0, Math.max(0, width - 1))}\u2026` : text;
683
+ return visible.padEnd(width, " ");
684
+ };
685
+
686
+ // src/features/tui/components/TableList/TableListRow.tsx
687
+ import { Box as Box5, Text as Text4 } from "ink";
688
+ import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
689
+ var TableListRow = ({
690
+ columns,
691
+ row,
692
+ selected
693
+ }) => /* @__PURE__ */ jsx9(SelectableOption, { selected, children: ({ color, backgroundColor }) => /* @__PURE__ */ jsxs4(Box5, { children: [
694
+ /* @__PURE__ */ jsx9(SelectionPrefix, { selected }),
695
+ columns.map((column, i) => {
696
+ const sep = i === 0 ? "" : " ";
697
+ const cell = row.cells[column.key];
698
+ if (column.render) {
699
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
700
+ sep ? /* @__PURE__ */ jsx9(Text4, { backgroundColor, children: sep }) : null,
701
+ /* @__PURE__ */ jsx9(Box5, { width: column.width, children: column.render(cell, row, {
702
+ selected,
703
+ width: column.width,
704
+ color,
705
+ backgroundColor
706
+ }) })
707
+ ] }, column.key);
708
+ }
709
+ return /* @__PURE__ */ jsxs4(Text4, { color, backgroundColor, children: [
710
+ sep,
711
+ fitCell(cell, column.width)
712
+ ] }, column.key);
713
+ })
714
+ ] }) });
715
+
716
+ // src/features/tui/components/TableList/TableList.tsx
717
+ import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
718
+ var TableList = ({
719
+ columns,
720
+ rows,
721
+ navigationDisabled = false,
722
+ onSelect
723
+ }) => /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
724
+ /* @__PURE__ */ jsx10(Box6, { children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
725
+ " ",
726
+ columns.map((column) => fitCell(column.label, column.width)).join(" ")
727
+ ] }) }),
728
+ /* @__PURE__ */ jsx10(
729
+ VirtualizedList,
730
+ {
731
+ items: rows,
732
+ reservedTerminalRows: DEFAULT_RESERVED_TERMINAL_ROWS + 1,
733
+ navigationDisabled,
734
+ getKey: (row) => row.key,
735
+ renderItem: (row, _rowIndex, selected) => /* @__PURE__ */ jsx10(TableListRow, { columns, row, selected: !navigationDisabled && selected }),
736
+ onSelect: (row) => onSelect?.(row)
737
+ }
738
+ )
739
+ ] });
740
+
741
+ // src/features/tui/components/BaseResourceListScreen/BaseResourceListScreen.tsx
742
+ import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
743
+ var BaseResourceListScreen = ({
744
+ title,
745
+ loadingLabel,
746
+ emptyMessage,
747
+ resources,
748
+ isLoading,
749
+ isError,
750
+ error,
751
+ columns,
752
+ getRow,
753
+ searchable,
754
+ query = "",
755
+ onSearch,
756
+ onSelectResource
757
+ }) => {
758
+ const rows = resources ?? [];
759
+ const [searchMode, setSearchMode] = useState6(false);
760
+ const handleSelect = (row) => {
761
+ if (row.value !== void 0 && onSelectResource) onSelectResource(row.value);
762
+ };
763
+ return /* @__PURE__ */ jsxs6(
764
+ ViewLayout,
765
+ {
766
+ title: query ? `Search: "${query}"` : title,
767
+ searchable,
768
+ onSearch,
769
+ onSearchModeChange: setSearchMode,
770
+ children: [
771
+ isLoading && /* @__PURE__ */ jsx11(Box7, { children: /* @__PURE__ */ jsx11(Spinner, { label: loadingLabel }) }),
772
+ isError && /* @__PURE__ */ jsx11(StatusMessage2, { variant: "error", children: error }),
773
+ !isLoading && !isError && rows.length === 0 && !query && /* @__PURE__ */ jsx11(StatusMessage2, { variant: "warning", children: emptyMessage }),
774
+ !isLoading && !isError && (rows.length > 0 || query) && /* @__PURE__ */ jsx11(
775
+ TableList,
776
+ {
777
+ columns,
778
+ rows: rows.map(getRow),
779
+ navigationDisabled: searchMode,
780
+ onSelect: handleSelect
781
+ }
782
+ )
783
+ ]
784
+ }
785
+ );
786
+ };
787
+
788
+ // src/app/views/profiles/ProfilesList.tsx
789
+ import { jsx as jsx12 } from "react/jsx-runtime";
790
+ var ProfilesList = () => {
791
+ const request = useProfilesList();
792
+ return /* @__PURE__ */ jsx12(
793
+ BaseResourceListScreen,
794
+ {
795
+ title: "Profiles",
796
+ loadingLabel: "Loading profiles...",
797
+ emptyMessage: "No profiles found",
798
+ resources: request.data,
799
+ isLoading: request.isLoading,
800
+ isError: request.isError,
801
+ error: request.error,
802
+ columns: profileColumns,
803
+ getRow: getProfileRow
804
+ }
805
+ );
806
+ };
807
+
808
+ // src/app/views/profiles/ProfilesMenu.tsx
809
+ import { Spinner as Spinner2 } from "@inkjs/ui";
810
+ import { useEffect as useEffect5 } from "react";
811
+ import { useNavigate as useNavigate3 } from "react-router";
812
+ import { jsx as jsx13 } from "react/jsx-runtime";
813
+ var ProfilesMenu = () => {
814
+ const navigate = useNavigate3();
815
+ useEffect5(() => {
816
+ navigate("/profiles/list", { replace: true });
817
+ }, [navigate]);
818
+ return /* @__PURE__ */ jsx13(Spinner2, { label: "Loading..." });
819
+ };
820
+
821
+ // src/app/views/project/ProjectExport.tsx
822
+ import { Spinner as Spinner3, StatusMessage as StatusMessage3 } from "@inkjs/ui";
823
+ import { useEffect as useEffect6, useState as useState8 } from "react";
824
+
825
+ // src/app/hooks/useFeatureMutation.ts
826
+ import { useCallback as useCallback3, useRef, useState as useState7 } from "react";
827
+ var useFeatureMutation = (fn) => {
828
+ const fnRef = useRef(fn);
829
+ const activeMutationRef = useRef(0);
830
+ const [state, setState] = useState7({
831
+ data: void 0,
832
+ error: void 0,
833
+ status: "idle"
834
+ });
835
+ fnRef.current = fn;
836
+ const trigger = useCallback3(async (arg) => {
837
+ const mutationId = activeMutationRef.current + 1;
838
+ activeMutationRef.current = mutationId;
839
+ setState({
840
+ data: void 0,
841
+ error: void 0,
842
+ status: "pending"
843
+ });
844
+ try {
845
+ const data = await fnRef.current(arg);
846
+ if (activeMutationRef.current === mutationId) {
847
+ setState({ data, error: void 0, status: "fulfilled" });
848
+ }
849
+ return { data };
850
+ } catch (err) {
851
+ const error = formatErrorMessage(err);
852
+ if (activeMutationRef.current === mutationId) {
853
+ setState({ data: void 0, error, status: "rejected" });
854
+ }
855
+ return { error };
856
+ }
857
+ }, []);
858
+ return [
859
+ trigger,
860
+ {
861
+ data: state.data,
862
+ error: state.error,
863
+ isLoading: state.status === "pending",
864
+ isSuccess: state.status === "fulfilled",
865
+ isError: state.status === "rejected"
866
+ }
867
+ ];
868
+ };
869
+
870
+ // src/features/tui/components/TextInput/TextInput.tsx
871
+ import { Box as Box8, Text as Text6 } from "ink";
872
+ import InkTextInput from "ink-text-input";
873
+ import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
874
+ var TextInput = (props) => {
875
+ useShortcutBlocker();
876
+ return /* @__PURE__ */ jsxs7(Box8, { children: [
877
+ /* @__PURE__ */ jsx14(Text6, { color: "cyan", children: "\u203A " }),
878
+ /* @__PURE__ */ jsx14(InkTextInput, { ...props })
879
+ ] });
880
+ };
881
+
882
+ // src/app/views/project/ProjectExport.tsx
883
+ import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
884
+ var ProjectExport = () => {
885
+ const [outputValue, setOutputValue] = useState8("");
886
+ const [output, setOutput] = useState8(null);
887
+ const [exportProjectMutation, exportProjectResult] = useFeatureMutation(
888
+ (outputPath) => exportProject(outputPath || void 0)
889
+ );
890
+ useEffect6(() => {
891
+ if (output !== null) void exportProjectMutation(output);
892
+ }, [exportProjectMutation, output]);
893
+ if (output === null) {
894
+ return /* @__PURE__ */ jsx15(ViewLayout, { title: "Output file (blank = auto)", hints: "confirm", hintsPosition: "bottom", children: /* @__PURE__ */ jsx15(TextInput, { value: outputValue, onChange: setOutputValue, onSubmit: setOutput }) });
895
+ }
896
+ return /* @__PURE__ */ jsxs8(ViewLayout, { hintsHidden: true, children: [
897
+ exportProjectResult.isLoading && /* @__PURE__ */ jsx15(Spinner3, { label: "Exporting project..." }),
898
+ exportProjectResult.isError && /* @__PURE__ */ jsx15(StatusMessage3, { variant: "error", children: exportProjectResult.error }),
899
+ exportProjectResult.data && /* @__PURE__ */ jsx15(StatusMessage3, { variant: "success", children: `Exported project to ${exportProjectResult.data.outPath} (${exportProjectResult.data.size} bytes)` })
900
+ ] });
901
+ };
902
+
903
+ // src/app/views/project/ProjectImport.tsx
904
+ import { Spinner as Spinner4, StatusMessage as StatusMessage4 } from "@inkjs/ui";
905
+ import { useEffect as useEffect7, useState as useState10 } from "react";
906
+ import { useNavigate as useNavigate4 } from "react-router";
907
+
908
+ // src/features/tui/components/FilePicker/FilePicker.tsx
909
+ import { readdirSync } from "fs";
910
+ import { dirname, join, resolve } from "path";
911
+ import { Box as Box9, Text as Text7 } from "ink";
912
+ import { useMemo, useState as useState9 } from "react";
913
+
914
+ // src/utils/filePicker.ts
915
+ import { extname } from "path";
916
+ var toFilePickerItems = (entries, allowedExtensions) => {
917
+ const normalizedExtensions = new Set(
918
+ allowedExtensions.map((extension) => extension.toLowerCase())
919
+ );
920
+ return entries.filter((entry) => {
921
+ if (entry.isDirectory()) return true;
922
+ return normalizedExtensions.has(extname(entry.name).toLowerCase());
923
+ }).map((entry) => {
924
+ const type = entry.isDirectory() ? "directory" : "file";
925
+ return {
926
+ key: `${type === "directory" ? "dir" : "file"}:${entry.name}`,
927
+ label: type === "directory" ? `${entry.name}/` : entry.name,
928
+ name: entry.name,
929
+ type
930
+ };
931
+ }).sort((left, right) => {
932
+ if (left.type !== right.type) return left.type === "directory" ? -1 : 1;
933
+ return left.name.localeCompare(right.name);
934
+ });
935
+ };
936
+
937
+ // src/features/tui/components/FilePicker/FilePicker.tsx
938
+ import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
939
+ var FilePicker = ({
940
+ label,
941
+ extensions,
942
+ initialDir = userCwd(),
943
+ onSubmit,
944
+ onBack
945
+ }) => {
946
+ const [currentDir, setCurrentDir] = useState9(() => resolve(userCwd(), initialDir));
947
+ useEscHandler(() => {
948
+ onBack();
949
+ return true;
950
+ });
951
+ const { items, error } = useMemo(() => {
952
+ try {
953
+ const entries = readdirSync(currentDir, { withFileTypes: true });
954
+ const pickerItems = toFilePickerItems(entries, extensions);
955
+ const parent = dirname(currentDir);
956
+ const allItems = parent === currentDir ? pickerItems : [
957
+ {
958
+ key: "dir:..",
959
+ label: "../",
960
+ name: "..",
961
+ type: "directory"
962
+ },
963
+ ...pickerItems
964
+ ];
965
+ return { items: allItems, error: null };
966
+ } catch (err) {
967
+ return {
968
+ items: [],
969
+ error: err instanceof Error ? err.message : String(err)
970
+ };
971
+ }
972
+ }, [currentDir, extensions]);
973
+ const handleSelect = (item) => {
974
+ const selected = item.value;
975
+ if (!selected) return;
976
+ if (selected.type === "directory") {
977
+ setCurrentDir((dir) => resolve(dir, selected.name));
978
+ return;
979
+ }
980
+ onSubmit(join(currentDir, selected.name));
981
+ };
982
+ return /* @__PURE__ */ jsx16(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(ViewLayout, { title: `${label}: ${currentDir}`, hints: "navigationOpenSelect", children: [
983
+ /* @__PURE__ */ jsx16(
984
+ MenuList,
985
+ {
986
+ options: items.map((item) => ({ label: item.label, value: item })),
987
+ onSelect: handleSelect
988
+ },
989
+ currentDir
990
+ ),
991
+ items.length === 0 && /* @__PURE__ */ jsx16(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text7, { dimColor: true, children: "No matching files in this directory." }) }),
992
+ error && /* @__PURE__ */ jsx16(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text7, { color: "red", children: error }) })
993
+ ] }) });
994
+ };
995
+
996
+ // src/app/views/project/ProjectImport.tsx
997
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
998
+ var ProjectImport = () => {
999
+ const navigate = useNavigate4();
1000
+ const [file, setFile] = useState10(null);
1001
+ const [importProjectMutation, importProjectResult] = useFeatureMutation(importProject);
1002
+ useEffect7(() => {
1003
+ if (file) importProjectMutation(file);
1004
+ }, [file, importProjectMutation]);
1005
+ if (!file) {
1006
+ return /* @__PURE__ */ jsx17(
1007
+ FilePicker,
1008
+ {
1009
+ label: "Select .zip file",
1010
+ extensions: [".zip"],
1011
+ onSubmit: setFile,
1012
+ onBack: () => navigate(-1)
1013
+ }
1014
+ );
1015
+ }
1016
+ return /* @__PURE__ */ jsxs10(ViewLayout, { hintsHidden: true, children: [
1017
+ importProjectResult.isLoading && /* @__PURE__ */ jsx17(Spinner4, { label: `Importing project from ${file}...` }),
1018
+ importProjectResult.isError && /* @__PURE__ */ jsx17(StatusMessage4, { variant: "error", children: importProjectResult.error }),
1019
+ importProjectResult.data && /* @__PURE__ */ jsx17(StatusMessage4, { variant: "success", children: `Project import started from ${importProjectResult.data.path} (${importProjectResult.data.size} bytes)` })
1020
+ ] });
1021
+ };
1022
+
1023
+ // src/app/views/project/ProjectInit.tsx
1024
+ import { useApp as useApp3 } from "ink";
1025
+ import { useEffect as useEffect8, useState as useState12 } from "react";
1026
+ import { useLocation as useLocation4, useNavigate as useNavigate5 } from "react-router";
1027
+
1028
+ // src/app/components/ProjectPicker/ProjectPicker.tsx
1029
+ import { Spinner as Spinner5, StatusMessage as StatusMessage5 } from "@inkjs/ui";
1030
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
1031
+ var ProjectPicker = ({ onSelect, currentProjectId }) => {
1032
+ const projectsQuery = useFeatureQuery(listProjects, []);
1033
+ const projects = projectsQuery.data ?? [];
1034
+ return /* @__PURE__ */ jsxs11(ViewLayout, { title: "Choose BugBug project", hints: "navigationSelect", children: [
1035
+ projectsQuery.isLoading && /* @__PURE__ */ jsx18(Spinner5, { label: "Loading projects..." }),
1036
+ projectsQuery.isError && /* @__PURE__ */ jsx18(StatusMessage5, { variant: "error", children: `Error: ${projectsQuery.error ?? "Unknown error"}` }),
1037
+ !projectsQuery.isLoading && !projectsQuery.isError && projects.length === 0 && /* @__PURE__ */ jsx18(StatusMessage5, { variant: "warning", children: "No projects found" }),
1038
+ !projectsQuery.isLoading && !projectsQuery.isError && projects.length > 0 && /* @__PURE__ */ jsx18(
1039
+ MenuList,
1040
+ {
1041
+ options: projects.map((project) => ({
1042
+ key: project.id,
1043
+ label: project.id === currentProjectId ? ` ${project.name} (current)` : ` ${project.name}`,
1044
+ value: project
1045
+ })),
1046
+ onSelect: (item) => {
1047
+ if (item.value) onSelect(item.value);
1048
+ }
1049
+ }
1050
+ )
1051
+ ] });
1052
+ };
1053
+
1054
+ // src/app/views/project/ProjectInitAuth.tsx
1055
+ import { StatusMessage as StatusMessage6 } from "@inkjs/ui";
1056
+ import { useState as useState11 } from "react";
1057
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
1058
+ var ProjectInitAuth = ({
1059
+ onTokenReceive,
1060
+ onTokenError,
1061
+ message
1062
+ }) => {
1063
+ const [method, setMethod] = useState11("choose");
1064
+ const [tokenInput, setTokenInput] = useState11("");
1065
+ const handleLogin = () => {
1066
+ void (async () => {
1067
+ try {
1068
+ const { token } = await login();
1069
+ updateSdkConfig({ apiToken: token });
1070
+ onTokenReceive(token, { isUserToken: true });
1071
+ } catch (err) {
1072
+ onTokenError(err instanceof Error ? err.message : String(err));
1073
+ }
1074
+ })();
1075
+ };
1076
+ const handleTokenSubmit = (value) => {
1077
+ const trimmed = value.trim();
1078
+ if (!trimmed) {
1079
+ onTokenError("Token must not be empty.");
1080
+ return;
1081
+ }
1082
+ try {
1083
+ updateSdkConfig({ apiToken: trimmed });
1084
+ onTokenReceive(trimmed, { isUserToken: false });
1085
+ } catch (err) {
1086
+ onTokenError(err instanceof Error ? err.message : String(err));
1087
+ }
1088
+ };
1089
+ if (method === "token") {
1090
+ return /* @__PURE__ */ jsx19(EscProvider, { onUnhandled: () => setMethod("choose"), children: /* @__PURE__ */ jsxs12(ViewLayout, { title: "BugBug project API token", hints: "confirm", hintsPosition: "bottom", children: [
1091
+ /* @__PURE__ */ jsx19(
1092
+ TextInput,
1093
+ {
1094
+ value: tokenInput,
1095
+ onChange: setTokenInput,
1096
+ onSubmit: handleTokenSubmit,
1097
+ mask: "*"
1098
+ }
1099
+ ),
1100
+ message && /* @__PURE__ */ jsx19(StatusMessage6, { variant: "error", children: `Error: ${message}` })
1101
+ ] }) });
1102
+ }
1103
+ return /* @__PURE__ */ jsxs12(ViewLayout, { title: "How do you want to authenticate?", hints: "navigationSelect", children: [
1104
+ /* @__PURE__ */ jsx19(
1105
+ MenuList,
1106
+ {
1107
+ options: [
1108
+ {
1109
+ label: "Login using BugBug",
1110
+ description: "Authenticate via BugBug and use your user token",
1111
+ value: "login"
1112
+ },
1113
+ {
1114
+ label: "Set API Token",
1115
+ description: "Paste a project or organization API token",
1116
+ value: "token"
1117
+ }
1118
+ ],
1119
+ onSelect: (item) => {
1120
+ if (item.value === "login") handleLogin();
1121
+ else setMethod("token");
1122
+ }
1123
+ }
1124
+ ),
1125
+ message && /* @__PURE__ */ jsx19(StatusMessage6, { variant: "error", children: `Error: ${message}` })
1126
+ ] });
1127
+ };
1128
+
1129
+ // src/app/views/project/ProjectInit.tsx
1130
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
1131
+ var ProjectInit = () => {
1132
+ const { exit } = useApp3();
1133
+ const navigate = useNavigate5();
1134
+ const location = useLocation4();
1135
+ const state = location.state;
1136
+ const redirectUrl = state?.redirectUrl ?? "/";
1137
+ const onlyThisSession = state?.onlyThisSession ?? false;
1138
+ const ignoreConfiguredAuth = state?.ignoreConfiguredAuth ?? false;
1139
+ const { auth, config, authError, completeSession } = useNavigatorSession();
1140
+ const [token, setToken] = useState12(
1141
+ ignoreConfiguredAuth ? null : auth.token ?? null
1142
+ );
1143
+ const [usingUserToken, setUsingUserToken] = useState12(false);
1144
+ const [error, setError] = useState12(null);
1145
+ useEffect8(() => {
1146
+ if (auth.kind !== "ready" || ignoreConfiguredAuth) return;
1147
+ if (onlyThisSession) {
1148
+ navigate(redirectUrl, { replace: true });
1149
+ }
1150
+ }, [auth.kind, ignoreConfiguredAuth, navigate, redirectUrl, onlyThisSession, exit]);
1151
+ const handleTokenReceive = (newToken, { isUserToken }) => {
1152
+ setError(null);
1153
+ setUsingUserToken(isUserToken);
1154
+ setToken(newToken);
1155
+ };
1156
+ const handleTokenError = (errorMessage) => {
1157
+ setError(errorMessage);
1158
+ };
1159
+ const handleProjectSelectAndFinish = (project) => {
1160
+ if (!token) return;
1161
+ if (usingUserToken) switchProject(project.id, { currentProjectId: config.projectId });
1162
+ const completeProjectSession = () => {
1163
+ if (usingUserToken) completeSession(token, project.id, { isUserToken: true });
1164
+ else completeSession(token, project.id);
1165
+ };
1166
+ if (onlyThisSession) {
1167
+ completeProjectSession();
1168
+ navigate(redirectUrl, { replace: true });
1169
+ return;
1170
+ }
1171
+ (async () => {
1172
+ try {
1173
+ await initProject(token, project.id);
1174
+ completeProjectSession();
1175
+ exit();
1176
+ } catch (err) {
1177
+ setError(err instanceof Error ? err.message : String(err));
1178
+ }
1179
+ })();
1180
+ };
1181
+ const message = error ?? authError;
1182
+ return /* @__PURE__ */ jsxs13(EscProvider, { onUnhandled: exit, children: [
1183
+ !token && /* @__PURE__ */ jsx20(
1184
+ ProjectInitAuth,
1185
+ {
1186
+ onTokenReceive: handleTokenReceive,
1187
+ onTokenError: handleTokenError,
1188
+ message
1189
+ }
1190
+ ),
1191
+ token && /* @__PURE__ */ jsx20(ProjectPicker, { onSelect: handleProjectSelectAndFinish, currentProjectId: auth.projectId })
1192
+ ] });
1193
+ };
1194
+
1195
+ // src/app/views/project/ProjectMenu.tsx
1196
+ import { jsx as jsx21 } from "react/jsx-runtime";
1197
+ var ProjectMenu = () => {
1198
+ const canSwitchProject = !hasProjectConfigFile();
1199
+ return /* @__PURE__ */ jsx21(ViewLayout, { children: /* @__PURE__ */ jsx21(
1200
+ MenuList,
1201
+ {
1202
+ options: [
1203
+ { label: "Export project (ZIP)", shortcut: "e", to: "/project/export" },
1204
+ ...canSwitchProject ? [{ label: "Import project (ZIP)", shortcut: "i", to: "/project/import" }] : []
1205
+ ]
1206
+ }
1207
+ ) });
1208
+ };
1209
+
1210
+ // src/app/views/project/ProjectSwitch.tsx
1211
+ import { useNavigate as useNavigate6 } from "react-router";
1212
+ import { jsx as jsx22 } from "react/jsx-runtime";
1213
+ var ProjectSwitch = () => {
1214
+ const navigate = useNavigate6();
1215
+ const { config, setSessionProject } = useNavigatorSession();
1216
+ const handleSelect = (project) => {
1217
+ switchProject(project.id, { currentProjectId: config.projectId });
1218
+ setSessionProject(project.id);
1219
+ navigate("/", { replace: true });
1220
+ };
1221
+ return /* @__PURE__ */ jsx22(ProjectPicker, { onSelect: handleSelect, currentProjectId: config.projectId });
1222
+ };
1223
+
1224
+ // src/app/views/settings/SettingsMenu.tsx
1225
+ import { jsx as jsx23 } from "react/jsx-runtime";
1226
+ var SettingsMenu = () => {
1227
+ const isUserToken = useIsUserToken();
1228
+ const options = [
1229
+ {
1230
+ label: `Set current session token`,
1231
+ shortcut: "t",
1232
+ to: "/settings/setSessionToken"
1233
+ },
1234
+ ...isUserToken ? [
1235
+ {
1236
+ label: "Logout",
1237
+ to: "/logout"
1238
+ }
1239
+ ] : []
1240
+ ];
1241
+ return /* @__PURE__ */ jsx23(ViewLayout, { children: /* @__PURE__ */ jsx23(MenuList, { options }) });
1242
+ };
1243
+
1244
+ // src/app/views/settings/SettingsSetSessionToken.tsx
1245
+ import { StatusMessage as StatusMessage7 } from "@inkjs/ui";
1246
+ import { useState as useState13 } from "react";
1247
+ import { jsx as jsx24, jsxs as jsxs14 } from "react/jsx-runtime";
1248
+ var SettingsSetSessionToken = () => {
1249
+ const { auth, completeSession } = useNavigatorSession();
1250
+ const [tokenValue, setTokenValue] = useState13("");
1251
+ const [token, setToken] = useState13(null);
1252
+ const [error, setError] = useState13(null);
1253
+ const [saved, setSaved] = useState13(false);
1254
+ const handleTokenSubmit = (value) => {
1255
+ const trimmed = value.trim();
1256
+ if (!trimmed) {
1257
+ setError("Token must not be empty.");
1258
+ return;
1259
+ }
1260
+ try {
1261
+ updateSdkConfig({ apiToken: trimmed });
1262
+ setError(null);
1263
+ setSaved(false);
1264
+ setToken(trimmed);
1265
+ } catch (err) {
1266
+ setError(err instanceof Error ? err.message : String(err));
1267
+ }
1268
+ };
1269
+ const handleProjectSelect = (project) => {
1270
+ if (!token) return;
1271
+ completeSession(token, project.id);
1272
+ setSaved(true);
1273
+ };
1274
+ if (token === null) {
1275
+ return /* @__PURE__ */ jsxs14(ViewLayout, { title: "Project API token", hints: "confirm", hintsPosition: "bottom", children: [
1276
+ /* @__PURE__ */ jsx24(
1277
+ TextInput,
1278
+ {
1279
+ value: tokenValue,
1280
+ onChange: setTokenValue,
1281
+ onSubmit: handleTokenSubmit,
1282
+ mask: "*"
1283
+ }
1284
+ ),
1285
+ error && /* @__PURE__ */ jsx24(StatusMessage7, { variant: "error", children: `Error: ${error}` })
1286
+ ] });
1287
+ }
1288
+ if (!saved) {
1289
+ return /* @__PURE__ */ jsx24(ProjectPicker, { onSelect: handleProjectSelect, currentProjectId: auth.projectId });
1290
+ }
1291
+ return /* @__PURE__ */ jsx24(ViewLayout, { hintsHidden: true, children: /* @__PURE__ */ jsx24(StatusMessage7, { variant: "success", children: "Token saved and valid only for this session" }) });
1292
+ };
1293
+
1294
+ // src/app/views/suiteRuns/SuiteRunDetails.tsx
1295
+ import { Spinner as Spinner6, StatusMessage as StatusMessage8 } from "@inkjs/ui";
1296
+ import { Text as Text8 } from "ink";
1297
+ import { useLocation as useLocation5, useParams } from "react-router";
1298
+
1299
+ // src/app/hooks/useSuiteRuns.ts
1300
+ var useSuiteRunsList = () => useFeatureQuery(() => listSuiteRuns(), []);
1301
+ var useSuiteRunDetails = (runId) => useFeatureQuery(async () => {
1302
+ if (!runId) return void 0;
1303
+ return getSuiteRunDetails(runId);
1304
+ }, [runId]);
1305
+
1306
+ // src/features/suiteRuns/suiteRuns.utils.ts
1307
+ var getSuiteRunName = (run) => run.name || run.id;
1308
+
1309
+ // src/app/views/suiteRuns/SuiteRunDetails.tsx
1310
+ import { jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
1311
+ var SuiteRunDetails = () => {
1312
+ const { runId } = useParams();
1313
+ const location = useLocation5();
1314
+ const state = location.state;
1315
+ const request = useSuiteRunDetails(runId);
1316
+ const title = request.data ? getSuiteRunName(request.data) : state?.suiteRunName;
1317
+ const menuState = title ? { suiteRunName: title } : void 0;
1318
+ if (!request.data || !runId) {
1319
+ return /* @__PURE__ */ jsxs15(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
1320
+ request.isLoading && /* @__PURE__ */ jsx25(Spinner6, { label: "Loading suite run..." }),
1321
+ request.isError && /* @__PURE__ */ jsx25(StatusMessage8, { variant: "error", children: request.error }),
1322
+ !request.isLoading && !request.isError && !request.data && /* @__PURE__ */ jsx25(Text8, { color: "yellow", children: "Suite run not found." })
1323
+ ] });
1324
+ }
1325
+ return /* @__PURE__ */ jsx25(BaseSuiteRunDetails, { runId, title, menuState });
1326
+ };
1327
+
1328
+ // src/app/views/suiteRuns/SuiteRunOpen.tsx
1329
+ import { Spinner as Spinner7, StatusMessage as StatusMessage9 } from "@inkjs/ui";
1330
+ import { Box as Box10, Text as Text9 } from "ink";
1331
+ import { useEffect as useEffect9 } from "react";
1332
+ import { useLocation as useLocation6, useParams as useParams2 } from "react-router";
1333
+ import { Fragment as Fragment2, jsx as jsx26, jsxs as jsxs16 } from "react/jsx-runtime";
1334
+ var WebUrlUnavailableMessage = () => /* @__PURE__ */ jsx26(StatusMessage9, { variant: "warning", children: "No web URL available for this run" });
1335
+ var OpenSuiteRun = ({ url }) => {
1336
+ const [openSuiteRun, openSuiteRunResult] = useFeatureMutation(async (url2) => {
1337
+ await openExternalUrl(url2);
1338
+ return url2;
1339
+ });
1340
+ useEffect9(() => {
1341
+ if (url) void openSuiteRun(url);
1342
+ }, [openSuiteRun, url]);
1343
+ if (!url) return /* @__PURE__ */ jsx26(WebUrlUnavailableMessage, {});
1344
+ return /* @__PURE__ */ jsxs16(Fragment2, { children: [
1345
+ openSuiteRunResult.isLoading && /* @__PURE__ */ jsx26(Spinner7, { label: "Opening BugBug..." }),
1346
+ openSuiteRunResult.isError && /* @__PURE__ */ jsx26(StatusMessage9, { variant: "error", children: openSuiteRunResult.error }),
1347
+ openSuiteRunResult.data && /* @__PURE__ */ jsxs16(Box10, { flexDirection: "column", children: [
1348
+ /* @__PURE__ */ jsx26(StatusMessage9, { variant: "success", children: "Opened in BugBug" }),
1349
+ /* @__PURE__ */ jsx26(Text9, { dimColor: true, children: openSuiteRunResult.data })
1350
+ ] })
1351
+ ] });
1352
+ };
1353
+ var SuiteRunOpen = () => {
1354
+ const { runId } = useParams2();
1355
+ const location = useLocation6();
1356
+ const state = location.state;
1357
+ const request = useSuiteRunDetails(runId);
1358
+ const title = request.data ? getSuiteRunName(request.data) : state?.suiteRunName ?? runId;
1359
+ return /* @__PURE__ */ jsxs16(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
1360
+ request.isLoading && /* @__PURE__ */ jsx26(Spinner7, { label: "Loading..." }),
1361
+ request.isError && /* @__PURE__ */ jsx26(Text9, { color: "red", children: request.error }),
1362
+ !request.isLoading && !request.isError && /* @__PURE__ */ jsx26(OpenSuiteRun, { url: request.data?.webappUrl })
1363
+ ] });
1364
+ };
1365
+
1366
+ // src/app/views/suiteRuns/SuiteRunShowTestRuns.tsx
1367
+ import { Spinner as Spinner8 } from "@inkjs/ui";
1368
+ import { StatusMessage as StatusMessage10 } from "@inkjs/ui";
1369
+ import { useLocation as useLocation7, useParams as useParams3 } from "react-router";
1370
+
1371
+ // src/features/testRuns/components/TestRunsList.tsx
1372
+ import { useNavigate as useNavigate7 } from "react-router";
1373
+
1374
+ // src/features/testRuns/testRuns.utils.ts
1375
+ var getTestRunName = (run) => run.name || run.id;
1376
+ var DEBUG_ARTIFACT_LABELS = {
1377
+ logs_partial: "Download partial test run logs",
1378
+ logs: "Download test run logs",
1379
+ cloud_logs: "Download cloud logs",
1380
+ container_stats: "Download container stats",
1381
+ chrome_debug_logs: "Download Chrome debug logs",
1382
+ network_logs: "Download network logs (HAR)",
1383
+ dom_snapshot: "Show DOM snapshot"
1384
+ };
1385
+ var getDebugArtifactLabel = (artifact) => DEBUG_ARTIFACT_LABELS[artifact.kind];
1386
+
1387
+ // src/features/testRuns/components/TestRunsList.constants.tsx
1388
+ import { jsx as jsx27 } from "react/jsx-runtime";
1389
+ var TEST_RUN_LIST_COLUMNS = [
1390
+ { key: "name", label: "Name", width: 30 },
1391
+ { key: "duration", label: "Duration", width: 8 },
1392
+ { key: "profileName", label: "Profile", width: 8 },
1393
+ { key: "triggeredBy", label: "Run by", width: 8 },
1394
+ { key: "errorCode", label: "Error", width: 20 },
1395
+ {
1396
+ key: "status",
1397
+ label: "Status",
1398
+ width: 15,
1399
+ render: (_cell, row, ctx) => /* @__PURE__ */ jsx27(
1400
+ Status,
1401
+ {
1402
+ status: row.value?.status,
1403
+ autoRetried: row.value?.isAutoRetried,
1404
+ color: ctx.color,
1405
+ backgroundColor: ctx.backgroundColor
1406
+ }
1407
+ )
1408
+ }
1409
+ ];
1410
+
1411
+ // src/features/testRuns/components/TestRunsList.utils.tsx
1412
+ var getTestRunRow = (run) => {
1413
+ return {
1414
+ key: run.id,
1415
+ cells: {
1416
+ name: getTestRunName(run),
1417
+ status: run.status ?? "",
1418
+ errorCode: run.errorCode ?? "",
1419
+ profileName: run.profileName ?? "",
1420
+ screenSizeType: run.screenSizeType ?? "",
1421
+ triggeredBy: run.triggeredBy ?? "",
1422
+ duration: formatDuration(run.duration, { started: run.started, ended: run.ended })
1423
+ },
1424
+ value: run
1425
+ };
1426
+ };
1427
+
1428
+ // src/features/testRuns/components/TestRunsList.tsx
1429
+ import { jsx as jsx28 } from "react/jsx-runtime";
1430
+ var TestRunsList = ({
1431
+ title,
1432
+ loadingLabel = "Loading test runs...",
1433
+ emptyMessage = "No test runs found",
1434
+ testRuns,
1435
+ isLoading,
1436
+ isError,
1437
+ error
1438
+ }) => {
1439
+ const navigate = useNavigate7();
1440
+ return /* @__PURE__ */ jsx28(
1441
+ BaseResourceListScreen,
1442
+ {
1443
+ title,
1444
+ loadingLabel,
1445
+ emptyMessage,
1446
+ resources: testRuns,
1447
+ isLoading,
1448
+ isError,
1449
+ error,
1450
+ columns: TEST_RUN_LIST_COLUMNS,
1451
+ getRow: getTestRunRow,
1452
+ onSelectResource: (run) => navigate(ROUTES.testRuns.details(run.id), { state: { testRunName: getTestRunName(run) } })
1453
+ }
1454
+ );
1455
+ };
1456
+
1457
+ // src/app/views/suiteRuns/SuiteRunShowTestRuns.tsx
1458
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1459
+ var SuiteRunTestRuns = ({ run, title }) => {
1460
+ const request = useFeatureQuery(() => getSuiteRunTestRuns(run.id), [run.id]);
1461
+ return /* @__PURE__ */ jsx29(
1462
+ TestRunsList,
1463
+ {
1464
+ title,
1465
+ loadingLabel: `Loading test runs for ${getSuiteRunName(run)}...`,
1466
+ emptyMessage: "No test runs available",
1467
+ testRuns: request.data,
1468
+ isLoading: request.isLoading,
1469
+ isError: request.isError,
1470
+ error: request.error
1471
+ }
1472
+ );
1473
+ };
1474
+ var SuiteRunShowTestRuns = () => {
1475
+ const { runId } = useParams3();
1476
+ const location = useLocation7();
1477
+ const state = location.state;
1478
+ const request = useSuiteRunDetails(runId);
1479
+ const title = request.data ? `Test runs in "${getSuiteRunName(request.data)}"` : state?.suiteRunName ? `Test runs in "${state.suiteRunName}"` : void 0;
1480
+ if (!request.data) {
1481
+ return /* @__PURE__ */ jsxs17(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
1482
+ request.isLoading && /* @__PURE__ */ jsx29(Spinner8, { label: "Loading..." }),
1483
+ request.isError && /* @__PURE__ */ jsx29(StatusMessage10, { variant: "error", children: request.error }),
1484
+ !request.isLoading && !request.isError && /* @__PURE__ */ jsx29(StatusMessage10, { variant: "warning", children: "No test runs available" })
1485
+ ] });
1486
+ }
1487
+ return /* @__PURE__ */ jsx29(SuiteRunTestRuns, { run: request.data, title: title ?? "" });
1488
+ };
1489
+
1490
+ // src/app/views/suiteRuns/SuiteRunsList.tsx
1491
+ import { useNavigate as useNavigate8 } from "react-router";
1492
+
1493
+ // src/app/views/suiteRuns/SuiteRunsList.constants.tsx
1494
+ import { jsx as jsx30 } from "react/jsx-runtime";
1495
+ var SUITE_RUN_LIST_COLUMNS = [
1496
+ { key: "name", label: "Name", width: 30 },
1497
+ { key: "duration", label: "Duration", width: 10 },
1498
+ { key: "profileName", label: "Profile", width: 8 },
1499
+ { key: "triggeredBy", label: "Run by", width: 10 },
1500
+ { key: "result", label: "Result", width: 14 },
1501
+ {
1502
+ key: "status",
1503
+ label: "Status",
1504
+ width: 10,
1505
+ render: (_cell, row, ctx) => /* @__PURE__ */ jsx30(Status, { status: row.value?.status, color: ctx.color, backgroundColor: ctx.backgroundColor })
1506
+ }
1507
+ ];
1508
+
1509
+ // src/app/views/suiteRuns/SuiteRunsList.utils.tsx
1510
+ var getSuiteRunResult = (run) => {
1511
+ const testRuns = run.testRuns ?? [];
1512
+ if (testRuns.length === 0) return "";
1513
+ const passed = testRuns.filter((testRun) => testRun.status === "passed").length;
1514
+ return `${passed}/${testRuns.length} passed`;
1515
+ };
1516
+ var getSuiteRunRow = (run) => {
1517
+ return {
1518
+ key: run.id,
1519
+ cells: {
1520
+ name: getSuiteRunName(run),
1521
+ status: run.status ?? "",
1522
+ result: getSuiteRunResult(run),
1523
+ profileName: run.profileName ?? "",
1524
+ runMode: run.runMode ?? "",
1525
+ triggeredBy: run.triggeredBy ?? "",
1526
+ duration: formatDuration(run.duration, { started: run.started, ended: run.ended })
1527
+ },
1528
+ value: run
1529
+ };
1530
+ };
1531
+
1532
+ // src/app/views/suiteRuns/SuiteRunsList.tsx
1533
+ import { jsx as jsx31 } from "react/jsx-runtime";
1534
+ var SuiteRunsList = () => {
1535
+ const navigate = useNavigate8();
1536
+ const request = useSuiteRunsList();
1537
+ return /* @__PURE__ */ jsx31(
1538
+ BaseResourceListScreen,
1539
+ {
1540
+ loadingLabel: "Loading suite runs...",
1541
+ emptyMessage: "No suite runs found",
1542
+ resources: request.data,
1543
+ isLoading: request.isLoading,
1544
+ isError: request.isError,
1545
+ error: request.error,
1546
+ columns: SUITE_RUN_LIST_COLUMNS,
1547
+ getRow: getSuiteRunRow,
1548
+ onSelectResource: (run) => navigate(ROUTES.suiteRuns.details(run.id), {
1549
+ state: { suiteRunName: getSuiteRunName(run) }
1550
+ })
1551
+ }
1552
+ );
1553
+ };
1554
+
1555
+ // src/app/views/suiteRuns/SuiteRunsMenu.tsx
1556
+ import { useEffect as useEffect10 } from "react";
1557
+ import { useNavigate as useNavigate9 } from "react-router";
1558
+ var SuiteRunsMenu = () => {
1559
+ const navigate = useNavigate9();
1560
+ useEffect10(() => {
1561
+ navigate(ROUTES.suiteRuns.list(), { replace: true });
1562
+ }, [navigate]);
1563
+ return null;
1564
+ };
1565
+
1566
+ // src/app/views/suites/RunSuite.tsx
1567
+ import { useNavigate as useNavigate10 } from "react-router";
1568
+
1569
+ // src/app/views/suites/BaseSuitesList.tsx
1570
+ import { useState as useState14 } from "react";
1571
+
1572
+ // src/app/hooks/useSuites.ts
1573
+ var useSuitesList = (query) => useFeatureQuery(() => loadSuiteTableResources(query), [query]);
1574
+ var useSuiteDetails = (suiteId) => useFeatureQuery(async () => {
1575
+ if (!suiteId) return void 0;
1576
+ return getSuiteDetails(suiteId);
1577
+ }, [suiteId]);
1578
+
1579
+ // src/features/suites/suites.constants.tsx
1580
+ import { jsx as jsx32 } from "react/jsx-runtime";
1581
+ var SUITE_COLUMNS = [
1582
+ { key: "name", label: "Name", width: 42 },
1583
+ {
1584
+ key: "status",
1585
+ label: "Status",
1586
+ width: 14,
1587
+ render: (_cell, row, ctx) => /* @__PURE__ */ jsx32(Status, { status: row.value?.status, color: ctx.color, backgroundColor: ctx.backgroundColor })
1588
+ }
1589
+ ];
1590
+
1591
+ // src/features/suites/suites.utils.tsx
1592
+ var getSuiteRow = (suite) => ({
1593
+ key: suite.id,
1594
+ cells: {
1595
+ name: getSuiteName(suite),
1596
+ status: formatRunStatus(suite.status)
1597
+ },
1598
+ value: suite
1599
+ });
1600
+ var getSuiteName = (suite) => suite.name || suite.id || "Untitled";
1601
+
1602
+ // src/app/views/suites/BaseSuitesList.tsx
1603
+ import { jsx as jsx33 } from "react/jsx-runtime";
1604
+ var BaseSuitesList = ({
1605
+ title,
1606
+ emptyMessage = "No suites found",
1607
+ onSelectSuite
1608
+ }) => {
1609
+ const [query, setQuery] = useState14("");
1610
+ const request = useSuitesList(query);
1611
+ return /* @__PURE__ */ jsx33(
1612
+ BaseResourceListScreen,
1613
+ {
1614
+ title,
1615
+ loadingLabel: "Loading suites...",
1616
+ emptyMessage,
1617
+ resources: request.data,
1618
+ isLoading: request.isLoading,
1619
+ isError: request.isError,
1620
+ error: request.error,
1621
+ columns: SUITE_COLUMNS,
1622
+ getRow: getSuiteRow,
1623
+ searchable: true,
1624
+ query,
1625
+ onSearch: (next) => setQuery(next.trim()),
1626
+ onSelectResource: onSelectSuite
1627
+ }
1628
+ );
1629
+ };
1630
+
1631
+ // src/app/views/suites/RunSuite.tsx
1632
+ import { jsx as jsx34 } from "react/jsx-runtime";
1633
+ var RunSuite = () => {
1634
+ const navigate = useNavigate10();
1635
+ return /* @__PURE__ */ jsx34(
1636
+ BaseSuitesList,
1637
+ {
1638
+ title: "Select a suite to run:",
1639
+ emptyMessage: "No suites available",
1640
+ onSelectSuite: (suite) => navigate(ROUTES.suites.run(suite.id))
1641
+ }
1642
+ );
1643
+ };
1644
+
1645
+ // src/app/views/suites/SuiteDetails.tsx
1646
+ import { Spinner as Spinner9, StatusMessage as StatusMessage11 } from "@inkjs/ui";
1647
+ import { Box as Box11 } from "ink";
1648
+ import { useLocation as useLocation8, useParams as useParams4 } from "react-router";
1649
+ import { jsx as jsx35, jsxs as jsxs18 } from "react/jsx-runtime";
1650
+ var SuiteDetails = () => {
1651
+ const { suiteId } = useParams4();
1652
+ const location = useLocation8();
1653
+ const state = location.state;
1654
+ const request = useSuiteDetails(suiteId);
1655
+ const title = request.data ? getSuiteName(request.data) : state?.suiteName;
1656
+ const detailsState = title ? { suiteName: title } : void 0;
1657
+ const data = request.data;
1658
+ const isNotFound = !request.isLoading && !request.isError && data === void 0;
1659
+ const isReady = !request.isLoading && !request.isError && data !== void 0;
1660
+ const hints = isReady ? void 0 : "none";
1661
+ const options = [
1662
+ {
1663
+ label: "Run",
1664
+ shortcut: "r",
1665
+ description: "Start a fresh run of this suite",
1666
+ to: ROUTES.suites.run(suiteId ?? ""),
1667
+ state: detailsState
1668
+ },
1669
+ {
1670
+ label: "Show last run",
1671
+ shortcut: "h",
1672
+ description: "View the most recent run for this suite",
1673
+ to: ROUTES.suites.lastRun(suiteId ?? ""),
1674
+ state: detailsState
1675
+ },
1676
+ {
1677
+ label: "Open in BugBug",
1678
+ shortcut: "o",
1679
+ description: "Open the suite page in your browser",
1680
+ to: ROUTES.suites.open(suiteId ?? "")
1681
+ }
1682
+ ];
1683
+ return /* @__PURE__ */ jsxs18(ViewLayout, { title, hints, children: [
1684
+ request.isLoading && /* @__PURE__ */ jsx35(Box11, { children: /* @__PURE__ */ jsx35(Spinner9, { label: "Loading suite..." }) }),
1685
+ request.isError && /* @__PURE__ */ jsx35(StatusMessage11, { variant: "error", children: request.error }),
1686
+ isNotFound && /* @__PURE__ */ jsx35(StatusMessage11, { variant: "warning", children: "Suite not found" }),
1687
+ isReady && /* @__PURE__ */ jsx35(MenuList, { options })
1688
+ ] });
1689
+ };
1690
+
1691
+ // src/app/views/suites/SuiteDetailsLastRun.tsx
1692
+ import { Spinner as Spinner10, StatusMessage as StatusMessage12 } from "@inkjs/ui";
1693
+ import { Text as Text10 } from "ink";
1694
+ import { useLocation as useLocation9, useParams as useParams5 } from "react-router";
1695
+ import { jsx as jsx36, jsxs as jsxs19 } from "react/jsx-runtime";
1696
+ var SuiteDetailsLastRun = () => {
1697
+ const { suiteId } = useParams5();
1698
+ const location = useLocation9();
1699
+ const state = location.state;
1700
+ const request = useSuiteDetails(suiteId);
1701
+ const suite = request.data;
1702
+ const isReady = !request.isLoading && !request.isError;
1703
+ const title = suite ? getSuiteName(suite) : state?.suiteName ?? suiteId;
1704
+ if (!isReady || !suite?.lastRunId) {
1705
+ return /* @__PURE__ */ jsxs19(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
1706
+ request.isLoading && /* @__PURE__ */ jsx36(Spinner10, { label: "Loading..." }),
1707
+ request.isError && /* @__PURE__ */ jsx36(Text10, { color: "red", children: request.error }),
1708
+ isReady && !suite?.lastRunId && /* @__PURE__ */ jsx36(StatusMessage12, { variant: "warning", children: `No previous run found for ${suite ? getSuiteName(suite) : suiteId ?? "suite"}` })
1709
+ ] });
1710
+ }
1711
+ return /* @__PURE__ */ jsx36(
1712
+ BaseSuiteRunDetails,
1713
+ {
1714
+ runId: suite.lastRunId,
1715
+ title,
1716
+ menuState: { suiteRunName: title }
1717
+ }
1718
+ );
1719
+ };
1720
+
1721
+ // src/app/views/suites/SuiteDetailsOpen.tsx
1722
+ import { StatusMessage as StatusMessage13 } from "@inkjs/ui";
1723
+ import { Spinner as Spinner11 } from "@inkjs/ui";
1724
+ import { Box as Box12, Text as Text11 } from "ink";
1725
+ import { useEffect as useEffect11 } from "react";
1726
+ import { useParams as useParams6 } from "react-router";
1727
+ import { jsx as jsx37, jsxs as jsxs20 } from "react/jsx-runtime";
1728
+ var WebUrlUnavailableMessage2 = () => /* @__PURE__ */ jsx37(StatusMessage13, { variant: "warning", children: "No web URL available for this suite" });
1729
+ var SuiteDetailsOpen = () => {
1730
+ const { suiteId } = useParams6();
1731
+ const request = useFeatureQuery(async () => {
1732
+ if (!suiteId) return void 0;
1733
+ return getSuite(suiteId);
1734
+ }, [suiteId]);
1735
+ const url = request.data?.webappUrl;
1736
+ const isReady = !request.isLoading && !request.isError;
1737
+ const [openSuite, openSuiteResult] = useFeatureMutation(async (url2) => {
1738
+ await openExternalUrl(url2);
1739
+ return url2;
1740
+ });
1741
+ useEffect11(() => {
1742
+ if (isReady && url) void openSuite(url);
1743
+ }, [isReady, openSuite, url]);
1744
+ return /* @__PURE__ */ jsxs20(ViewLayout, { hintsHidden: true, children: [
1745
+ request.isLoading && /* @__PURE__ */ jsx37(Spinner11, { label: "Loading..." }),
1746
+ request.isError && /* @__PURE__ */ jsx37(Text11, { color: "red", children: request.error }),
1747
+ isReady && !url && /* @__PURE__ */ jsx37(WebUrlUnavailableMessage2, {}),
1748
+ isReady && url && openSuiteResult.isLoading && /* @__PURE__ */ jsx37(Spinner11, { label: "Opening BugBug..." }),
1749
+ isReady && url && openSuiteResult.isError && /* @__PURE__ */ jsx37(StatusMessage13, { variant: "error", children: openSuiteResult.error }),
1750
+ isReady && url && openSuiteResult.data && /* @__PURE__ */ jsxs20(Box12, { flexDirection: "column", children: [
1751
+ /* @__PURE__ */ jsx37(StatusMessage13, { variant: "success", children: "Opened in BugBug" }),
1752
+ /* @__PURE__ */ jsx37(Text11, { dimColor: true, children: openSuiteResult.data })
1753
+ ] })
1754
+ ] });
1755
+ };
1756
+
1757
+ // src/app/views/suites/SuiteDetailsRun.tsx
1758
+ import { Spinner as Spinner12, StatusMessage as StatusMessage14 } from "@inkjs/ui";
1759
+ import { useEffect as useEffect12 } from "react";
1760
+ import { useLocation as useLocation10, useNavigate as useNavigate11, useParams as useParams7 } from "react-router";
1761
+ import { jsx as jsx38, jsxs as jsxs21 } from "react/jsx-runtime";
1762
+ var SuiteDetailsRun = () => {
1763
+ const { suiteId } = useParams7();
1764
+ const location = useLocation10();
1765
+ const navigate = useNavigate11();
1766
+ const state = location.state;
1767
+ const title = state?.suiteName ?? suiteId;
1768
+ const [runSuiteMutation, runSuiteResult] = useFeatureMutation((id) => runSuite(id, {}));
1769
+ useEffect12(() => {
1770
+ if (!suiteId) return;
1771
+ const startRun = async () => {
1772
+ try {
1773
+ const result = await runSuiteMutation(suiteId);
1774
+ if ("data" in result) {
1775
+ navigate(ROUTES.suites.lastRun(suiteId), { replace: true, state });
1776
+ }
1777
+ } catch {
1778
+ }
1779
+ };
1780
+ startRun();
1781
+ }, [navigate, runSuiteMutation, state, suiteId]);
1782
+ if (!runSuiteResult.data) {
1783
+ return /* @__PURE__ */ jsxs21(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
1784
+ runSuiteResult.isError && /* @__PURE__ */ jsx38(StatusMessage14, { variant: "error", children: runSuiteResult.error }),
1785
+ !runSuiteResult.isError && /* @__PURE__ */ jsx38(Spinner12, { label: "Initializing..." })
1786
+ ] });
1787
+ }
1788
+ return /* @__PURE__ */ jsx38(BaseSuiteRunDetails, { runId: runSuiteResult.data.id, title, menuState: state });
1789
+ };
1790
+
1791
+ // src/app/views/suites/SuitesList.tsx
1792
+ import { useNavigate as useNavigate12 } from "react-router";
1793
+ import { jsx as jsx39 } from "react/jsx-runtime";
1794
+ var SuitesList = () => {
1795
+ const navigate = useNavigate12();
1796
+ return /* @__PURE__ */ jsx39(
1797
+ BaseSuitesList,
1798
+ {
1799
+ onSelectSuite: (suite) => navigate(ROUTES.suites.details(suite.id), { state: { suiteName: getSuiteName(suite) } })
1800
+ }
1801
+ );
1802
+ };
1803
+
1804
+ // src/app/views/suites/SuitesMenu.tsx
1805
+ import { jsx as jsx40 } from "react/jsx-runtime";
1806
+ var SuitesMenu = () => {
1807
+ return /* @__PURE__ */ jsx40(ViewLayout, { children: /* @__PURE__ */ jsx40(
1808
+ MenuList,
1809
+ {
1810
+ options: [
1811
+ { label: "List suites", shortcut: "l", to: "/suites/list" },
1812
+ { label: "Run a suite", shortcut: "r", to: "/suites/run" }
1813
+ ]
1814
+ }
1815
+ ) });
1816
+ };
1817
+
1818
+ // src/app/views/testRuns/TestRunDebugArtifacts.tsx
1819
+ import { Spinner as Spinner13, StatusMessage as StatusMessage15 } from "@inkjs/ui";
1820
+ import { useLocation as useLocation11, useParams as useParams8 } from "react-router";
1821
+
1822
+ // src/app/hooks/useTestRuns.ts
1823
+ var useTestRunsList = () => useFeatureQuery(() => listTestRuns(), []);
1824
+ var useTestRunDetails = (runId) => useFeatureQuery(async () => {
1825
+ if (!runId) return void 0;
1826
+ return getTestRunDetails(runId);
1827
+ }, [runId]);
1828
+
1829
+ // src/app/views/testRuns/TestRunDebugArtifacts.tsx
1830
+ import { jsx as jsx41 } from "react/jsx-runtime";
1831
+ var ArtifactsUnavailableMessage = () => /* @__PURE__ */ jsx41(StatusMessage15, { variant: "warning", children: "No debug artifacts available" });
1832
+ var DebugArtifacts = ({ runId }) => {
1833
+ const request = useFeatureQuery(() => listTestRunDebugArtifacts(runId), [runId]);
1834
+ if (request.isLoading) {
1835
+ return /* @__PURE__ */ jsx41(Spinner13, { label: `Loading debug artifacts for ${runId}...` });
1836
+ }
1837
+ if (request.isError) {
1838
+ return /* @__PURE__ */ jsx41(StatusMessage15, { variant: "error", children: request.error });
1839
+ }
1840
+ if (!request.data || request.data.length === 0) {
1841
+ return /* @__PURE__ */ jsx41(ArtifactsUnavailableMessage, {});
1842
+ }
1843
+ return /* @__PURE__ */ jsx41(
1844
+ MenuList,
1845
+ {
1846
+ options: request.data.map((artifact) => ({
1847
+ label: getDebugArtifactLabel(artifact),
1848
+ value: artifact
1849
+ })),
1850
+ onSelect: (item) => {
1851
+ const artifact = item.value;
1852
+ if (artifact?.file) {
1853
+ void openExternalUrl(artifact.file);
1854
+ }
1855
+ }
1856
+ }
1857
+ );
1858
+ };
1859
+ var TestRunDebugArtifacts = () => {
1860
+ const { runId } = useParams8();
1861
+ const location = useLocation11();
1862
+ const state = location.state;
1863
+ const detailsRequest = useTestRunDetails(runId);
1864
+ const title = detailsRequest.data ? getTestRunName(detailsRequest.data) : state?.testRunName ?? "Debug artifacts";
1865
+ return /* @__PURE__ */ jsx41(
1866
+ ViewLayout,
1867
+ {
1868
+ title,
1869
+ hints: runId ? "navigationOpenSelect" : "none",
1870
+ hintsPosition: "bottom",
1871
+ children: runId ? /* @__PURE__ */ jsx41(DebugArtifacts, { runId }) : /* @__PURE__ */ jsx41(ArtifactsUnavailableMessage, {})
1872
+ }
1873
+ );
1874
+ };
1875
+
1876
+ // src/app/views/testRuns/TestRunDetails.tsx
1877
+ import { Spinner as Spinner14, StatusMessage as StatusMessage16 } from "@inkjs/ui";
1878
+ import { Text as Text12 } from "ink";
1879
+ import { useLocation as useLocation12, useParams as useParams9 } from "react-router";
1880
+ import { jsx as jsx42, jsxs as jsxs22 } from "react/jsx-runtime";
1881
+ var TestRunDetails = () => {
1882
+ const { runId } = useParams9();
1883
+ const location = useLocation12();
1884
+ const state = location.state;
1885
+ const request = useTestRunDetails(runId);
1886
+ const title = request.data ? getTestRunName(request.data) : state?.testRunName;
1887
+ const menuState = title ? { testRunName: title } : void 0;
1888
+ if (!request.data || !runId) {
1889
+ return /* @__PURE__ */ jsxs22(ViewLayout, { title, hintsHidden: true, children: [
1890
+ request.isLoading && /* @__PURE__ */ jsx42(Spinner14, { label: "Loading test run..." }),
1891
+ request.isError && /* @__PURE__ */ jsx42(StatusMessage16, { variant: "error", children: request.error }),
1892
+ !request.isLoading && !request.isError && !request.data && /* @__PURE__ */ jsx42(Text12, { color: "yellow", children: "Test run not found." })
1893
+ ] });
1894
+ }
1895
+ return /* @__PURE__ */ jsx42(BaseTestRunDetails, { runId, title, menuState });
1896
+ };
1897
+
1898
+ // src/app/views/testRuns/TestRunJunitReport.tsx
1899
+ import { Spinner as Spinner15, StatusMessage as StatusMessage17 } from "@inkjs/ui";
1900
+ import { Text as Text13 } from "ink";
1901
+ import { useEffect as useEffect13 } from "react";
1902
+ import { useLocation as useLocation13, useParams as useParams10 } from "react-router";
1903
+ import { Fragment as Fragment3, jsx as jsx43, jsxs as jsxs23 } from "react/jsx-runtime";
1904
+ var DownloadReport = ({ runId }) => {
1905
+ const [downloadReport, downloadReportResult] = useFeatureMutation(async (runId2) => {
1906
+ const xml = await getTestRunJunitReport(runId2);
1907
+ return writeReportXml(xml, defaultJunitReportPath("test", runId2));
1908
+ });
1909
+ useEffect13(() => {
1910
+ void downloadReport(runId);
1911
+ }, [downloadReport, runId]);
1912
+ return /* @__PURE__ */ jsxs23(Fragment3, { children: [
1913
+ downloadReportResult.isLoading && /* @__PURE__ */ jsx43(Spinner15, { label: "Downloading JUnit report..." }),
1914
+ downloadReportResult.isError && /* @__PURE__ */ jsx43(StatusMessage17, { variant: "error", children: downloadReportResult.error }),
1915
+ downloadReportResult.data?.outPath && /* @__PURE__ */ jsx43(StatusMessage17, { variant: "success", children: `Saved JUnit report to ${downloadReportResult.data.outPath} (${downloadReportResult.data.size} chars)` })
1916
+ ] });
1917
+ };
1918
+ var TestRunJunitReport = () => {
1919
+ const { runId } = useParams10();
1920
+ const location = useLocation13();
1921
+ const state = location.state;
1922
+ const request = useTestRunDetails(runId);
1923
+ const title = request.data ? getTestRunName(request.data) : state?.testRunName ?? runId;
1924
+ return /* @__PURE__ */ jsxs23(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
1925
+ request.isLoading && /* @__PURE__ */ jsx43(Spinner15, { label: "Loading..." }),
1926
+ request.isError && /* @__PURE__ */ jsx43(Text13, { color: "red", children: request.error }),
1927
+ !request.isLoading && !request.isError && runId && /* @__PURE__ */ jsx43(DownloadReport, { runId }),
1928
+ !request.isLoading && !request.isError && !runId && /* @__PURE__ */ jsx43(StatusMessage17, { variant: "warning", children: "No test run selected" })
1929
+ ] });
1930
+ };
1931
+
1932
+ // src/app/views/testRuns/TestRunOpen.tsx
1933
+ import { Spinner as Spinner16, StatusMessage as StatusMessage18 } from "@inkjs/ui";
1934
+ import { Box as Box13, Text as Text14 } from "ink";
1935
+ import { useEffect as useEffect14 } from "react";
1936
+ import { useLocation as useLocation14, useParams as useParams11 } from "react-router";
1937
+ import { Fragment as Fragment4, jsx as jsx44, jsxs as jsxs24 } from "react/jsx-runtime";
1938
+ var WebUrlUnavailableMessage3 = () => /* @__PURE__ */ jsx44(StatusMessage18, { variant: "warning", children: "No web URL available for this run" });
1939
+ var OpenRun = ({ url }) => {
1940
+ const [openRun, openRunResult] = useFeatureMutation(async (url2) => {
1941
+ await openExternalUrl(url2);
1942
+ return url2;
1943
+ });
1944
+ useEffect14(() => {
1945
+ if (url) void openRun(url);
1946
+ }, [openRun, url]);
1947
+ if (!url) return /* @__PURE__ */ jsx44(WebUrlUnavailableMessage3, {});
1948
+ return /* @__PURE__ */ jsxs24(Fragment4, { children: [
1949
+ openRunResult.isLoading && /* @__PURE__ */ jsx44(Spinner16, { label: "Opening BugBug..." }),
1950
+ openRunResult.isError && /* @__PURE__ */ jsx44(StatusMessage18, { variant: "error", children: openRunResult.error }),
1951
+ openRunResult.data && /* @__PURE__ */ jsxs24(Box13, { flexDirection: "column", children: [
1952
+ /* @__PURE__ */ jsx44(StatusMessage18, { variant: "success", children: "Opened in BugBug" }),
1953
+ /* @__PURE__ */ jsx44(Text14, { dimColor: true, children: openRunResult.data })
1954
+ ] })
1955
+ ] });
1956
+ };
1957
+ var TestRunOpen = () => {
1958
+ const { runId } = useParams11();
1959
+ const location = useLocation14();
1960
+ const state = location.state;
1961
+ const request = useTestRunDetails(runId);
1962
+ const title = request.data ? getTestRunName(request.data) : state?.testRunName ?? runId;
1963
+ return /* @__PURE__ */ jsxs24(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
1964
+ request.isLoading && /* @__PURE__ */ jsx44(Spinner16, { label: "Loading..." }),
1965
+ request.isError && /* @__PURE__ */ jsx44(Text14, { color: "red", children: request.error }),
1966
+ !request.isLoading && !request.isError && /* @__PURE__ */ jsx44(OpenRun, { url: request.data?.webappUrl })
1967
+ ] });
1968
+ };
1969
+
1970
+ // src/app/views/testRuns/TestRunsList.tsx
1971
+ import { jsx as jsx45 } from "react/jsx-runtime";
1972
+ var TestRunsList2 = () => {
1973
+ const request = useTestRunsList();
1974
+ return /* @__PURE__ */ jsx45(
1975
+ TestRunsList,
1976
+ {
1977
+ testRuns: request.data,
1978
+ isLoading: request.isLoading,
1979
+ isError: request.isError,
1980
+ error: request.error
1981
+ }
1982
+ );
1983
+ };
1984
+
1985
+ // src/app/views/testRuns/TestRunsMenu.tsx
1986
+ import { useEffect as useEffect15 } from "react";
1987
+ import { useNavigate as useNavigate13 } from "react-router";
1988
+ var TestRunsMenu = () => {
1989
+ const navigate = useNavigate13();
1990
+ useEffect15(() => {
1991
+ navigate(ROUTES.testRuns.list(), { replace: true });
1992
+ }, [navigate]);
1993
+ return null;
1994
+ };
1995
+
1996
+ // src/app/views/tests/RunTest.tsx
1997
+ import { useNavigate as useNavigate14 } from "react-router";
1998
+
1999
+ // src/app/views/tests/BaseTestsList.tsx
2000
+ import { useState as useState15 } from "react";
2001
+
2002
+ // src/app/hooks/useTests.ts
2003
+ var useTestsList = (query) => useFeatureQuery(() => loadTestTableResources(query), [query]);
2004
+ var useTestDetails = (testId) => useFeatureQuery(async () => {
2005
+ if (!testId) return void 0;
2006
+ return getTestDetails(testId);
2007
+ }, [testId]);
2008
+
2009
+ // src/features/tests/tests.constants.tsx
2010
+ import { jsx as jsx46 } from "react/jsx-runtime";
2011
+ var TEST_COLUMNS = [
2012
+ { key: "name", label: "Name", width: 42 },
2013
+ {
2014
+ key: "status",
2015
+ label: "Status",
2016
+ width: 14,
2017
+ render: (_cell, row, ctx) => /* @__PURE__ */ jsx46(Status, { status: row.value?.status, color: ctx.color, backgroundColor: ctx.backgroundColor })
2018
+ }
2019
+ ];
2020
+
2021
+ // src/features/tests/tests.utils.tsx
2022
+ var getTestRow = (test) => ({
2023
+ key: test.id,
2024
+ cells: {
2025
+ name: getTestName(test),
2026
+ status: formatRunStatus(test.status)
2027
+ },
2028
+ value: test
2029
+ });
2030
+ var getTestName = (test) => test.name || test.id || "Untitled";
2031
+
2032
+ // src/app/views/tests/BaseTestsList.tsx
2033
+ import { jsx as jsx47 } from "react/jsx-runtime";
2034
+ var BaseTestsList = ({
2035
+ title,
2036
+ emptyMessage = "No tests found",
2037
+ onSelectTest
2038
+ }) => {
2039
+ const [query, setQuery] = useState15("");
2040
+ const request = useTestsList(query);
2041
+ return /* @__PURE__ */ jsx47(
2042
+ BaseResourceListScreen,
2043
+ {
2044
+ title,
2045
+ loadingLabel: "Loading tests...",
2046
+ emptyMessage,
2047
+ resources: request.data,
2048
+ isLoading: request.isLoading,
2049
+ isError: request.isError,
2050
+ error: request.error,
2051
+ columns: TEST_COLUMNS,
2052
+ getRow: getTestRow,
2053
+ searchable: true,
2054
+ query,
2055
+ onSearch: (next) => setQuery(next.trim()),
2056
+ onSelectResource: onSelectTest
2057
+ }
2058
+ );
2059
+ };
2060
+
2061
+ // src/app/views/tests/RunTest.tsx
2062
+ import { jsx as jsx48 } from "react/jsx-runtime";
2063
+ var RunTest = () => {
2064
+ const navigate = useNavigate14();
2065
+ return /* @__PURE__ */ jsx48(
2066
+ BaseTestsList,
2067
+ {
2068
+ title: "Select a test to run:",
2069
+ emptyMessage: "No tests available",
2070
+ onSelectTest: (test) => navigate(ROUTES.tests.run(test.id))
2071
+ }
2072
+ );
2073
+ };
2074
+
2075
+ // src/app/views/tests/TestDetails.tsx
2076
+ import { Spinner as Spinner17, StatusMessage as StatusMessage19 } from "@inkjs/ui";
2077
+ import { Box as Box14 } from "ink";
2078
+ import { useLocation as useLocation15, useParams as useParams12 } from "react-router";
2079
+ import { jsx as jsx49, jsxs as jsxs25 } from "react/jsx-runtime";
2080
+ var TestDetails = () => {
2081
+ const { testId } = useParams12();
2082
+ const location = useLocation15();
2083
+ const state = location.state;
2084
+ const request = useTestDetails(testId);
2085
+ const title = request.data ? getTestName(request.data) : state?.testName;
2086
+ const detailsState = title ? { testName: title } : void 0;
2087
+ const data = request.data;
2088
+ const isNotFound = !request.isLoading && !request.isError && data === void 0;
2089
+ const isReady = !request.isLoading && !request.isError && data !== void 0;
2090
+ const hints = isReady ? void 0 : "none";
2091
+ const options = [
2092
+ {
2093
+ label: "Run",
2094
+ shortcut: "r",
2095
+ description: "Start a fresh run of this test",
2096
+ to: ROUTES.tests.run(testId ?? ""),
2097
+ state: detailsState
2098
+ },
2099
+ {
2100
+ label: "Show last run",
2101
+ shortcut: "h",
2102
+ description: "View the most recent run for this test",
2103
+ to: ROUTES.tests.lastRun(testId ?? ""),
2104
+ state: detailsState
2105
+ },
2106
+ {
2107
+ label: "Export (YAML)",
2108
+ shortcut: "e",
2109
+ description: "Save the test definition to a YAML file in the current directory",
2110
+ to: ROUTES.tests.export(testId ?? "")
2111
+ },
2112
+ {
2113
+ label: "Open in BugBug",
2114
+ shortcut: "o",
2115
+ description: "Open the test page in your browser",
2116
+ to: ROUTES.tests.open(testId ?? "")
2117
+ }
2118
+ ];
2119
+ return /* @__PURE__ */ jsxs25(ViewLayout, { title, hints, children: [
2120
+ request.isLoading && /* @__PURE__ */ jsx49(Box14, { children: /* @__PURE__ */ jsx49(Spinner17, { label: "Loading test..." }) }),
2121
+ request.isError && /* @__PURE__ */ jsx49(StatusMessage19, { variant: "error", children: request.error }),
2122
+ isNotFound && /* @__PURE__ */ jsx49(StatusMessage19, { variant: "warning", children: "Test not found." }),
2123
+ isReady && /* @__PURE__ */ jsx49(MenuList, { options })
2124
+ ] });
2125
+ };
2126
+
2127
+ // src/app/views/tests/TestDetailsExport.tsx
2128
+ import { StatusMessage as StatusMessage20 } from "@inkjs/ui";
2129
+ import { Spinner as Spinner18 } from "@inkjs/ui";
2130
+ import { useEffect as useEffect16 } from "react";
2131
+ import { useParams as useParams13 } from "react-router";
2132
+ import { jsx as jsx50, jsxs as jsxs26 } from "react/jsx-runtime";
2133
+ var TestDetailsExport = () => {
2134
+ const { testId } = useParams13();
2135
+ const [exportTestMutation, exportTestResult] = useFeatureMutation(
2136
+ (id) => exportTest(id, "yaml")
2137
+ );
2138
+ useEffect16(() => {
2139
+ if (testId) void exportTestMutation(testId);
2140
+ }, [exportTestMutation, testId]);
2141
+ return /* @__PURE__ */ jsxs26(ViewLayout, { hintsHidden: true, children: [
2142
+ exportTestResult.isLoading && /* @__PURE__ */ jsx50(Spinner18, { label: `Exporting ${testId}...` }),
2143
+ exportTestResult.isError && /* @__PURE__ */ jsx50(StatusMessage20, { variant: "error", children: exportTestResult.error }),
2144
+ exportTestResult.data && /* @__PURE__ */ jsx50(StatusMessage20, { variant: "success", children: `Exported to ${exportTestResult.data.outPath} (${exportTestResult.data.size} chars)` })
2145
+ ] });
2146
+ };
2147
+
2148
+ // src/app/views/tests/TestDetailsLastRun.tsx
2149
+ import { Spinner as Spinner19, StatusMessage as StatusMessage21 } from "@inkjs/ui";
2150
+ import { Text as Text15 } from "ink";
2151
+ import { useLocation as useLocation16, useParams as useParams14 } from "react-router";
2152
+ import { jsx as jsx51, jsxs as jsxs27 } from "react/jsx-runtime";
2153
+ var TestDetailsLastRun = () => {
2154
+ const { testId } = useParams14();
2155
+ const location = useLocation16();
2156
+ const state = location.state;
2157
+ const request = useTestDetails(testId);
2158
+ const test = request.data;
2159
+ const isReady = !request.isLoading && !request.isError;
2160
+ const title = test ? getTestName(test) : state?.testName ?? testId;
2161
+ if (!isReady || !test?.lastRunId) {
2162
+ return /* @__PURE__ */ jsxs27(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
2163
+ request.isLoading && /* @__PURE__ */ jsx51(Spinner19, { label: "Loading..." }),
2164
+ request.isError && /* @__PURE__ */ jsx51(Text15, { color: "red", children: request.error }),
2165
+ isReady && !test?.lastRunId && /* @__PURE__ */ jsx51(StatusMessage21, { variant: "warning", children: `No previous run found for ${test ? getTestName(test) : testId ?? "test"}` })
2166
+ ] });
2167
+ }
2168
+ return /* @__PURE__ */ jsx51(BaseTestRunDetails, { runId: test.lastRunId, title, menuState: { testRunName: title } });
2169
+ };
2170
+
2171
+ // src/app/views/tests/TestDetailsOpen.tsx
2172
+ import { StatusMessage as StatusMessage22 } from "@inkjs/ui";
2173
+ import { Spinner as Spinner20 } from "@inkjs/ui";
2174
+ import { Box as Box15, Text as Text16 } from "ink";
2175
+ import { useEffect as useEffect17 } from "react";
2176
+ import { useParams as useParams15 } from "react-router";
2177
+ import { jsx as jsx52, jsxs as jsxs28 } from "react/jsx-runtime";
2178
+ var WebUrlUnavailableMessage4 = () => /* @__PURE__ */ jsx52(StatusMessage22, { variant: "warning", children: "No web URL available for this test" });
2179
+ var TestDetailsOpen = () => {
2180
+ const { testId } = useParams15();
2181
+ const request = useFeatureQuery(async () => {
2182
+ if (!testId) return void 0;
2183
+ return getTest(testId);
2184
+ }, [testId]);
2185
+ const url = request.data?.webappUrl;
2186
+ const isReady = !request.isLoading && !request.isError;
2187
+ const [openTest, openTestResult] = useFeatureMutation(async (url2) => {
2188
+ await openExternalUrl(url2);
2189
+ return url2;
2190
+ });
2191
+ useEffect17(() => {
2192
+ if (isReady && url) void openTest(url);
2193
+ }, [isReady, openTest, url]);
2194
+ return /* @__PURE__ */ jsxs28(ViewLayout, { hintsHidden: true, children: [
2195
+ request.isLoading && /* @__PURE__ */ jsx52(Spinner20, { label: "Loading..." }),
2196
+ request.isError && /* @__PURE__ */ jsx52(Text16, { color: "red", children: request.error }),
2197
+ isReady && !url && /* @__PURE__ */ jsx52(WebUrlUnavailableMessage4, {}),
2198
+ isReady && url && openTestResult.isLoading && /* @__PURE__ */ jsx52(Spinner20, { label: "Opening BugBug..." }),
2199
+ isReady && url && openTestResult.isError && /* @__PURE__ */ jsx52(StatusMessage22, { variant: "error", children: openTestResult.error }),
2200
+ isReady && url && openTestResult.data && /* @__PURE__ */ jsxs28(Box15, { flexDirection: "column", children: [
2201
+ /* @__PURE__ */ jsx52(StatusMessage22, { variant: "success", children: "Opened in BugBug" }),
2202
+ /* @__PURE__ */ jsx52(Text16, { dimColor: true, children: openTestResult.data })
2203
+ ] })
2204
+ ] });
2205
+ };
2206
+
2207
+ // src/app/views/tests/TestDetailsRun.tsx
2208
+ import { Spinner as Spinner21, StatusMessage as StatusMessage23 } from "@inkjs/ui";
2209
+ import { useEffect as useEffect18 } from "react";
2210
+ import { useLocation as useLocation17, useNavigate as useNavigate15, useParams as useParams16 } from "react-router";
2211
+ import { jsx as jsx53, jsxs as jsxs29 } from "react/jsx-runtime";
2212
+ var TestDetailsRun = () => {
2213
+ const { testId } = useParams16();
2214
+ const location = useLocation17();
2215
+ const navigate = useNavigate15();
2216
+ const state = location.state;
2217
+ const title = state?.testName ?? testId;
2218
+ const [runTestMutation, runTestResult] = useFeatureMutation((id) => runTest(id, {}));
2219
+ useEffect18(() => {
2220
+ if (!testId) return;
2221
+ const startRun = async () => {
2222
+ try {
2223
+ const result = await runTestMutation(testId);
2224
+ if ("data" in result) {
2225
+ navigate(ROUTES.tests.lastRun(testId), { replace: true, state });
2226
+ }
2227
+ } catch {
2228
+ }
2229
+ };
2230
+ startRun();
2231
+ }, [navigate, runTestMutation, state, testId]);
2232
+ if (!runTestResult.data) {
2233
+ return /* @__PURE__ */ jsxs29(ViewLayout, { title, hintsHidden: true, hintsPosition: "bottom", children: [
2234
+ runTestResult.isError && /* @__PURE__ */ jsx53(StatusMessage23, { variant: "error", children: runTestResult.error }),
2235
+ !runTestResult.isError && /* @__PURE__ */ jsx53(Spinner21, { label: "Initializing..." })
2236
+ ] });
2237
+ }
2238
+ return /* @__PURE__ */ jsx53(BaseTestRunDetails, { runId: runTestResult.data.id, title, menuState: state });
2239
+ };
2240
+
2241
+ // src/app/views/tests/TestImport.tsx
2242
+ import { Spinner as Spinner22, StatusMessage as StatusMessage24 } from "@inkjs/ui";
2243
+ import { useEffect as useEffect19, useState as useState16 } from "react";
2244
+ import { useNavigate as useNavigate16 } from "react-router";
2245
+ import { jsx as jsx54, jsxs as jsxs30 } from "react/jsx-runtime";
2246
+ var TestImport = ({ initialDirectory } = {}) => {
2247
+ const navigate = useNavigate16();
2248
+ const [file, setFile] = useState16(null);
2249
+ const [conflictMode, setConflictMode] = useState16(null);
2250
+ const [importTestMutation, importTestResult] = useFeatureMutation(
2251
+ async ({ file: file2, conflictMode: conflictMode2 }) => {
2252
+ detectFormat(file2);
2253
+ return importTest(file2, {
2254
+ conflictMode: conflictMode2 || void 0
2255
+ });
2256
+ }
2257
+ );
2258
+ useEffect19(() => {
2259
+ if (file && conflictMode !== null) {
2260
+ void importTestMutation({ file, conflictMode });
2261
+ }
2262
+ }, [conflictMode, file, importTestMutation]);
2263
+ useEscHandler(() => {
2264
+ if (!file || conflictMode !== null) return false;
2265
+ setFile(null);
2266
+ return true;
2267
+ });
2268
+ if (!file) {
2269
+ return /* @__PURE__ */ jsx54(
2270
+ FilePicker,
2271
+ {
2272
+ label: "Select .yaml/.yml/.zip file",
2273
+ initialDir: initialDirectory,
2274
+ extensions: [".yaml", ".yml", ".zip"],
2275
+ onSubmit: setFile,
2276
+ onBack: () => navigate(-1)
2277
+ }
2278
+ );
2279
+ }
2280
+ if (conflictMode === null) {
2281
+ return /* @__PURE__ */ jsx54(ViewLayout, { title: "Conflict mode (blank = API default)", children: /* @__PURE__ */ jsx54(
2282
+ MenuList,
2283
+ {
2284
+ options: [
2285
+ { label: "Default", value: "" },
2286
+ { label: "Overwrite", value: "overwrite" }
2287
+ ],
2288
+ onSelect: (item) => setConflictMode(item.value ?? "")
2289
+ }
2290
+ ) });
2291
+ }
2292
+ return /* @__PURE__ */ jsxs30(ViewLayout, { hintsHidden: true, hintsPosition: "bottom", children: [
2293
+ importTestResult.isLoading && /* @__PURE__ */ jsx54(Spinner22, { label: `Importing ${file}...` }),
2294
+ importTestResult.isError && /* @__PURE__ */ jsx54(StatusMessage24, { variant: "error", children: importTestResult.error }),
2295
+ importTestResult.data && /* @__PURE__ */ jsx54(StatusMessage24, { variant: "success", children: `Imported test: ${importTestResult.data.id ?? "(unknown)"}${importTestResult.data.name ? ` (${importTestResult.data.name})` : ""}` })
2296
+ ] });
2297
+ };
2298
+
2299
+ // src/app/views/tests/TestsList.tsx
2300
+ import { useNavigate as useNavigate17 } from "react-router";
2301
+ import { jsx as jsx55 } from "react/jsx-runtime";
2302
+ var TestsList = () => {
2303
+ const navigate = useNavigate17();
2304
+ return /* @__PURE__ */ jsx55(
2305
+ BaseTestsList,
2306
+ {
2307
+ onSelectTest: (test) => navigate(ROUTES.tests.details(test.id), { state: { testName: getTestName(test) } })
2308
+ }
2309
+ );
2310
+ };
2311
+
2312
+ // src/app/views/tests/TestsMenu.tsx
2313
+ import { jsx as jsx56 } from "react/jsx-runtime";
2314
+ var TestsMenu = () => {
2315
+ return /* @__PURE__ */ jsx56(ViewLayout, { children: /* @__PURE__ */ jsx56(
2316
+ MenuList,
2317
+ {
2318
+ options: [
2319
+ { label: "List tests", shortcut: "l", to: "/tests/list" },
2320
+ {
2321
+ label: "Import test",
2322
+ shortcut: "i",
2323
+ description: "YAML or ZIP",
2324
+ to: "/tests/import"
2325
+ },
2326
+ { label: "Run a test", shortcut: "r", to: "/tests/run" }
2327
+ ]
2328
+ }
2329
+ ) });
2330
+ };
2331
+
2332
+ // src/app/router.tsx
2333
+ import { jsx as jsx57, jsxs as jsxs31 } from "react/jsx-runtime";
2334
+ var createNavigatorRouter = ({
2335
+ initialRoute,
2336
+ initialRouteState
2337
+ } = {}) => {
2338
+ const getInitialEntries = () => {
2339
+ if (initialRoute) {
2340
+ const routeEntry = initialRouteState === void 0 ? initialRoute : { pathname: initialRoute, state: initialRouteState };
2341
+ return [ROUTES.root(), routeEntry];
2342
+ }
2343
+ return [ROUTES.root()];
2344
+ };
2345
+ const initialEntries = getInitialEntries();
2346
+ const router = createMemoryRouter(
2347
+ createRoutesFromElements(
2348
+ /* @__PURE__ */ jsxs31(Route, { element: /* @__PURE__ */ jsx57(Navigator, {}), children: [
2349
+ /* @__PURE__ */ jsx57(Route, { index: true, element: /* @__PURE__ */ jsx57(MainMenu, {}) }),
2350
+ /* @__PURE__ */ jsx57(Route, { path: "/init", element: /* @__PURE__ */ jsx57(ProjectInit, {}) }),
2351
+ /* @__PURE__ */ jsx57(Route, { path: "/tests", element: /* @__PURE__ */ jsx57(TestsMenu, {}) }),
2352
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/list", element: /* @__PURE__ */ jsx57(TestsList, {}) }),
2353
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/import", element: /* @__PURE__ */ jsx57(TestImport, {}) }),
2354
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/run", element: /* @__PURE__ */ jsx57(RunTest, {}) }),
2355
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/:testId", element: /* @__PURE__ */ jsx57(TestDetails, {}) }),
2356
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/:testId/run", element: /* @__PURE__ */ jsx57(TestDetailsRun, {}) }),
2357
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/:testId/lastRun", element: /* @__PURE__ */ jsx57(TestDetailsLastRun, {}) }),
2358
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/:testId/export", element: /* @__PURE__ */ jsx57(TestDetailsExport, {}) }),
2359
+ /* @__PURE__ */ jsx57(Route, { path: "/tests/:testId/open", element: /* @__PURE__ */ jsx57(TestDetailsOpen, {}) }),
2360
+ /* @__PURE__ */ jsx57(Route, { path: "/testruns", element: /* @__PURE__ */ jsx57(TestRunsMenu, {}) }),
2361
+ /* @__PURE__ */ jsx57(Route, { path: "/testruns/list", element: /* @__PURE__ */ jsx57(TestRunsList2, {}) }),
2362
+ /* @__PURE__ */ jsx57(Route, { path: "/testruns/:runId", element: /* @__PURE__ */ jsx57(TestRunDetails, {}) }),
2363
+ /* @__PURE__ */ jsx57(Route, { path: "/testruns/:runId/debugArtifacts", element: /* @__PURE__ */ jsx57(TestRunDebugArtifacts, {}) }),
2364
+ /* @__PURE__ */ jsx57(Route, { path: "/testruns/:runId/junitReport", element: /* @__PURE__ */ jsx57(TestRunJunitReport, {}) }),
2365
+ /* @__PURE__ */ jsx57(Route, { path: "/testruns/:runId/open", element: /* @__PURE__ */ jsx57(TestRunOpen, {}) }),
2366
+ /* @__PURE__ */ jsx57(Route, { path: "/suites", element: /* @__PURE__ */ jsx57(SuitesMenu, {}) }),
2367
+ /* @__PURE__ */ jsx57(Route, { path: "/suites/list", element: /* @__PURE__ */ jsx57(SuitesList, {}) }),
2368
+ /* @__PURE__ */ jsx57(Route, { path: "/suites/run", element: /* @__PURE__ */ jsx57(RunSuite, {}) }),
2369
+ /* @__PURE__ */ jsx57(Route, { path: "/suites/:suiteId", element: /* @__PURE__ */ jsx57(SuiteDetails, {}) }),
2370
+ /* @__PURE__ */ jsx57(Route, { path: "/suites/:suiteId/run", element: /* @__PURE__ */ jsx57(SuiteDetailsRun, {}) }),
2371
+ /* @__PURE__ */ jsx57(Route, { path: "/suites/:suiteId/lastRun", element: /* @__PURE__ */ jsx57(SuiteDetailsLastRun, {}) }),
2372
+ /* @__PURE__ */ jsx57(Route, { path: "/suites/:suiteId/open", element: /* @__PURE__ */ jsx57(SuiteDetailsOpen, {}) }),
2373
+ /* @__PURE__ */ jsx57(Route, { path: "/suiteruns", element: /* @__PURE__ */ jsx57(SuiteRunsMenu, {}) }),
2374
+ /* @__PURE__ */ jsx57(Route, { path: "/suiteruns/list", element: /* @__PURE__ */ jsx57(SuiteRunsList, {}) }),
2375
+ /* @__PURE__ */ jsx57(Route, { path: "/suiteruns/:runId", element: /* @__PURE__ */ jsx57(SuiteRunDetails, {}) }),
2376
+ /* @__PURE__ */ jsx57(Route, { path: "/suiteruns/:runId/showTestRuns", element: /* @__PURE__ */ jsx57(SuiteRunShowTestRuns, {}) }),
2377
+ /* @__PURE__ */ jsx57(Route, { path: "/suiteruns/:runId/open", element: /* @__PURE__ */ jsx57(SuiteRunOpen, {}) }),
2378
+ /* @__PURE__ */ jsx57(Route, { path: "/profiles", element: /* @__PURE__ */ jsx57(ProfilesMenu, {}) }),
2379
+ /* @__PURE__ */ jsx57(Route, { path: "/profiles/list", element: /* @__PURE__ */ jsx57(ProfilesList, {}) }),
2380
+ /* @__PURE__ */ jsx57(Route, { path: "/project", element: /* @__PURE__ */ jsx57(ProjectMenu, {}) }),
2381
+ /* @__PURE__ */ jsx57(Route, { path: "/project/switch", element: /* @__PURE__ */ jsx57(ProjectSwitch, {}) }),
2382
+ /* @__PURE__ */ jsx57(Route, { path: "/project/export", element: /* @__PURE__ */ jsx57(ProjectExport, {}) }),
2383
+ /* @__PURE__ */ jsx57(Route, { path: "/project/import", element: /* @__PURE__ */ jsx57(ProjectImport, {}) }),
2384
+ /* @__PURE__ */ jsx57(Route, { path: "/settings", element: /* @__PURE__ */ jsx57(SettingsMenu, {}) }),
2385
+ /* @__PURE__ */ jsx57(Route, { path: "/settings/setSessionToken", element: /* @__PURE__ */ jsx57(SettingsSetSessionToken, {}) }),
2386
+ /* @__PURE__ */ jsx57(Route, { path: "/logout", element: /* @__PURE__ */ jsx57(Logout, {}) })
2387
+ ] })
2388
+ ),
2389
+ { initialEntries, initialIndex: initialEntries.length - 1 }
2390
+ );
2391
+ let currentPathname = router.state.location.pathname;
2392
+ router.subscribe((state) => {
2393
+ const nextPathname = state.location.pathname;
2394
+ if (nextPathname === currentPathname) return;
2395
+ currentPathname = nextPathname;
2396
+ analytics.trackTuiNavigation(nextPathname);
2397
+ });
2398
+ return router;
2399
+ };
2400
+
2401
+ // src/app/NavigatorApp.tsx
2402
+ import { jsx as jsx58 } from "react/jsx-runtime";
2403
+ var NavigatorApp = ({ initialRoute, initialRouteState }) => {
2404
+ const [router] = useState17(() => createNavigatorRouter({ initialRoute, initialRouteState }));
2405
+ return /* @__PURE__ */ jsx58(NavigatorSessionProvider, { children: /* @__PURE__ */ jsx58(RouterProvider, { router }) });
2406
+ };
2407
+ export {
2408
+ NavigatorApp
2409
+ };
2410
+ //# sourceMappingURL=NavigatorApp-WSJMM3OT.js.map