@pokit/tabs-core 0.0.1 → 0.0.2

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,206 @@
1
+ /**
2
+ * State Reducer for Event-Driven CLI
3
+ *
4
+ * Builds a state tree from CLI events using a reducer pattern.
5
+ * Framework-agnostic - used by both Ink and OpenTUI adapters.
6
+ */
7
+
8
+ import type { CLIEvent, ActivityId, GroupId } from '@pokit/core';
9
+ import type { EventDrivenState, ActivityNode, GroupNode } from './types.js';
10
+
11
+ /**
12
+ * Create initial state
13
+ */
14
+ export function createInitialState(): EventDrivenState {
15
+ return {
16
+ appName: undefined,
17
+ version: undefined,
18
+ exitCode: undefined,
19
+ activities: new Map(),
20
+ groups: new Map(),
21
+ rootChildren: [],
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Reducer function for CLI events
27
+ */
28
+ export function reducer(state: EventDrivenState, event: CLIEvent): EventDrivenState {
29
+ switch (event.type) {
30
+ case 'root:start':
31
+ return {
32
+ ...state,
33
+ appName: event.appName,
34
+ version: event.version,
35
+ };
36
+
37
+ case 'root:end':
38
+ return {
39
+ ...state,
40
+ exitCode: event.exitCode,
41
+ };
42
+
43
+ case 'group:start': {
44
+ const newGroup: GroupNode = {
45
+ type: 'group',
46
+ id: event.id,
47
+ parentId: event.parentId,
48
+ label: event.label,
49
+ layout: event.layout,
50
+ children: [],
51
+ };
52
+
53
+ const newGroups = new Map(state.groups);
54
+ newGroups.set(event.id, newGroup);
55
+
56
+ if (event.parentId) {
57
+ const parentGroup = state.groups.get(event.parentId);
58
+ if (parentGroup) {
59
+ const updatedParent: GroupNode = {
60
+ ...parentGroup,
61
+ children: [...parentGroup.children, event.id],
62
+ };
63
+ newGroups.set(event.parentId, updatedParent);
64
+ }
65
+ return { ...state, groups: newGroups };
66
+ }
67
+
68
+ return {
69
+ ...state,
70
+ groups: newGroups,
71
+ rootChildren: [...state.rootChildren, event.id],
72
+ };
73
+ }
74
+
75
+ case 'group:end':
76
+ return state;
77
+
78
+ case 'activity:start': {
79
+ const newActivity: ActivityNode = {
80
+ type: 'activity',
81
+ id: event.id,
82
+ parentId: event.parentId,
83
+ label: event.label,
84
+ status: 'running',
85
+ meta: event.meta,
86
+ logs: [],
87
+ };
88
+
89
+ const newActivities = new Map(state.activities);
90
+ newActivities.set(event.id, newActivity);
91
+
92
+ if (event.parentId) {
93
+ const parentGroup = state.groups.get(event.parentId as GroupId);
94
+ if (parentGroup) {
95
+ const newGroups = new Map(state.groups);
96
+ const updatedParent: GroupNode = {
97
+ ...parentGroup,
98
+ children: [...parentGroup.children, event.id],
99
+ };
100
+ newGroups.set(event.parentId as GroupId, updatedParent);
101
+ return { ...state, activities: newActivities, groups: newGroups };
102
+ }
103
+ return { ...state, activities: newActivities };
104
+ }
105
+
106
+ return {
107
+ ...state,
108
+ activities: newActivities,
109
+ rootChildren: [...state.rootChildren, event.id],
110
+ };
111
+ }
112
+
113
+ case 'activity:update': {
114
+ const activity = state.activities.get(event.id);
115
+ if (!activity) return state;
116
+
117
+ const updatedActivity: ActivityNode = {
118
+ ...activity,
119
+ progress: event.payload.progress ?? activity.progress,
120
+ message: event.payload.message ?? activity.message,
121
+ };
122
+
123
+ const newActivities = new Map(state.activities);
124
+ newActivities.set(event.id, updatedActivity);
125
+
126
+ return { ...state, activities: newActivities };
127
+ }
128
+
129
+ case 'activity:success': {
130
+ const activity = state.activities.get(event.id);
131
+ if (!activity) return state;
132
+
133
+ const updatedActivity: ActivityNode = {
134
+ ...activity,
135
+ status: 'success',
136
+ };
137
+
138
+ const newActivities = new Map(state.activities);
139
+ newActivities.set(event.id, updatedActivity);
140
+
141
+ return { ...state, activities: newActivities };
142
+ }
143
+
144
+ case 'activity:failure': {
145
+ const activity = state.activities.get(event.id);
146
+ if (!activity) return state;
147
+
148
+ const errorMessage = event.error instanceof Error ? event.error.message : String(event.error);
149
+
150
+ const updatedActivity: ActivityNode = {
151
+ ...activity,
152
+ status: 'failure',
153
+ message: errorMessage,
154
+ };
155
+
156
+ const newActivities = new Map(state.activities);
157
+ newActivities.set(event.id, updatedActivity);
158
+
159
+ return { ...state, activities: newActivities };
160
+ }
161
+
162
+ case 'log': {
163
+ if (!event.activityId) return state;
164
+
165
+ const activity = state.activities.get(event.activityId);
166
+ if (!activity) return state;
167
+
168
+ const updatedActivity: ActivityNode = {
169
+ ...activity,
170
+ logs: [...activity.logs, { level: event.level, message: event.message }],
171
+ };
172
+
173
+ const newActivities = new Map(state.activities);
174
+ newActivities.set(event.activityId, updatedActivity);
175
+
176
+ return { ...state, activities: newActivities };
177
+ }
178
+
179
+ default:
180
+ return state;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get activities that belong to a tabs group
186
+ */
187
+ export function getTabsGroupActivities(state: EventDrivenState, groupId: GroupId): ActivityNode[] {
188
+ const group = state.groups.get(groupId);
189
+ if (!group || group.layout !== 'tabs') return [];
190
+
191
+ return group.children
192
+ .map((childId) => state.activities.get(childId as ActivityId))
193
+ .filter((activity): activity is ActivityNode => activity !== undefined);
194
+ }
195
+
196
+ /**
197
+ * Find the first tabs group in the state
198
+ */
199
+ export function findTabsGroup(state: EventDrivenState): GroupNode | undefined {
200
+ for (const group of state.groups.values()) {
201
+ if (group.layout === 'tabs') {
202
+ return group;
203
+ }
204
+ }
205
+ return undefined;
206
+ }
package/src/types.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Shared Types for CLI Tabs
3
+ *
4
+ * Framework-agnostic types used by both Ink and OpenTUI adapters.
5
+ */
6
+
7
+ import type { ActivityId, GroupId, GroupLayout } from '@pokit/core';
8
+
9
+ // =============================================================================
10
+ // Tab Process Types
11
+ // =============================================================================
12
+
13
+ export type TabStatus = 'running' | 'done' | 'error' | 'stopped';
14
+
15
+ export type TabProcess = {
16
+ id: string;
17
+ label: string;
18
+ exec: string;
19
+ output: string[];
20
+ status: TabStatus;
21
+ exitCode?: number;
22
+ };
23
+
24
+ export const MAX_OUTPUT_LINES = 10_000;
25
+ export const MAX_LINE_LENGTH = 5_000;
26
+ export const BUFFER_WARNING_THRESHOLD = 80; // Percentage
27
+
28
+ // =============================================================================
29
+ // Status Indicator Styling
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Status indicator configuration for tabs.
34
+ *
35
+ * Color scheme:
36
+ * - running: green (active process)
37
+ * - done: cyan (completed/inactive)
38
+ * - error: red (failed)
39
+ * - stopped: yellow (manually stopped)
40
+ */
41
+ export type StatusIndicator = {
42
+ color: string;
43
+ colorBright: string;
44
+ icon: string;
45
+ };
46
+
47
+ export const STATUS_INDICATORS: Record<TabStatus, StatusIndicator> = {
48
+ running: { color: '#00AA00', colorBright: '#00FF00', icon: '●' },
49
+ done: { color: '#00AAAA', colorBright: '#00FFFF', icon: '○' },
50
+ error: { color: '#AA0000', colorBright: '#FF0000', icon: '✗' },
51
+ stopped: { color: '#AAAA00', colorBright: '#FFFF00', icon: '■' },
52
+ };
53
+
54
+ export function getStatusIndicator(
55
+ status: TabStatus,
56
+ bright: boolean = false
57
+ ): { color: string; icon: string } {
58
+ const indicator = STATUS_INDICATORS[status];
59
+ return {
60
+ color: bright ? indicator.colorBright : indicator.color,
61
+ icon: indicator.icon,
62
+ };
63
+ }
64
+
65
+ // =============================================================================
66
+ // Event-Driven State Types
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Activity node in the state tree
71
+ */
72
+ export type ActivityNode = {
73
+ type: 'activity';
74
+ id: ActivityId;
75
+ parentId?: GroupId | ActivityId;
76
+ label: string;
77
+ status: 'running' | 'success' | 'failure';
78
+ progress?: number;
79
+ message?: string;
80
+ meta?: Record<string, unknown>;
81
+ logs: Array<{ level: string; message: string }>;
82
+ };
83
+
84
+ /**
85
+ * Group node in the state tree
86
+ */
87
+ export type GroupNode = {
88
+ type: 'group';
89
+ id: GroupId;
90
+ parentId?: GroupId;
91
+ label: string;
92
+ layout: GroupLayout;
93
+ children: Array<ActivityId | GroupId>;
94
+ };
95
+
96
+ /**
97
+ * Root state for the event-driven CLI
98
+ */
99
+ export type EventDrivenState = {
100
+ appName?: string;
101
+ version?: string;
102
+ exitCode?: number;
103
+ activities: Map<ActivityId, ActivityNode>;
104
+ groups: Map<GroupId, GroupNode>;
105
+ rootChildren: Array<ActivityId | GroupId>;
106
+ };