soja-cli 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +117 -0
  3. package/dist/application/chat.js +1 -0
  4. package/dist/application/filters.js +27 -0
  5. package/dist/application/import.js +1 -0
  6. package/dist/application/ports.js +1 -0
  7. package/dist/application/repository-root.js +18 -0
  8. package/dist/application/services/backup-service.js +93 -0
  9. package/dist/application/services/folder-service.js +116 -0
  10. package/dist/application/services/git-workflow-service.js +593 -0
  11. package/dist/application/services/index.js +24 -0
  12. package/dist/application/services/project-service.js +129 -0
  13. package/dist/application/services/session-service.js +52 -0
  14. package/dist/application/services/task-service.js +350 -0
  15. package/dist/application/services/update-service.js +60 -0
  16. package/dist/application/services/workspace-service.js +81 -0
  17. package/dist/application/timeline.js +83 -0
  18. package/dist/application/types.js +1 -0
  19. package/dist/application/validation.js +30 -0
  20. package/dist/bootstrap.js +91 -0
  21. package/dist/cli/commands/access.js +60 -0
  22. package/dist/cli/commands/account.js +203 -0
  23. package/dist/cli/commands/args.js +27 -0
  24. package/dist/cli/commands/backup.js +70 -0
  25. package/dist/cli/commands/chat.js +133 -0
  26. package/dist/cli/commands/dev.js +59 -0
  27. package/dist/cli/commands/folders.js +59 -0
  28. package/dist/cli/commands/git-ops.js +200 -0
  29. package/dist/cli/commands/import.js +48 -0
  30. package/dist/cli/commands/info.js +82 -0
  31. package/dist/cli/commands/project.js +108 -0
  32. package/dist/cli/commands/start.js +34 -0
  33. package/dist/cli/commands/sync.js +43 -0
  34. package/dist/cli/commands/task.js +309 -0
  35. package/dist/cli/commands/tui.js +43 -0
  36. package/dist/cli/commands/update.js +20 -0
  37. package/dist/cli/commands/workspace.js +55 -0
  38. package/dist/cli/index.js +103 -0
  39. package/dist/cli/output.js +56 -0
  40. package/dist/cli/prompt.js +28 -0
  41. package/dist/cli/runtime.js +43 -0
  42. package/dist/config/config.js +103 -0
  43. package/dist/config/credentials.js +68 -0
  44. package/dist/config/paths.js +18 -0
  45. package/dist/config/server.js +21 -0
  46. package/dist/config/version.js +7 -0
  47. package/dist/data/local/activity.js +33 -0
  48. package/dist/data/local/clock.js +1 -0
  49. package/dist/data/local/comments.js +27 -0
  50. package/dist/data/local/index.js +48 -0
  51. package/dist/data/local/projects.js +53 -0
  52. package/dist/data/local/tasks.js +104 -0
  53. package/dist/data/local/users.js +38 -0
  54. package/dist/data/local/workspaces.js +55 -0
  55. package/dist/data/remote/api-client.js +103 -0
  56. package/dist/data/remote/live.js +93 -0
  57. package/dist/data/repositories.js +1 -0
  58. package/dist/data/sync/chat.js +242 -0
  59. package/dist/data/sync/engine.js +459 -0
  60. package/dist/data/sync/importer.js +156 -0
  61. package/dist/data/sync/index.js +109 -0
  62. package/dist/data/sync/services.js +263 -0
  63. package/dist/data/sync/store.js +200 -0
  64. package/dist/database/client.js +76 -0
  65. package/dist/database/migrate.js +27 -0
  66. package/dist/database/migrations/0000_init.sql +100 -0
  67. package/dist/database/migrations/0001_git-operations.sql +19 -0
  68. package/dist/database/migrations/0002_merge-detection.sql +19 -0
  69. package/dist/database/migrations/0003_sync-replica.sql +40 -0
  70. package/dist/database/migrations/0004_chat-replica.sql +52 -0
  71. package/dist/database/migrations/0005_pr-events.sql +18 -0
  72. package/dist/database/migrations/0006_archive.sql +20 -0
  73. package/dist/database/migrations/meta/0000_snapshot.json +710 -0
  74. package/dist/database/migrations/meta/0001_snapshot.json +717 -0
  75. package/dist/database/migrations/meta/0002_snapshot.json +724 -0
  76. package/dist/database/migrations/meta/0003_snapshot.json +972 -0
  77. package/dist/database/migrations/meta/0004_snapshot.json +1168 -0
  78. package/dist/database/migrations/meta/0005_snapshot.json +1168 -0
  79. package/dist/database/migrations/meta/0006_snapshot.json +1183 -0
  80. package/dist/database/migrations/meta/_journal.json +55 -0
  81. package/dist/database/schema.js +170 -0
  82. package/dist/dev/seed.js +84 -0
  83. package/dist/domain/activity.js +22 -0
  84. package/dist/domain/chat.js +21 -0
  85. package/dist/domain/entities.js +1 -0
  86. package/dist/domain/errors.js +23 -0
  87. package/dist/domain/naming.js +33 -0
  88. package/dist/domain/task.js +140 -0
  89. package/dist/domain/workflow.js +23 -0
  90. package/dist/git/cli-git.js +363 -0
  91. package/dist/git/console.js +37 -0
  92. package/dist/git/diagnose.js +145 -0
  93. package/dist/git/merge-evidence.js +23 -0
  94. package/dist/git/releases.js +95 -0
  95. package/dist/git/types.js +16 -0
  96. package/dist/ui/App.js +71 -0
  97. package/dist/ui/Shell.js +211 -0
  98. package/dist/ui/app-state.js +213 -0
  99. package/dist/ui/branding/CompactLogo.js +8 -0
  100. package/dist/ui/branding/Logo.js +22 -0
  101. package/dist/ui/branding/Splash.js +21 -0
  102. package/dist/ui/branding/brand.js +23 -0
  103. package/dist/ui/chrome/Sidebar.js +50 -0
  104. package/dist/ui/chrome/StatusBar.js +26 -0
  105. package/dist/ui/chrome/Toast.js +11 -0
  106. package/dist/ui/chrome/TopBar.js +10 -0
  107. package/dist/ui/chrome/context.js +33 -0
  108. package/dist/ui/components/ConsoleLines.js +16 -0
  109. package/dist/ui/components/EmptyState.js +8 -0
  110. package/dist/ui/components/Labels.js +25 -0
  111. package/dist/ui/components/PullRequestLabel.js +27 -0
  112. package/dist/ui/components/TaskTable.js +41 -0
  113. package/dist/ui/components/TextField.js +11 -0
  114. package/dist/ui/components/TextInput.js +29 -0
  115. package/dist/ui/components/task-columns.js +27 -0
  116. package/dist/ui/copy.js +23 -0
  117. package/dist/ui/hooks/use-commands.js +58 -0
  118. package/dist/ui/hooks/use-flows.js +254 -0
  119. package/dist/ui/hooks/use-layout.js +36 -0
  120. package/dist/ui/hooks/use-list.js +28 -0
  121. package/dist/ui/hooks/use-measure.js +22 -0
  122. package/dist/ui/hooks/use-query.js +29 -0
  123. package/dist/ui/hooks/use-task-actions.js +450 -0
  124. package/dist/ui/hooks/use-terminal-size.js +19 -0
  125. package/dist/ui/input/KeyProvider.js +101 -0
  126. package/dist/ui/input/dispatcher.js +26 -0
  127. package/dist/ui/input/list-navigation.js +38 -0
  128. package/dist/ui/input/mouse.js +78 -0
  129. package/dist/ui/input/text-editing.js +62 -0
  130. package/dist/ui/kit/Badge.js +8 -0
  131. package/dist/ui/kit/Button.js +13 -0
  132. package/dist/ui/kit/Clickable.js +14 -0
  133. package/dist/ui/kit/Gauge.js +9 -0
  134. package/dist/ui/kit/Keycap.js +7 -0
  135. package/dist/ui/kit/Panel.js +30 -0
  136. package/dist/ui/kit/ScrollBar.js +14 -0
  137. package/dist/ui/kit/Spinner.js +9 -0
  138. package/dist/ui/kit/Tabs.js +12 -0
  139. package/dist/ui/mascot/AgentScene.js +30 -0
  140. package/dist/ui/mascot/Mascot.js +26 -0
  141. package/dist/ui/mascot/ansi.js +25 -0
  142. package/dist/ui/mascot/engine.js +146 -0
  143. package/dist/ui/mascot/index.js +6 -0
  144. package/dist/ui/mascot/palette.js +18 -0
  145. package/dist/ui/mascot/scenes/bugHunt.js +115 -0
  146. package/dist/ui/mascot/scenes/celebrate.js +76 -0
  147. package/dist/ui/mascot/scenes/cleaning.js +156 -0
  148. package/dist/ui/mascot/scenes/clockIn.js +105 -0
  149. package/dist/ui/mascot/scenes/deadline.js +125 -0
  150. package/dist/ui/mascot/scenes/empty.js +149 -0
  151. package/dist/ui/mascot/scenes/fire.js +138 -0
  152. package/dist/ui/mascot/scenes/index.js +25 -0
  153. package/dist/ui/mascot/scenes/planning.js +73 -0
  154. package/dist/ui/mascot/scenes/resting.js +90 -0
  155. package/dist/ui/mascot/scenes/syncing.js +116 -0
  156. package/dist/ui/mascot/scenes/working.js +193 -0
  157. package/dist/ui/mascot/selection.js +32 -0
  158. package/dist/ui/mascot/states.js +25 -0
  159. package/dist/ui/navigation/routes.js +17 -0
  160. package/dist/ui/overlays/CommitOverlay.js +93 -0
  161. package/dist/ui/overlays/ConfirmOverlay.js +42 -0
  162. package/dist/ui/overlays/GitLogOverlay.js +22 -0
  163. package/dist/ui/overlays/GitRunOverlay.js +94 -0
  164. package/dist/ui/overlays/NewTaskOverlay.js +117 -0
  165. package/dist/ui/overlays/OverlayFrame.js +35 -0
  166. package/dist/ui/overlays/PickerOverlay.js +72 -0
  167. package/dist/ui/overlays/PromptOverlay.js +51 -0
  168. package/dist/ui/overlays/SearchOverlay.js +49 -0
  169. package/dist/ui/overlays/options.js +29 -0
  170. package/dist/ui/overlays/types.js +1 -0
  171. package/dist/ui/screens/AccessScreen.js +91 -0
  172. package/dist/ui/screens/ChatScreen.js +450 -0
  173. package/dist/ui/screens/HelpScreen.js +99 -0
  174. package/dist/ui/screens/ProjectsScreen.js +66 -0
  175. package/dist/ui/screens/ScreenFrame.js +14 -0
  176. package/dist/ui/screens/SetupScreen.js +79 -0
  177. package/dist/ui/screens/TaskListScreen.js +152 -0
  178. package/dist/ui/screens/TaskScreen.js +193 -0
  179. package/dist/ui/screens/WelcomeScreen.js +254 -0
  180. package/dist/ui/screens/WorkspacesScreen.js +64 -0
  181. package/dist/ui/theme/detect.js +59 -0
  182. package/dist/ui/theme/theme.js +135 -0
  183. package/dist/utils/editor.js +37 -0
  184. package/dist/utils/errors.js +23 -0
  185. package/dist/utils/text.js +60 -0
  186. package/dist/utils/time.js +25 -0
  187. package/docs/NPM_PUBLISH.md +35 -0
  188. package/docs/PRIVACY.md +38 -0
  189. package/package.json +74 -0
@@ -0,0 +1,117 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useState } from 'react';
4
+ import { TASK_PRIORITIES, TASK_TYPES } from '../../domain/task.js';
5
+ import { useAppState } from '../app-state.js';
6
+ import { TextField } from '../components/TextField.js';
7
+ import { Button } from '../kit/Button.js';
8
+ import { Clickable } from '../kit/Clickable.js';
9
+ import { useQuery } from '../hooks/use-query.js';
10
+ import { Layer } from '../input/dispatcher.js';
11
+ import { useKeys } from '../input/KeyProvider.js';
12
+ import { palette, symbols, toneColors } from '../theme/theme.js';
13
+ import { memberOptions, priorityOptions, projectOptions, NONE, typeOptions, fromOption } from './options.js';
14
+ import { OverlayFrame } from './OverlayFrame.js';
15
+ import { completeFrom } from './PromptOverlay.js';
16
+ const FIELDS = ['title', 'project', 'type', 'priority', 'assignee', 'requester'];
17
+ const LABELS = {
18
+ title: 'Title',
19
+ project: 'Project',
20
+ type: 'Type',
21
+ priority: 'Priority',
22
+ assignee: 'Assignee',
23
+ requester: 'Requested by',
24
+ };
25
+ /**
26
+ * Quick capture. Only the title is required and Enter creates from any
27
+ * field, so "type title, Enter" is the whole flow when defaults are fine.
28
+ */
29
+ export function NewTaskOverlay({ spec }) {
30
+ const { services, session, closeOverlay, run } = useAppState();
31
+ const context = useQuery(async () => {
32
+ const [projects, members, requesters] = await Promise.all([
33
+ services.projects.list(session),
34
+ services.workspaces.members(session),
35
+ services.tasks.knownRequesters(session),
36
+ ]);
37
+ return { projects: projectOptions(projects), members: memberOptions(members, session.user.id), requesters };
38
+ }, `new-task:${session.workspace.id}`);
39
+ const [focus, setFocus] = useState('title');
40
+ const [title, setTitle] = useState('');
41
+ const [requester, setRequester] = useState('');
42
+ const [projectId, setProjectId] = useState(spec.projectId ?? NONE);
43
+ const [type, setType] = useState('feature');
44
+ const [priority, setPriority] = useState('medium');
45
+ const [assigneeId, setAssigneeId] = useState(session.user.id);
46
+ const [busy, setBusy] = useState(false);
47
+ const projects = context.data?.projects ?? [];
48
+ const members = context.data?.members ?? [];
49
+ const requesters = context.data?.requesters ?? [];
50
+ const completion = focus === 'requester' ? completeFrom(requester, requesters) : null;
51
+ const create = async () => {
52
+ if (busy)
53
+ return;
54
+ setBusy(true);
55
+ const created = await run(() => services.tasks.create(session, {
56
+ title,
57
+ projectId: fromOption(projectId),
58
+ type,
59
+ priority,
60
+ assigneeId: fromOption(assigneeId),
61
+ requester,
62
+ }), `Task created: ${title.trim()}`);
63
+ setBusy(false);
64
+ if (created)
65
+ closeOverlay(spec);
66
+ };
67
+ const cycle = (values, current, step) => values[(values.indexOf(current) + step + values.length) % values.length] ?? current;
68
+ const cycleOption = (options, current, step) => cycle(options.map((option) => option.value), current, step);
69
+ const moveFocus = (direction) => setFocus(cycle(FIELDS, focus, direction));
70
+ const step = (field, direction) => {
71
+ if (field === 'project')
72
+ setProjectId(cycleOption(projects, projectId, direction));
73
+ if (field === 'type')
74
+ setType(cycle(TASK_TYPES, type, direction));
75
+ if (field === 'priority')
76
+ setPriority(cycle([...TASK_PRIORITIES].reverse(), priority, direction));
77
+ if (field === 'assignee')
78
+ setAssigneeId(cycleOption(members, assigneeId, direction));
79
+ };
80
+ useKeys(Layer.overlay, (input, key) => {
81
+ if (key.escape)
82
+ closeOverlay(spec);
83
+ else if (key.return)
84
+ void create();
85
+ else if (key.tab && completion)
86
+ setRequester(completion);
87
+ else if ((key.tab && !key.shift) || key.downArrow)
88
+ moveFocus(1);
89
+ else if ((key.tab && key.shift) || key.upArrow)
90
+ moveFocus(-1);
91
+ else if (focus !== 'title' && focus !== 'requester' && (key.leftArrow || key.rightArrow || input === ' ' || input === 'h' || input === 'l')) {
92
+ step(focus, key.leftArrow || input === 'h' ? -1 : 1);
93
+ }
94
+ return true;
95
+ });
96
+ const option = (options, value) => options.find((candidate) => candidate.value === value);
97
+ /** `‹ Feature ›`: a value you change with ←/→ or by clicking it. */
98
+ const choice = (field, picked) => {
99
+ const focused = focus === field;
100
+ const color = picked?.tone ? toneColors(picked.tone).fg : picked?.dim ? palette.muted : palette.text;
101
+ return (_jsxs(Clickable, { onClick: () => (focused ? step(field, 1) : setFocus(field)), layer: Layer.overlay, children: [_jsxs(Text, { backgroundColor: focused ? palette.selection : palette.neutralSoft, children: [_jsx(Text, { color: focused ? palette.accent : palette.faint, children: ' ‹ ' }), _jsx(Text, { color: color, bold: focused, children: picked?.label ?? '—' }), _jsx(Text, { color: focused ? palette.accent : palette.faint, children: ' › ' })] }), picked?.hint && focused ? _jsx(Text, { color: palette.muted, children: ` ${picked.hint}` }) : null] }));
102
+ };
103
+ const rows = {
104
+ title: _jsx(TextField, { value: title, onChange: setTitle, active: focus === 'title', placeholder: "What needs to happen?", width: 46 }),
105
+ project: choice('project', option(projects, projectId)),
106
+ type: choice('type', option(typeOptions, type)),
107
+ priority: choice('priority', option(priorityOptions, priority)),
108
+ assignee: choice('assignee', option(members, assigneeId)),
109
+ requester: (_jsxs(Box, { children: [_jsx(TextField, { value: requester, onChange: setRequester, active: focus === 'requester', placeholder: requesters[0] ?? 'Marketing, Finance…', width: 30 }), completion ? _jsx(Text, { color: palette.muted, children: ` tab → ${completion}` }) : null] })),
110
+ };
111
+ return (_jsxs(OverlayFrame, { title: "New task", context: session.workspace.name, width: 70, hints: [
112
+ ['enter', 'create'],
113
+ ['tab ↓', 'next field'],
114
+ ['←→', 'change'],
115
+ ['esc', 'cancel'],
116
+ ], children: [FIELDS.map((field) => (_jsx(Clickable, { onClick: () => setFocus(field), layer: Layer.overlay, children: _jsxs(Box, { marginBottom: field === 'title' ? 1 : 0, flexGrow: 1, children: [_jsx(Box, { width: 15, flexShrink: 0, children: _jsxs(Text, { color: focus === field ? palette.accent : palette.muted, bold: focus === field, children: [focus === field ? `${symbols.pointer} ` : ' ', LABELS[field]] }) }), rows[field]] }) }, field))), _jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: palette.faint, children: "Only the title is required." }), _jsxs(Box, { gap: 2, children: [_jsx(Button, { label: "Cancel", onPress: () => closeOverlay(spec) }), _jsx(Button, { label: "Create task", variant: "primary", onPress: () => void create() })] })] })] }));
117
+ }
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useRef } from 'react';
4
+ import { useAppState } from '../app-state.js';
5
+ import { useMeasure } from '../hooks/use-measure.js';
6
+ import { useTerminalSize } from '../hooks/use-terminal-size.js';
7
+ import { useHints } from '../chrome/context.js';
8
+ import { Layer } from '../input/dispatcher.js';
9
+ import { Clickable } from '../kit/Clickable.js';
10
+ import { Panel } from '../kit/Panel.js';
11
+ import { palette } from '../theme/theme.js';
12
+ /**
13
+ * A floating window over the screen, centered, on a raised surface. The
14
+ * screen stays visible behind it; clicking outside closes it, like Esc.
15
+ */
16
+ export function OverlayFrame({ title, context, hints, width = 76, children }) {
17
+ useHints(hints);
18
+ const { closeOverlay } = useAppState();
19
+ const { columns, rows } = useTerminalSize();
20
+ const ref = useRef(null);
21
+ const { height } = useMeasure(ref);
22
+ const hasMeasured = height > 0;
23
+ const shown = Math.min(width, columns - 4);
24
+ const longContext = context !== undefined && context.length + title.length + 10 > shown;
25
+ const top = hasMeasured ? Math.max(1, Math.floor((rows - height) / 2)) : 2;
26
+ return (_jsxs(_Fragment, { children: [_jsx(Box, { position: "absolute", top: 0, left: 0, width: columns, height: rows, children: _jsx(Clickable, { onClick: () => closeOverlay(), layer: Layer.overlay, width: columns, flexGrow: 1, children: _jsx(Box, { width: columns, height: rows }) }) }), _jsx(Box, { ref: ref, position: "absolute", top: top, left: Math.max(0, Math.floor((columns - shown) / 2)), width: shown, flexDirection: "column", children: _jsx(Clickable, { onClick: () => undefined, layer: Layer.overlay, flexDirection: "column", children: _jsx(Panel, { title: title, aside: longContext ? undefined : context, focused: true, backgroundColor: palette.surface, width: shown, children: _jsxs(Box, { flexDirection: "column", paddingY: 0, children: [longContext ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: palette.muted, children: context }) })) : null, children] }) }) }) })] }));
27
+ }
28
+ /** Rows a window can give its list or content (the window floats with a margin). */
29
+ export function useOverlayHeight() {
30
+ return Math.max(3, useTerminalSize().rows - 10);
31
+ }
32
+ /** Width available inside a window of `width` columns. */
33
+ export function useOverlayWidth(width = 76) {
34
+ return Math.max(20, Math.min(width, useTerminalSize().columns - 4) - 4);
35
+ }
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useState } from 'react';
4
+ import { useAppState } from '../app-state.js';
5
+ import { TextField } from '../components/TextField.js';
6
+ import { useList } from '../hooks/use-list.js';
7
+ import { Layer } from '../input/dispatcher.js';
8
+ import { useKeys } from '../input/KeyProvider.js';
9
+ import { Clickable } from '../kit/Clickable.js';
10
+ import { ScrollBar } from '../kit/ScrollBar.js';
11
+ import { palette, symbols, toneColors } from '../theme/theme.js';
12
+ import { OverlayFrame, useOverlayHeight } from './OverlayFrame.js';
13
+ const CREATE = '__create__';
14
+ export function matchOptions(options, query) {
15
+ const needle = query.trim().toLowerCase();
16
+ if (!needle)
17
+ return [...options];
18
+ return options.filter((option) => `${option.label} ${option.hint ?? ''}`.toLowerCase().includes(needle));
19
+ }
20
+ export function PickerOverlay({ spec }) {
21
+ const { closeOverlay } = useAppState();
22
+ const [query, setQuery] = useState('');
23
+ const [busy, setBusy] = useState(false);
24
+ const height = Math.min(14, useOverlayHeight() - (spec.filterable ? 3 : 0));
25
+ const matches = matchOptions(spec.options, query);
26
+ const canCreate = Boolean(spec.create && query.trim() && matches.length === 0);
27
+ const items = canCreate && spec.create ? [{ value: CREATE, label: spec.create.label(query.trim()) }] : matches;
28
+ const initialIndex = Math.max(0, spec.options.findIndex((option) => option.value === spec.initial));
29
+ const list = useList(items.length, height, !spec.filterable, initialIndex);
30
+ const choose = async (option) => {
31
+ if (!option || busy)
32
+ return;
33
+ setBusy(true);
34
+ const result = option.value === CREATE && spec.create ? await spec.create.onCreate(query.trim()) : await spec.onSelect(option.value);
35
+ setBusy(false);
36
+ if (result !== false)
37
+ closeOverlay(spec);
38
+ };
39
+ useKeys(Layer.overlay, (input, key) => {
40
+ if (key.escape)
41
+ closeOverlay(spec);
42
+ else if (key.return)
43
+ void choose(items[list.index]);
44
+ else if (list.handleKey(input, key))
45
+ return true;
46
+ else if (!spec.filterable && /^[1-9]$/.test(input))
47
+ void choose(items[Number(input) - 1]);
48
+ return true; // modal: nothing below reacts while a picker is open
49
+ });
50
+ const visible = items.slice(list.offset, list.offset + height);
51
+ const labelWidth = Math.max(8, ...items.map((option) => option.label.length)) + 2;
52
+ const hintWidth = Math.max(0, ...items.map((option) => option.hint?.length ?? 0));
53
+ // Wide enough for the options and, when possible, for the title and context on one line.
54
+ const width = Math.min(96, Math.max(44, labelWidth + hintWidth + 14, (spec.context?.length ?? 0) + spec.title.length + 12));
55
+ return (_jsxs(OverlayFrame, { title: spec.title, context: spec.context, width: width, hints: [
56
+ ['↑↓', 'move'],
57
+ ['enter', 'select'],
58
+ ...(spec.filterable ? [] : [['1-9', 'pick']]),
59
+ ['esc', 'cancel'],
60
+ ], children: [spec.filterable ? (_jsx(Box, { marginBottom: 1, children: _jsx(TextField, { value: query, onChange: (next) => {
61
+ setQuery(next);
62
+ list.select(0);
63
+ }, placeholder: "Type to filter" }) })) : null, items.length === 0 ? _jsx(Text, { color: palette.faint, children: "No matches." }) : null, _jsxs(Box, { children: [_jsx(Clickable, { onWheel: (direction) => list.select(list.index + direction), layer: Layer.overlay, flexDirection: "column", flexGrow: 1, children: visible.map((option, position) => {
64
+ const index = list.offset + position;
65
+ const selected = index === list.index;
66
+ const current = option.value === spec.initial;
67
+ const bg = selected ? { backgroundColor: palette.selection } : {};
68
+ const tone = option.tone ? toneColors(option.tone).fg : undefined;
69
+ const color = option.value === CREATE ? palette.accent : option.dim ? palette.muted : (tone ?? palette.text);
70
+ return (_jsx(Clickable, { onClick: () => void choose(option), layer: Layer.overlay, children: _jsxs(Box, { flexGrow: 1, ...bg, children: [_jsx(Text, { color: palette.accent, ...bg, children: selected ? `${symbols.pointer} ` : ' ' }), !spec.filterable ? _jsx(Text, { color: palette.faint, ...bg, children: index < 9 ? `${index + 1} ` : ' ' }) : null, _jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: color, bold: selected, ...bg, children: option.label }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, children: option.hint ? (_jsx(Text, { color: palette.muted, wrap: "truncate-middle", ...bg, children: option.hint })) : null }), current ? _jsx(Text, { color: palette.accent, ...bg, children: ` ${symbols.check} ` }) : null] }) }, option.value));
71
+ }) }), _jsx(ScrollBar, { total: items.length, visible: height, offset: list.offset, height: Math.min(height, items.length) })] })] }));
72
+ }
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useState } from 'react';
4
+ import { useAppState } from '../app-state.js';
5
+ import { TextField } from '../components/TextField.js';
6
+ import { palette } from '../theme/theme.js';
7
+ import { Layer } from '../input/dispatcher.js';
8
+ import { useKeys } from '../input/KeyProvider.js';
9
+ import { OverlayFrame } from './OverlayFrame.js';
10
+ export function completeFrom(value, suggestions) {
11
+ const needle = value.trim().toLowerCase();
12
+ if (!needle)
13
+ return null;
14
+ const match = suggestions.find((suggestion) => suggestion.toLowerCase().startsWith(needle));
15
+ return match && match !== value ? match : null;
16
+ }
17
+ export function PromptOverlay({ spec }) {
18
+ const { closeOverlay, notify } = useAppState();
19
+ const [value, setValue] = useState(spec.initial ?? '');
20
+ const [busy, setBusy] = useState(false);
21
+ const suggestions = spec.suggestions ?? [];
22
+ const completion = completeFrom(value, suggestions);
23
+ const submit = async () => {
24
+ if (busy)
25
+ return;
26
+ if (!spec.allowEmpty && !value.trim()) {
27
+ notify('Type something first.', 'error');
28
+ return;
29
+ }
30
+ setBusy(true);
31
+ const result = await spec.onSubmit(value);
32
+ setBusy(false);
33
+ if (result !== false)
34
+ closeOverlay(spec);
35
+ };
36
+ useKeys(Layer.overlay, (_input, key) => {
37
+ if (key.escape)
38
+ closeOverlay(spec);
39
+ else if (key.return)
40
+ void submit();
41
+ else if (key.tab && completion)
42
+ setValue(completion);
43
+ return true;
44
+ });
45
+ const others = suggestions.filter((suggestion) => suggestion !== value).slice(0, 6);
46
+ return (_jsxs(OverlayFrame, { title: spec.title, context: spec.context, hints: [
47
+ ['enter', 'save'],
48
+ ...(suggestions.length ? [['tab', 'complete']] : []),
49
+ ['esc', 'cancel'],
50
+ ], children: [_jsx(TextField, { value: value, onChange: setValue, placeholder: spec.placeholder }), completion ? (_jsx(Text, { color: palette.muted, children: `tab → ${completion}` })) : others.length ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: palette.faint, wrap: "truncate-end", children: `known: ${others.join(', ')}` }) })) : null] }));
51
+ }
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useState } from 'react';
4
+ import { useAppState } from '../app-state.js';
5
+ import { EmptyState } from '../components/EmptyState.js';
6
+ import { TaskTable, tableRows } from '../components/TaskTable.js';
7
+ import { TextField } from '../components/TextField.js';
8
+ import { palette } from '../theme/theme.js';
9
+ import { EMPTY_SEARCH } from '../copy.js';
10
+ import { useList } from '../hooks/use-list.js';
11
+ import { useQuery } from '../hooks/use-query.js';
12
+ import { Layer } from '../input/dispatcher.js';
13
+ import { useKeys } from '../input/KeyProvider.js';
14
+ import { OverlayFrame, useOverlayHeight, useOverlayWidth } from './OverlayFrame.js';
15
+ const WIDTH = 104;
16
+ /** Live search by title or ID. Results update on every keystroke. */
17
+ export function SearchOverlay({ spec }) {
18
+ const { services, session, closeOverlay, go } = useAppState();
19
+ const width = useOverlayWidth(WIDTH);
20
+ const [query, setQuery] = useState('');
21
+ const results = useQuery(() => services.tasks.search(session, query, 100), `search:${session.workspace.id}:${query}`);
22
+ const tasks = results.data ?? [];
23
+ const tableHeight = Math.min(16, useOverlayHeight() - 3);
24
+ const open = (index) => {
25
+ const task = tasks[index];
26
+ if (!task)
27
+ return;
28
+ closeOverlay(spec);
29
+ go({ type: 'push', route: { name: 'task', ref: task.ref } });
30
+ };
31
+ const list = useList(tasks.length, tableRows(tableHeight), false);
32
+ useKeys(Layer.overlay, (input, key) => {
33
+ if (key.escape)
34
+ closeOverlay(spec);
35
+ else if (key.return)
36
+ open(list.index);
37
+ else
38
+ list.handleKey(input, key);
39
+ return true;
40
+ });
41
+ return (_jsxs(OverlayFrame, { title: "Search", context: "title or ID, closed and archived too", width: WIDTH, hints: [
42
+ ['↑/↓', 'move'],
43
+ ['enter', 'open'],
44
+ ['esc', 'close'],
45
+ ], children: [_jsx(Box, { marginBottom: 1, children: _jsx(TextField, { value: query, onChange: (next) => {
46
+ setQuery(next);
47
+ list.select(0);
48
+ }, placeholder: "stripe, SOJA-12\u2026" }) }), !query.trim() ? (_jsx(Text, { color: palette.faint, children: "Start typing: a word from the title, or a number like 12." })) : tasks.length === 0 && !results.loading ? (_jsx(EmptyState, { lines: EMPTY_SEARCH })) : (_jsx(TaskTable, { tasks: tasks, selected: list.index, offset: list.offset, height: tableHeight, width: width, layer: Layer.overlay, onSelect: (index) => list.select(index), onOpen: open, onScroll: (delta) => list.select(list.index + delta) }))] }));
49
+ }
@@ -0,0 +1,29 @@
1
+ import { TASK_PRIORITIES, TASK_STATUSES, TASK_TYPES, TYPE_LABELS } from '../../domain/task.js';
2
+ import { priorityStyles, priorityTone, statusStyles, statusTone } from '../theme/theme.js';
3
+ export const NONE = '__none__';
4
+ export const statusOptions = TASK_STATUSES.map((status) => {
5
+ const style = statusStyles[status];
6
+ return { value: status, label: `${style.glyph} ${style.label}`, tone: statusTone[status], dim: status === 'cancelled' };
7
+ });
8
+ export const priorityOptions = [...TASK_PRIORITIES].reverse().map((priority) => {
9
+ const style = priorityStyles[priority];
10
+ return { value: priority, label: priority === 'none' ? 'NONE' : style.label, tone: priorityTone[priority], dim: priority === 'none' };
11
+ });
12
+ export const typeOptions = TASK_TYPES.map((type) => ({ value: type, label: TYPE_LABELS[type] }));
13
+ export function memberOptions(members, meId) {
14
+ return [
15
+ ...members.map((member) => ({
16
+ value: member.id,
17
+ label: `@${member.username}`,
18
+ hint: member.id === meId ? `${member.displayName} (you)` : member.displayName,
19
+ })),
20
+ { value: NONE, label: 'Unassigned', dim: true },
21
+ ];
22
+ }
23
+ export function projectOptions(projects) {
24
+ return [
25
+ ...projects.map((project) => ({ value: project.id, label: project.name, hint: project.key })),
26
+ { value: NONE, label: 'No project', dim: true },
27
+ ];
28
+ }
29
+ export const fromOption = (value) => (value === NONE ? null : value);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { FileConfigStore } from '../../config/config.js';
5
+ import { CredentialStore } from '../../config/credentials.js';
6
+ import { resolvePaths } from '../../config/paths.js';
7
+ import { ApiClient } from '../../data/remote/api-client.js';
8
+ import { toDisplayError } from '../../utils/errors.js';
9
+ import { useLayout } from '../hooks/use-layout.js';
10
+ import { Layer } from '../input/dispatcher.js';
11
+ import { useKeys } from '../input/KeyProvider.js';
12
+ import { Badge } from '../kit/Badge.js';
13
+ import { Clickable } from '../kit/Clickable.js';
14
+ import { Panel } from '../kit/Panel.js';
15
+ import { palette, symbols } from '../theme/theme.js';
16
+ import { ScreenFrame } from './ScreenFrame.js';
17
+ export function AccessScreen({ active }) {
18
+ const { height, width } = useLayout();
19
+ const [requests, setRequests] = useState([]);
20
+ const [index, setIndex] = useState(0);
21
+ const [error, setError] = useState('');
22
+ const [busy, setBusy] = useState(false);
23
+ const api = useCallback(() => {
24
+ const remote = new FileConfigStore(resolvePaths().configFile).load()?.remote;
25
+ const token = remote ? new CredentialStore(resolvePaths().credentialsFile).token(remote.apiUrl) : null;
26
+ return remote && token ? new ApiClient(remote.apiUrl, token) : null;
27
+ }, []);
28
+ const refresh = useCallback(async () => {
29
+ const client = api();
30
+ if (!client) {
31
+ setError('La revisión de solicitudes requiere una sesión remota.');
32
+ return;
33
+ }
34
+ try {
35
+ setRequests((await client.get('/v1/admin/access-requests')).requests);
36
+ setError('');
37
+ }
38
+ catch (failure) {
39
+ setError(toDisplayError(failure).message);
40
+ }
41
+ }, [api]);
42
+ useEffect(() => {
43
+ if (!active)
44
+ return;
45
+ const timer = setTimeout(() => void refresh(), 0);
46
+ return () => clearTimeout(timer);
47
+ }, [active, refresh]);
48
+ const decide = async (decision) => {
49
+ const request = requests[index];
50
+ const client = api();
51
+ if (!request || !client || busy)
52
+ return;
53
+ setBusy(true);
54
+ try {
55
+ await client.post(`/v1/admin/access-requests/${request.id}/${decision}`);
56
+ await refresh();
57
+ setIndex(Math.min(index, Math.max(0, requests.length - 2)));
58
+ }
59
+ catch (failure) {
60
+ setError(toDisplayError(failure).message);
61
+ }
62
+ finally {
63
+ setBusy(false);
64
+ }
65
+ };
66
+ useKeys(Layer.screen, (input, key) => {
67
+ if (key.upArrow || input === 'k') {
68
+ setIndex((n) => Math.max(0, n - 1));
69
+ return true;
70
+ }
71
+ if (key.downArrow || input === 'j') {
72
+ setIndex((n) => Math.min(requests.length - 1, n + 1));
73
+ return true;
74
+ }
75
+ if (input === 'y') {
76
+ void decide('approve');
77
+ return true;
78
+ }
79
+ if (input === 'n') {
80
+ void decide('reject');
81
+ return true;
82
+ }
83
+ if (input === 'r') {
84
+ void refresh();
85
+ return true;
86
+ }
87
+ return false;
88
+ }, active);
89
+ const request = requests[index];
90
+ return _jsx(ScreenFrame, { title: "Access approvals", aside: `${requests.length} pending`, hints: [[`↑↓`, 'select'], ['y', 'approve'], ['n', 'reject'], ['r', 'refresh'], ['esc', 'back']], children: _jsxs(Box, { flexDirection: "row", gap: 1, height: Math.max(5, height - 3), children: [_jsxs(Panel, { title: "Pending requests", width: Math.max(28, Math.floor(width * 0.42)), children: [requests.length ? requests.map((item, position) => _jsx(Clickable, { active: active, onClick: () => setIndex(position), children: _jsxs(Box, { backgroundColor: position === index ? palette.selection : undefined, gap: 1, children: [_jsx(Text, { color: palette.accent, children: position === index ? symbols.pointer : ' ' }), _jsx(Text, { color: palette.text, children: `@${item.username}` }), _jsx(Badge, { tone: "warning", children: "pending" })] }) }, item.id)) : _jsx(Text, { color: palette.muted, children: error ? 'Could not load requests.' : 'No pending requests.' }), busy ? _jsx(Text, { color: palette.muted, children: "Saving decision\u2026" }) : null] }), _jsxs(Panel, { title: request ? request.displayName : 'Request details', flexGrow: 1, children: [error ? _jsx(Text, { color: palette.danger, children: error }) : null, request ? _jsxs(_Fragment, { children: [_jsx(Text, { color: palette.muted, children: `@${request.username} · GitHub ID ${request.githubId} · ${request.countryCode ?? 'country missing'}` }), _jsx(Text, { color: palette.muted, children: `Date of birth: ${request.dateOfBirth ?? 'not provided'} · ${request.submittedAt}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: palette.text, bold: true, children: "Interest letter" }) }), _jsx(Text, { color: palette.text, wrap: "wrap", children: request.letter }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: palette.success, children: "y approve" }), _jsx(Text, { color: palette.danger, children: "n reject" })] })] }) : null] })] }) });
91
+ }