@stigg/terminal 0.0.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +47 -0
  3. package/dist/api/client.d.ts +6 -0
  4. package/dist/api/client.js +48 -0
  5. package/dist/api/format-key.d.ts +7 -0
  6. package/dist/api/format-key.js +12 -0
  7. package/dist/api/graphql-client.d.ts +5 -0
  8. package/dist/api/graphql-client.js +44 -0
  9. package/dist/api/operations.d.ts +65 -0
  10. package/dist/api/operations.js +77 -0
  11. package/dist/api/types.d.ts +18 -0
  12. package/dist/api/types.js +1 -0
  13. package/dist/auth/callback-server.d.ts +14 -0
  14. package/dist/auth/callback-server.js +145 -0
  15. package/dist/auth/config.d.ts +2 -0
  16. package/dist/auth/config.js +24 -0
  17. package/dist/auth/oauth.d.ts +17 -0
  18. package/dist/auth/oauth.js +94 -0
  19. package/dist/auth/storage.d.ts +6 -0
  20. package/dist/auth/storage.js +34 -0
  21. package/dist/bin.d.ts +2 -0
  22. package/dist/bin.js +3 -0
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.js +79 -0
  25. package/dist/commands/dash.d.ts +1 -0
  26. package/dist/commands/dash.js +24 -0
  27. package/dist/commands/debug.d.ts +1 -0
  28. package/dist/commands/debug.js +53 -0
  29. package/dist/commands/env.d.ts +1 -0
  30. package/dist/commands/env.js +15 -0
  31. package/dist/commands/init.d.ts +13 -0
  32. package/dist/commands/init.js +59 -0
  33. package/dist/commands/mcp.d.ts +7 -0
  34. package/dist/commands/mcp.js +37 -0
  35. package/dist/commands/skills.d.ts +6 -0
  36. package/dist/commands/skills.js +48 -0
  37. package/dist/headless/host-agent.d.ts +19 -0
  38. package/dist/headless/host-agent.js +64 -0
  39. package/dist/headless/init-phase1.d.ts +9 -0
  40. package/dist/headless/init-phase1.js +173 -0
  41. package/dist/headless/init-phase2.d.ts +36 -0
  42. package/dist/headless/init-phase2.js +150 -0
  43. package/dist/headless/next-step-prompt.d.ts +7 -0
  44. package/dist/headless/next-step-prompt.js +25 -0
  45. package/dist/headless/options.d.ts +30 -0
  46. package/dist/headless/options.js +77 -0
  47. package/dist/headless/reporter.d.ts +16 -0
  48. package/dist/headless/reporter.js +41 -0
  49. package/dist/headless/setup.d.ts +29 -0
  50. package/dist/headless/setup.js +80 -0
  51. package/dist/launch/agent.d.ts +55 -0
  52. package/dist/launch/agent.js +134 -0
  53. package/dist/mcp/clients/base.d.ts +49 -0
  54. package/dist/mcp/clients/base.js +66 -0
  55. package/dist/mcp/clients/claude-code.d.ts +22 -0
  56. package/dist/mcp/clients/claude-code.js +120 -0
  57. package/dist/mcp/clients/claude-desktop.d.ts +9 -0
  58. package/dist/mcp/clients/claude-desktop.js +35 -0
  59. package/dist/mcp/clients/codex.d.ts +13 -0
  60. package/dist/mcp/clients/codex.js +113 -0
  61. package/dist/mcp/clients/cursor.d.ts +9 -0
  62. package/dist/mcp/clients/cursor.js +26 -0
  63. package/dist/mcp/clients/index.d.ts +7 -0
  64. package/dist/mcp/clients/index.js +27 -0
  65. package/dist/mcp/clients/mcp-remote.d.ts +11 -0
  66. package/dist/mcp/clients/mcp-remote.js +13 -0
  67. package/dist/mcp/clients/vscode.d.ts +17 -0
  68. package/dist/mcp/clients/vscode.js +84 -0
  69. package/dist/mcp/clients.d.ts +4 -0
  70. package/dist/mcp/clients.js +50 -0
  71. package/dist/mcp/config-merge.d.ts +9 -0
  72. package/dist/mcp/config-merge.js +51 -0
  73. package/dist/mcp/writer.d.ts +6 -0
  74. package/dist/mcp/writer.js +65 -0
  75. package/dist/setup/storage.d.ts +21 -0
  76. package/dist/setup/storage.js +34 -0
  77. package/dist/skills/install.d.ts +19 -0
  78. package/dist/skills/install.js +64 -0
  79. package/dist/types.d.ts +35 -0
  80. package/dist/types.js +1 -0
  81. package/dist/ui/components/Card.d.ts +11 -0
  82. package/dist/ui/components/Card.js +19 -0
  83. package/dist/ui/components/ContextRow.d.ts +11 -0
  84. package/dist/ui/components/ContextRow.js +8 -0
  85. package/dist/ui/components/Footer.d.ts +10 -0
  86. package/dist/ui/components/Footer.js +9 -0
  87. package/dist/ui/components/Header.d.ts +11 -0
  88. package/dist/ui/components/Header.js +6 -0
  89. package/dist/ui/components/JsonPreview.d.ts +13 -0
  90. package/dist/ui/components/JsonPreview.js +25 -0
  91. package/dist/ui/components/SectionTitle.d.ts +6 -0
  92. package/dist/ui/components/SectionTitle.js +5 -0
  93. package/dist/ui/hooks/useAsyncEffect.d.ts +3 -0
  94. package/dist/ui/hooks/useAsyncEffect.js +20 -0
  95. package/dist/ui/hooks/useResize.d.ts +4 -0
  96. package/dist/ui/hooks/useResize.js +20 -0
  97. package/dist/ui/hud.d.ts +15 -0
  98. package/dist/ui/hud.js +30 -0
  99. package/dist/ui/ink-theme.d.ts +2 -0
  100. package/dist/ui/ink-theme.js +34 -0
  101. package/dist/ui/intro/LogoView.d.ts +12 -0
  102. package/dist/ui/intro/LogoView.js +226 -0
  103. package/dist/ui/intro/MatrixIntro.d.ts +6 -0
  104. package/dist/ui/intro/MatrixIntro.js +80 -0
  105. package/dist/ui/intro/logo.d.ts +6 -0
  106. package/dist/ui/intro/logo.js +21 -0
  107. package/dist/ui/messages.d.ts +5 -0
  108. package/dist/ui/messages.js +5 -0
  109. package/dist/ui/screens/DashScreen.d.ts +6 -0
  110. package/dist/ui/screens/DashScreen.js +27 -0
  111. package/dist/ui/screens/DebugScreen.d.ts +6 -0
  112. package/dist/ui/screens/DebugScreen.js +39 -0
  113. package/dist/ui/screens/InitScreen.d.ts +6 -0
  114. package/dist/ui/screens/InitScreen.js +138 -0
  115. package/dist/ui/screens/MenuScreen.d.ts +7 -0
  116. package/dist/ui/screens/MenuScreen.js +38 -0
  117. package/dist/ui/state.d.ts +72 -0
  118. package/dist/ui/state.js +107 -0
  119. package/dist/ui/steps/AccountStep.d.ts +8 -0
  120. package/dist/ui/steps/AccountStep.js +42 -0
  121. package/dist/ui/steps/ApiKeyStep.d.ts +8 -0
  122. package/dist/ui/steps/ApiKeyStep.js +91 -0
  123. package/dist/ui/steps/ClientsStep.d.ts +10 -0
  124. package/dist/ui/steps/ClientsStep.js +69 -0
  125. package/dist/ui/steps/CredentialKindStep.d.ts +7 -0
  126. package/dist/ui/steps/CredentialKindStep.js +18 -0
  127. package/dist/ui/steps/EnvironmentStep.d.ts +8 -0
  128. package/dist/ui/steps/EnvironmentStep.js +37 -0
  129. package/dist/ui/steps/LoginStep.d.ts +7 -0
  130. package/dist/ui/steps/LoginStep.js +56 -0
  131. package/dist/ui/steps/SkillsStep.d.ts +7 -0
  132. package/dist/ui/steps/SkillsStep.js +7 -0
  133. package/dist/ui/steps/SummaryStep.d.ts +8 -0
  134. package/dist/ui/steps/SummaryStep.js +41 -0
  135. package/dist/ui/steps/WritingStep.d.ts +10 -0
  136. package/dist/ui/steps/WritingStep.js +96 -0
  137. package/dist/ui/theme.d.ts +53 -0
  138. package/dist/ui/theme.js +66 -0
  139. package/dist/ui/tui/App.d.ts +10 -0
  140. package/dist/ui/tui/App.js +51 -0
  141. package/dist/ui/tui/components/ContextStrip.d.ts +11 -0
  142. package/dist/ui/tui/components/ContextStrip.js +8 -0
  143. package/dist/ui/tui/components/TitleBar.d.ts +6 -0
  144. package/dist/ui/tui/components/TitleBar.js +18 -0
  145. package/dist/ui/tui/components/WizardChecklist.d.ts +7 -0
  146. package/dist/ui/tui/components/WizardChecklist.js +69 -0
  147. package/dist/ui/tui/hooks/keyboard-hints-utils.d.ts +26 -0
  148. package/dist/ui/tui/hooks/keyboard-hints-utils.js +69 -0
  149. package/dist/ui/tui/hooks/useKeyBindings.d.ts +14 -0
  150. package/dist/ui/tui/hooks/useKeyBindings.js +44 -0
  151. package/dist/ui/tui/hooks/useKeyboardHints.d.ts +13 -0
  152. package/dist/ui/tui/hooks/useKeyboardHints.js +38 -0
  153. package/dist/ui/tui/hooks/useStdoutDimensions.d.ts +8 -0
  154. package/dist/ui/tui/hooks/useStdoutDimensions.js +28 -0
  155. package/dist/ui/tui/primitives/BlinkingLabel.d.ts +19 -0
  156. package/dist/ui/tui/primitives/BlinkingLabel.js +25 -0
  157. package/dist/ui/tui/primitives/ConfirmPrompt.d.ts +16 -0
  158. package/dist/ui/tui/primitives/ConfirmPrompt.js +36 -0
  159. package/dist/ui/tui/primitives/KeyboardHintsBar.d.ts +2 -0
  160. package/dist/ui/tui/primitives/KeyboardHintsBar.js +8 -0
  161. package/dist/ui/tui/primitives/PickerMenu.d.ts +38 -0
  162. package/dist/ui/tui/primitives/PickerMenu.js +162 -0
  163. package/dist/ui/tui/primitives/PromptLabel.d.ts +6 -0
  164. package/dist/ui/tui/primitives/PromptLabel.js +6 -0
  165. package/dist/ui/tui/primitives/ScreenContainer.d.ts +39 -0
  166. package/dist/ui/tui/primitives/ScreenContainer.js +39 -0
  167. package/dist/ui/tui/primitives/Spinner.d.ts +7 -0
  168. package/dist/ui/tui/primitives/Spinner.js +18 -0
  169. package/dist/ui/tui/screens/DashScreen.d.ts +6 -0
  170. package/dist/ui/tui/screens/DashScreen.js +37 -0
  171. package/dist/ui/tui/screens/DebugScreen.d.ts +6 -0
  172. package/dist/ui/tui/screens/DebugScreen.js +48 -0
  173. package/dist/ui/tui/screens/EnvScreen.d.ts +6 -0
  174. package/dist/ui/tui/screens/EnvScreen.js +192 -0
  175. package/dist/ui/tui/screens/InitScreen.d.ts +9 -0
  176. package/dist/ui/tui/screens/InitScreen.js +102 -0
  177. package/dist/ui/tui/screens/MenuScreen.d.ts +7 -0
  178. package/dist/ui/tui/screens/MenuScreen.js +84 -0
  179. package/dist/ui/tui/start-tui.d.ts +11 -0
  180. package/dist/ui/tui/start-tui.js +72 -0
  181. package/dist/ui/tui/steps/AccountStep.d.ts +8 -0
  182. package/dist/ui/tui/steps/AccountStep.js +42 -0
  183. package/dist/ui/tui/steps/ApiKeyStep.d.ts +8 -0
  184. package/dist/ui/tui/steps/ApiKeyStep.js +53 -0
  185. package/dist/ui/tui/steps/ClientsStep.d.ts +10 -0
  186. package/dist/ui/tui/steps/ClientsStep.js +52 -0
  187. package/dist/ui/tui/steps/CredentialKindStep.d.ts +7 -0
  188. package/dist/ui/tui/steps/CredentialKindStep.js +18 -0
  189. package/dist/ui/tui/steps/EnvironmentStep.d.ts +8 -0
  190. package/dist/ui/tui/steps/EnvironmentStep.js +38 -0
  191. package/dist/ui/tui/steps/LoginStep.d.ts +8 -0
  192. package/dist/ui/tui/steps/LoginStep.js +79 -0
  193. package/dist/ui/tui/steps/SkillsStep.d.ts +7 -0
  194. package/dist/ui/tui/steps/SkillsStep.js +7 -0
  195. package/dist/ui/tui/steps/SummaryStep.d.ts +10 -0
  196. package/dist/ui/tui/steps/SummaryStep.js +133 -0
  197. package/dist/ui/tui/steps/WritingStep.d.ts +10 -0
  198. package/dist/ui/tui/steps/WritingStep.js +101 -0
  199. package/package.json +62 -0
@@ -0,0 +1,14 @@
1
+ import { type KeyBinding } from './keyboard-hints-utils.js';
2
+ interface UseKeyBindingsOptions {
3
+ isActive?: boolean;
4
+ }
5
+ /**
6
+ * Declarative key bindings + automatic hint registration.
7
+ *
8
+ * Each binding declares `{match, label, action, handler}`. We wire `useInput`
9
+ * to dispatch the matching binding's handler, AND register `{label, action,
10
+ * priority}` with the KeyboardHintsProvider so the bottom hints bar updates
11
+ * automatically when this component mounts/unmounts.
12
+ */
13
+ export declare function useKeyBindings(id: string, bindings: KeyBinding[], opts?: UseKeyBindingsOptions): void;
14
+ export {};
@@ -0,0 +1,44 @@
1
+ import { useInput } from 'ink';
2
+ import { useEffect, useMemo } from 'react';
3
+ import { defaultPriorityFor, matchesKey, } from './keyboard-hints-utils.js';
4
+ import { useKeyboardHintsContext } from './useKeyboardHints.js';
5
+ /**
6
+ * Declarative key bindings + automatic hint registration.
7
+ *
8
+ * Each binding declares `{match, label, action, handler}`. We wire `useInput`
9
+ * to dispatch the matching binding's handler, AND register `{label, action,
10
+ * priority}` with the KeyboardHintsProvider so the bottom hints bar updates
11
+ * automatically when this component mounts/unmounts.
12
+ */
13
+ export function useKeyBindings(id, bindings, opts = {}) {
14
+ const { register, unregister } = useKeyboardHintsContext();
15
+ const isActive = opts.isActive ?? true;
16
+ const hintsKey = JSON.stringify(bindings.map((b) => [b.label, b.action, b.priority]));
17
+ const hints = useMemo(() => bindings.map((b) => {
18
+ const firstMatch = Array.isArray(b.match) ? b.match[0] : b.match;
19
+ const priority = b.priority ?? (firstMatch !== undefined ? defaultPriorityFor(firstMatch) : undefined);
20
+ return { label: b.label, action: b.action, priority };
21
+ }),
22
+ // hintsKey captures the relevant shape of bindings — stable across renders
23
+ // when callers pass equivalent arrays.
24
+ [hintsKey]);
25
+ useEffect(() => {
26
+ if (!isActive) {
27
+ unregister(id);
28
+ return undefined;
29
+ }
30
+ register(id, hints);
31
+ return () => {
32
+ unregister(id);
33
+ };
34
+ }, [id, hints, isActive, register, unregister]);
35
+ useInput((input, key) => {
36
+ for (const b of bindings) {
37
+ const matches = Array.isArray(b.match) ? b.match : [b.match];
38
+ if (matches.some((m) => matchesKey(m, input, key))) {
39
+ b.handler(input, key);
40
+ return;
41
+ }
42
+ }
43
+ }, { isActive });
44
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { type KeyHint } from './keyboard-hints-utils.js';
3
+ interface KeyboardHintsContextValue {
4
+ register: (id: string, hints: KeyHint[]) => void;
5
+ unregister: (id: string) => void;
6
+ hints: KeyHint[];
7
+ }
8
+ interface ProviderProps {
9
+ children: React.ReactNode;
10
+ }
11
+ export declare function KeyboardHintsProvider({ children }: ProviderProps): React.ReactElement;
12
+ export declare function useKeyboardHintsContext(): KeyboardHintsContextValue;
13
+ export {};
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useMemo, useState } from 'react';
3
+ import { dedupeAndSortHints } from './keyboard-hints-utils.js';
4
+ const Ctx = createContext(null);
5
+ export function KeyboardHintsProvider({ children }) {
6
+ const [bag, setBag] = useState(() => new Map());
7
+ const register = useCallback((id, hints) => {
8
+ setBag((prev) => {
9
+ const next = new Map(prev);
10
+ next.set(id, hints);
11
+ return next;
12
+ });
13
+ }, []);
14
+ const unregister = useCallback((id) => {
15
+ setBag((prev) => {
16
+ if (!prev.has(id))
17
+ return prev;
18
+ const next = new Map(prev);
19
+ next.delete(id);
20
+ return next;
21
+ });
22
+ }, []);
23
+ const hints = useMemo(() => dedupeAndSortHints(bag), [bag]);
24
+ const value = useMemo(() => ({ register, unregister, hints }), [register, unregister, hints]);
25
+ return _jsx(Ctx.Provider, { value: value, children: children });
26
+ }
27
+ export function useKeyboardHintsContext() {
28
+ const ctx = useContext(Ctx);
29
+ if (!ctx) {
30
+ // Allow use outside provider for testing / non-TUI rendering.
31
+ return {
32
+ register: () => { },
33
+ unregister: () => { },
34
+ hints: [],
35
+ };
36
+ }
37
+ return ctx;
38
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Returns the terminal's [columns, rows] and re-renders on resize.
3
+ *
4
+ * Ink's built-in `useStdout` doesn't trigger re-renders on resize — this hook
5
+ * subscribes to the `resize` event on the underlying stream and forces a state
6
+ * update so the entire layout reflows.
7
+ */
8
+ export declare function useStdoutDimensions(): readonly [number, number];
@@ -0,0 +1,28 @@
1
+ import { useStdout } from 'ink';
2
+ import { useEffect, useState } from 'react';
3
+ /**
4
+ * Returns the terminal's [columns, rows] and re-renders on resize.
5
+ *
6
+ * Ink's built-in `useStdout` doesn't trigger re-renders on resize — this hook
7
+ * subscribes to the `resize` event on the underlying stream and forces a state
8
+ * update so the entire layout reflows.
9
+ */
10
+ export function useStdoutDimensions() {
11
+ const { stdout } = useStdout();
12
+ const [size, setSize] = useState(() => [
13
+ stdout?.columns ?? 80,
14
+ stdout?.rows ?? 24,
15
+ ]);
16
+ useEffect(() => {
17
+ if (!stdout)
18
+ return undefined;
19
+ const onResize = () => {
20
+ setSize([stdout.columns ?? 80, stdout.rows ?? 24]);
21
+ };
22
+ stdout.on('resize', onResize);
23
+ return () => {
24
+ stdout.off('resize', onResize);
25
+ };
26
+ }, [stdout]);
27
+ return size;
28
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ interface BlinkingLabelProps {
3
+ text: string;
4
+ intervalMs?: number;
5
+ color?: string;
6
+ dimColor?: boolean;
7
+ bold?: boolean;
8
+ /** Visual cell width to reserve. Lets callers pad past JS string length when
9
+ * the label contains emoji that render as 2 cells. */
10
+ padWidth?: number;
11
+ }
12
+ /**
13
+ * Hard blink — text alternates between fully visible and fully hidden.
14
+ * Width is preserved during the off-frame via space padding so neighbours
15
+ * don't reflow. Stays steady (no interval) when animations are disabled
16
+ * (CI / NO_COLOR / dumb terminal / non-TTY).
17
+ */
18
+ export declare function BlinkingLabel({ text, intervalMs, color, dimColor, bold, padWidth, }: BlinkingLabelProps): React.ReactElement;
19
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { useEffect, useState } from 'react';
4
+ import { Colors, shouldRenderAnimation } from '../../theme.js';
5
+ /**
6
+ * Hard blink — text alternates between fully visible and fully hidden.
7
+ * Width is preserved during the off-frame via space padding so neighbours
8
+ * don't reflow. Stays steady (no interval) when animations are disabled
9
+ * (CI / NO_COLOR / dumb terminal / non-TTY).
10
+ */
11
+ export function BlinkingLabel({ text, intervalMs = 600, color = Colors.accent, dimColor, bold, padWidth, }) {
12
+ const [visible, setVisible] = useState(true);
13
+ const animate = shouldRenderAnimation();
14
+ useEffect(() => {
15
+ if (!animate)
16
+ return undefined;
17
+ const id = setInterval(() => {
18
+ setVisible((v) => !v);
19
+ }, intervalMs);
20
+ return () => clearInterval(id);
21
+ }, [animate, intervalMs]);
22
+ const blankWidth = padWidth ?? text.length;
23
+ const display = visible ? text : ' '.repeat(blankWidth);
24
+ return (_jsx(Text, { color: color, dimColor: dimColor, bold: bold, children: display }));
25
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ interface ConfirmPromptProps {
3
+ id: string;
4
+ message?: string;
5
+ defaultAnswer?: 'yes' | 'no';
6
+ onConfirm: () => void;
7
+ onCancel: () => void;
8
+ }
9
+ /**
10
+ * y/n prompt. Enter triggers the default answer (yes by default). Esc is
11
+ * intentionally not bound here — it bubbles up to the wizard host so the
12
+ * standard "esc → back" navigation works on confirm-style steps too.
13
+ * Registers hints: `y yes`, `n no`, `enter <default>`.
14
+ */
15
+ export declare function ConfirmPrompt({ id, message, defaultAnswer, onConfirm, onCancel, }: ConfirmPromptProps): React.ReactElement;
16
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Colors } from '../../theme.js';
4
+ import { KeyMatch } from '../hooks/keyboard-hints-utils.js';
5
+ import { useKeyBindings } from '../hooks/useKeyBindings.js';
6
+ import { PromptLabel } from './PromptLabel.js';
7
+ /**
8
+ * y/n prompt. Enter triggers the default answer (yes by default). Esc is
9
+ * intentionally not bound here — it bubbles up to the wizard host so the
10
+ * standard "esc → back" navigation works on confirm-style steps too.
11
+ * Registers hints: `y yes`, `n no`, `enter <default>`.
12
+ */
13
+ export function ConfirmPrompt({ id, message, defaultAnswer = 'yes', onConfirm, onCancel, }) {
14
+ const bindings = [
15
+ {
16
+ match: 'y',
17
+ label: 'y',
18
+ action: 'yes',
19
+ handler: onConfirm,
20
+ },
21
+ {
22
+ match: 'n',
23
+ label: 'n',
24
+ action: 'no',
25
+ handler: onCancel,
26
+ },
27
+ {
28
+ match: KeyMatch.Return,
29
+ label: 'enter',
30
+ action: defaultAnswer,
31
+ handler: defaultAnswer === 'yes' ? onConfirm : onCancel,
32
+ },
33
+ ];
34
+ useKeyBindings(`confirm:${id}`, bindings);
35
+ return (_jsxs(Box, { flexDirection: "column", children: [message && _jsx(PromptLabel, { children: message }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: Colors.muted, children: `[${defaultAnswer === 'yes' ? 'Y/n' : 'y/N'}]` }), _jsx(Text, { dimColor: true, children: " \u2014 press y/n or enter" })] }) })] }));
36
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare function KeyboardHintsBar(): React.ReactElement;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Colors } from '../../theme.js';
4
+ import { useKeyboardHintsContext } from '../hooks/useKeyboardHints.js';
5
+ export function KeyboardHintsBar() {
6
+ const { hints } = useKeyboardHintsContext();
7
+ return (_jsx(Box, { height: 1, paddingX: 1, children: hints.map((hint, i) => (_jsxs(Box, { marginRight: i < hints.length - 1 ? 2 : 0, children: [_jsx(Text, { bold: true, color: Colors.muted, children: hint.label }), _jsxs(Text, { dimColor: true, children: [" ", hint.action] })] }, `${hint.label}-${hint.action}-${i}`))) }));
8
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ export interface PickerOption<T> {
3
+ label: string;
4
+ value: T;
5
+ /** Short inline trailing text (rendered same-line, dimmed). */
6
+ hint?: string;
7
+ /** Dimmed secondary line rendered under the label (e.g. a long path). */
8
+ description?: string;
9
+ disabled?: boolean;
10
+ }
11
+ interface BasePickerMenuProps<T> {
12
+ id: string;
13
+ message?: string;
14
+ options: PickerOption<T>[];
15
+ centered?: boolean;
16
+ optionMarginBottom?: number;
17
+ /** When set, render at most this many options at a time and scroll the
18
+ * viewport as focus moves out of view. */
19
+ visibleCount?: number;
20
+ }
21
+ interface SinglePickerMenuProps<T> extends BasePickerMenuProps<T> {
22
+ mode?: 'single';
23
+ defaultValue?: T;
24
+ /** When true, prefix each label with `[N] ` and bind number keys 1..9 to
25
+ * jump-select the matching option. */
26
+ numbered?: boolean;
27
+ onSelect: (value: T) => void;
28
+ onChange?: (value: T) => void;
29
+ }
30
+ interface MultiPickerMenuProps<T> extends BasePickerMenuProps<T> {
31
+ mode: 'multi';
32
+ defaultValue?: T[];
33
+ onSelect: (values: T[]) => void;
34
+ onChange?: (values: T[]) => void;
35
+ }
36
+ export type PickerMenuProps<T> = SinglePickerMenuProps<T> | MultiPickerMenuProps<T>;
37
+ export declare function PickerMenu<T>(props: PickerMenuProps<T>): React.ReactElement;
38
+ export {};
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useEffect, useState } from 'react';
4
+ import { Colors, Icons } from '../../theme.js';
5
+ import { KeyMatch } from '../hooks/keyboard-hints-utils.js';
6
+ import { useKeyBindings } from '../hooks/useKeyBindings.js';
7
+ import { PromptLabel } from './PromptLabel.js';
8
+ function findInitialIndex(options, defaultValue) {
9
+ if (defaultValue === undefined)
10
+ return 0;
11
+ const i = options.findIndex((o) => o.value === defaultValue);
12
+ return i >= 0 ? i : 0;
13
+ }
14
+ export function PickerMenu(props) {
15
+ const { id, message, options, centered, optionMarginBottom = 0, visibleCount } = props;
16
+ const mode = props.mode ?? 'single';
17
+ const [focused, setFocused] = useState(() => mode === 'single'
18
+ ? findInitialIndex(options, props.defaultValue)
19
+ : 0);
20
+ const windowSize = visibleCount && visibleCount > 0 ? Math.min(visibleCount, options.length) : options.length;
21
+ const [windowStart, setWindowStart] = useState(() => clampWindowStart(focused, windowSize, options.length));
22
+ useEffect(() => {
23
+ setWindowStart((start) => {
24
+ if (focused < start)
25
+ return focused;
26
+ if (focused >= start + windowSize)
27
+ return focused - windowSize + 1;
28
+ return clampWindowStart(start, windowSize, options.length);
29
+ });
30
+ }, [focused, windowSize, options.length]);
31
+ const [selected, setSelected] = useState(() => {
32
+ if (mode === 'multi') {
33
+ const dv = props.defaultValue;
34
+ return new Set(Array.isArray(dv) ? dv : []);
35
+ }
36
+ return new Set();
37
+ });
38
+ const move = (delta) => {
39
+ if (options.length === 0)
40
+ return;
41
+ setFocused((f) => {
42
+ let next = f;
43
+ for (let i = 0; i < options.length; i++) {
44
+ next = (next + delta + options.length) % options.length;
45
+ if (!options[next].disabled)
46
+ break;
47
+ }
48
+ const focusedOpt = options[next];
49
+ if (focusedOpt && mode === 'single') {
50
+ props.onChange?.(focusedOpt.value);
51
+ }
52
+ return next;
53
+ });
54
+ };
55
+ const toggleCurrent = () => {
56
+ const opt = options[focused];
57
+ if (!opt || opt.disabled)
58
+ return;
59
+ setSelected((prev) => {
60
+ const next = new Set(prev);
61
+ if (next.has(opt.value)) {
62
+ next.delete(opt.value);
63
+ }
64
+ else {
65
+ next.add(opt.value);
66
+ }
67
+ props.onChange?.(Array.from(next));
68
+ return next;
69
+ });
70
+ };
71
+ const submit = () => {
72
+ const opt = options[focused];
73
+ if (!opt || opt.disabled)
74
+ return;
75
+ if (mode === 'multi') {
76
+ props.onSelect(Array.from(selected));
77
+ }
78
+ else {
79
+ props.onSelect(opt.value);
80
+ }
81
+ };
82
+ const bindings = [
83
+ {
84
+ match: [KeyMatch.UpArrow, KeyMatch.DownArrow],
85
+ label: '↑↓',
86
+ action: 'navigate',
87
+ handler: (_input, key) => {
88
+ if (key.upArrow)
89
+ move(-1);
90
+ else if (key.downArrow)
91
+ move(1);
92
+ },
93
+ },
94
+ ];
95
+ if (mode === 'multi') {
96
+ bindings.push({
97
+ match: KeyMatch.Space,
98
+ label: 'space',
99
+ action: 'toggle',
100
+ handler: toggleCurrent,
101
+ });
102
+ bindings.push({
103
+ match: KeyMatch.Return,
104
+ label: 'enter',
105
+ action: 'confirm',
106
+ handler: submit,
107
+ });
108
+ }
109
+ else {
110
+ bindings.push({
111
+ match: KeyMatch.Return,
112
+ label: 'enter',
113
+ action: 'select',
114
+ handler: submit,
115
+ });
116
+ }
117
+ const numbered = mode === 'single' && props.numbered === true;
118
+ if (numbered && options.length > 0) {
119
+ const max = Math.min(options.length, 9);
120
+ const keys = Array.from({ length: max }, (_, i) => String(i + 1));
121
+ bindings.push({
122
+ match: keys,
123
+ label: max === 1 ? '1' : `1-${max}`,
124
+ action: 'select',
125
+ priority: 40,
126
+ handler: (input) => {
127
+ const idx = parseInt(input, 10) - 1;
128
+ const opt = options[idx];
129
+ if (!opt || opt.disabled)
130
+ return;
131
+ setFocused(idx);
132
+ props.onSelect(opt.value);
133
+ },
134
+ });
135
+ }
136
+ useKeyBindings(`picker:${id}`, bindings);
137
+ const windowEnd = Math.min(options.length, windowStart + windowSize);
138
+ const hiddenAbove = windowStart;
139
+ const hiddenBelow = options.length - windowEnd;
140
+ return (_jsxs(Box, { flexDirection: "column", alignItems: centered ? 'center' : undefined, children: [message && _jsx(PromptLabel, { children: message }), _jsxs(Box, { flexDirection: "column", marginTop: message ? 1 : 0, children: [hiddenAbove > 0 && (_jsx(Text, { dimColor: true, children: ` ▲ ${hiddenAbove} more above` })), options.slice(windowStart, windowEnd).map((opt, j) => {
141
+ const i = windowStart + j;
142
+ const isFocused = i === focused;
143
+ const isChecked = mode === 'multi' && selected.has(opt.value);
144
+ const indicatorGlyph = mode === 'multi'
145
+ ? isChecked
146
+ ? Icons.squareFilled
147
+ : Icons.squareOpen
148
+ : isFocused
149
+ ? Icons.triangleSmallRight
150
+ : ' ';
151
+ const isDisabled = opt.disabled === true;
152
+ const numberTag = numbered && i < 9 ? `[${i + 1}] ` : '';
153
+ const fullLabel = `${numberTag}${opt.label}`;
154
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: optionMarginBottom, children: [_jsxs(Box, { children: [_jsx(Text, { color: isFocused && !isDisabled ? Colors.accent : undefined, dimColor: !isFocused || isDisabled, children: indicatorGlyph }), _jsx(Text, { children: " " }), _jsx(Text, { color: isFocused && !isDisabled ? Colors.accent : undefined, bold: isFocused && !isDisabled, dimColor: !isFocused || isDisabled, children: fullLabel }), opt.hint && _jsx(Text, { dimColor: true, children: ` ${opt.hint}` })] }), opt.description && (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "truncate-end", children: opt.description }) }))] }, `${id}-opt-${i}`));
155
+ }), hiddenBelow > 0 && (_jsx(Text, { dimColor: true, children: ` ▼ ${hiddenBelow} more below` }))] })] }));
156
+ }
157
+ function clampWindowStart(start, size, total) {
158
+ if (total === 0 || size === 0)
159
+ return 0;
160
+ const max = Math.max(0, total - size);
161
+ return Math.min(Math.max(0, start), max);
162
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface PromptLabelProps {
3
+ children: React.ReactNode;
4
+ }
5
+ export declare function PromptLabel({ children }: PromptLabelProps): React.ReactElement;
6
+ export {};
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { Colors } from '../../theme.js';
4
+ export function PromptLabel({ children }) {
5
+ return (_jsx(Text, { bold: true, color: Colors.accent, children: children }));
6
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ interface ScreenContainerProps {
3
+ children: React.ReactNode;
4
+ /**
5
+ * Optional full-terminal-width content rendered between the TitleBar and
6
+ * the centered body. Receives the live terminal column count so chrome
7
+ * (e.g. the menu's Logo + particle tail) can size to the screen edge.
8
+ */
9
+ chrome?: (cols: number) => React.ReactNode;
10
+ }
11
+ /**
12
+ * Always-on shell that wraps every screen.
13
+ *
14
+ * outer column (full terminal width × full terminal height)
15
+ * → KeyboardHintsProvider
16
+ * → TitleBar (full terminal width — lime strip bleeds edge to edge)
17
+ * → spacer (1 row — keeps the chrome / body off the title bar)
18
+ * → chrome?(cols) (full-width, currently only the menu Logo)
19
+ * → body wrapper (column-flex, full width, alignItems="center")
20
+ * → inner column (width = clamped 80..120, paddingX=1)
21
+ * → children (overflow hidden)
22
+ * → spacer (1 row gap between content and the hints bar)
23
+ * → KeyboardHintsBar
24
+ *
25
+ * Two flex details are load-bearing here:
26
+ * 1. The outer body wrapper sets `flexDirection="column"` explicitly. Without
27
+ * it, Yoga defaults to row, and `alignItems="center"` would center the
28
+ * inner column vertically instead of horizontally — pinning the body to
29
+ * the left edge and pushing it to the vertical middle of the screen.
30
+ * 2. Clamping the inner column while leaving the outer wrapper full-width is
31
+ * what produces the "body centered in the middle of the terminal" effect
32
+ * — empty gutters appear on both sides in wide terminals.
33
+ *
34
+ * No `flexGrow` on the body so it stays at its natural height and the
35
+ * KeyboardHintsBar tucks directly under the content. Leftover vertical space
36
+ * falls below the hints bar at the bottom of the terminal.
37
+ */
38
+ export declare function ScreenContainer({ children, chrome }: ScreenContainerProps): React.ReactElement;
39
+ export {};
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { MAX_SHELL_WIDTH, MIN_SHELL_WIDTH } from '../../theme.js';
4
+ import { TitleBar } from '../components/TitleBar.js';
5
+ import { useStdoutDimensions } from '../hooks/useStdoutDimensions.js';
6
+ import { KeyboardHintsProvider } from '../hooks/useKeyboardHints.js';
7
+ import { KeyboardHintsBar } from './KeyboardHintsBar.js';
8
+ /**
9
+ * Always-on shell that wraps every screen.
10
+ *
11
+ * outer column (full terminal width × full terminal height)
12
+ * → KeyboardHintsProvider
13
+ * → TitleBar (full terminal width — lime strip bleeds edge to edge)
14
+ * → spacer (1 row — keeps the chrome / body off the title bar)
15
+ * → chrome?(cols) (full-width, currently only the menu Logo)
16
+ * → body wrapper (column-flex, full width, alignItems="center")
17
+ * → inner column (width = clamped 80..120, paddingX=1)
18
+ * → children (overflow hidden)
19
+ * → spacer (1 row gap between content and the hints bar)
20
+ * → KeyboardHintsBar
21
+ *
22
+ * Two flex details are load-bearing here:
23
+ * 1. The outer body wrapper sets `flexDirection="column"` explicitly. Without
24
+ * it, Yoga defaults to row, and `alignItems="center"` would center the
25
+ * inner column vertically instead of horizontally — pinning the body to
26
+ * the left edge and pushing it to the vertical middle of the screen.
27
+ * 2. Clamping the inner column while leaving the outer wrapper full-width is
28
+ * what produces the "body centered in the middle of the terminal" effect
29
+ * — empty gutters appear on both sides in wide terminals.
30
+ *
31
+ * No `flexGrow` on the body so it stays at its natural height and the
32
+ * KeyboardHintsBar tucks directly under the content. Leftover vertical space
33
+ * falls below the hints bar at the bottom of the terminal.
34
+ */
35
+ export function ScreenContainer({ children, chrome }) {
36
+ const [cols, rows] = useStdoutDimensions();
37
+ const clampedWidth = Math.min(MAX_SHELL_WIDTH, Math.max(MIN_SHELL_WIDTH, cols));
38
+ return (_jsx(Box, { flexDirection: "column", height: rows, width: cols, children: _jsxs(KeyboardHintsProvider, { children: [_jsx(TitleBar, { width: cols }), _jsx(Box, { height: 1 }), chrome && (_jsxs(_Fragment, { children: [_jsx(Box, { flexShrink: 0, overflow: "hidden", children: chrome(cols) }), _jsx(Box, { height: 1 })] })), _jsx(Box, { width: cols, flexDirection: "column", alignItems: "center", flexShrink: 1, children: _jsxs(Box, { flexDirection: "column", width: clampedWidth, paddingX: 1, children: [_jsx(Box, { flexDirection: "column", overflow: "hidden", children: children }), _jsx(Box, { height: 2 }), _jsx(KeyboardHintsBar, {})] }) })] }) }));
39
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface SpinnerProps {
3
+ label?: string;
4
+ intervalMs?: number;
5
+ }
6
+ export declare function Spinner({ label, intervalMs }: SpinnerProps): React.ReactElement;
7
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { useEffect, useState } from 'react';
4
+ import { Colors, shouldRenderAnimation } from '../../theme.js';
5
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ export function Spinner({ label, intervalMs = 80 }) {
7
+ const [i, setI] = useState(0);
8
+ const animate = shouldRenderAnimation();
9
+ useEffect(() => {
10
+ if (!animate)
11
+ return undefined;
12
+ const id = setInterval(() => {
13
+ setI((n) => (n + 1) % FRAMES.length);
14
+ }, intervalMs);
15
+ return () => clearInterval(id);
16
+ }, [animate, intervalMs]);
17
+ return (_jsxs(Text, { children: [_jsx(Text, { color: Colors.accent, children: FRAMES[i] }), label ? ` ${label}` : ''] }));
18
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ onDone: () => void;
4
+ }
5
+ export declare function DashScreen({ onDone }: Props): React.ReactElement;
6
+ export {};
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import open from 'open';
4
+ import { useEffect, useState } from 'react';
5
+ import { Colors } from '../../theme.js';
6
+ import { KeyMatch } from '../hooks/keyboard-hints-utils.js';
7
+ import { useKeyBindings } from '../hooks/useKeyBindings.js';
8
+ import { PromptLabel } from '../primitives/PromptLabel.js';
9
+ const DASHBOARD_URL = 'https://app.stigg.io';
10
+ export function DashScreen({ onDone }) {
11
+ const [phase, setPhase] = useState('opening');
12
+ useEffect(() => {
13
+ let cancelled = false;
14
+ open(DASHBOARD_URL)
15
+ .then(() => {
16
+ if (!cancelled)
17
+ setPhase('launched');
18
+ })
19
+ .catch(() => {
20
+ if (!cancelled)
21
+ setPhase('failed');
22
+ });
23
+ return () => {
24
+ cancelled = true;
25
+ };
26
+ }, []);
27
+ const bindings = [
28
+ {
29
+ match: [KeyMatch.Return, KeyMatch.Escape],
30
+ label: 'enter',
31
+ action: 'menu',
32
+ handler: onDone,
33
+ },
34
+ ];
35
+ useKeyBindings('dash', bindings);
36
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PromptLabel, { children: "dash \u00B7 open Stigg dashboard" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Opening ", _jsx(Text, { color: Colors.accent, children: DASHBOARD_URL }), " in your browser\u2026"] }) }), _jsxs(Box, { marginTop: 1, children: [phase === 'opening' && _jsx(Text, { dimColor: true, children: "(launching)" }), phase === 'launched' && _jsx(Text, { color: Colors.success, children: "\u2713 Browser launched." }), phase === 'failed' && (_jsx(Text, { color: Colors.error, children: "Couldn't open a browser automatically \u2014 open the URL above manually." }))] })] }));
37
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ interface Props {
3
+ onDone: () => void;
4
+ }
5
+ export declare function DebugScreen({ onDone }: Props): React.ReactElement;
6
+ export {};