aibos-design-system 1.0.0 → 1.0.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,231 @@
1
+ /**
2
+ * CLI Autocomplete Engine
3
+ *
4
+ * Context-aware suggestion engine that understands whether the user is:
5
+ * - Typing a Command Key (e.g., "sta" -> "status:")
6
+ * - Choosing a Value (e.g., "status:ac" -> "active ")
7
+ * - Entering an Operator (e.g., "score:>" or "score:<")
8
+ *
9
+ * See docs/CLI_FILTER_COMMANDS.md for the complete command specification.
10
+ */
11
+
12
+ import { COMMAND_SCHEMA, type ValidCommand } from './cli-commands';
13
+
14
+ export interface Suggestion {
15
+ label: string;
16
+ type: 'key' | 'value' | 'operator';
17
+ insertText: string;
18
+ description?: string;
19
+ }
20
+
21
+ export interface ContextInfo {
22
+ word: string;
23
+ isKey: boolean;
24
+ keyContext: ValidCommand | null;
25
+ colonIndex: number;
26
+ }
27
+
28
+ /**
29
+ * AutocompleteEngine
30
+ *
31
+ * Analyzes cursor position and input text to generate context-aware suggestions.
32
+ * The "Policeman" enforces valid commands and values.
33
+ */
34
+ export class AutocompleteEngine {
35
+ /**
36
+ * Get suggestions based on current input and cursor position
37
+ *
38
+ * @param fullText - Complete input text
39
+ * @param cursorIndex - Current cursor position (0-based)
40
+ * @returns Array of suggestions to display
41
+ */
42
+ getSuggestions(fullText: string, cursorIndex: number): Suggestion[] {
43
+ const context = this.parseContext(fullText, cursorIndex);
44
+
45
+ if (context.isKey) {
46
+ // MODE A: Suggesting Keys
47
+ return this.getSuggestionsForKeys(context.word);
48
+ } else if (context.keyContext) {
49
+ // MODE B: Suggesting Values or Operators
50
+ return this.getSuggestionsForValues(context.keyContext, context.word);
51
+ }
52
+
53
+ return [];
54
+ }
55
+
56
+ /**
57
+ * Get suggestions for command keys
58
+ *
59
+ * When user types "sta", suggest "status:", "stage:"
60
+ */
61
+ private getSuggestionsForKeys(prefix: string): Suggestion[] {
62
+ const validKeys = Object.keys(COMMAND_SCHEMA) as ValidCommand[];
63
+ const lower = prefix.toLowerCase();
64
+
65
+ return validKeys
66
+ .filter(key => key.toLowerCase().startsWith(lower))
67
+ .map((key): Suggestion => {
68
+ const schema = COMMAND_SCHEMA[key];
69
+ return {
70
+ label: key,
71
+ type: 'key' as const,
72
+ insertText: key + ':', // Auto-append colon
73
+ description: schema.description,
74
+ };
75
+ })
76
+ .sort((a, b) => a.label.localeCompare(b.label));
77
+ }
78
+
79
+ /**
80
+ * Get suggestions for values or operators
81
+ *
82
+ * When user types "status:ac", suggest "active "
83
+ * When user types "score:>", suggest "1", "100", etc.
84
+ */
85
+ private getSuggestionsForValues(
86
+ key: ValidCommand,
87
+ prefix: string
88
+ ): Suggestion[] {
89
+ const schema = COMMAND_SCHEMA[key];
90
+ if (!schema) return [];
91
+
92
+ // For ENUM types: Suggest matching values
93
+ if (schema.type === 'enum' && schema.values) {
94
+ const lower = prefix.toLowerCase();
95
+ return schema.values
96
+ .filter(val => val.toLowerCase().startsWith(lower))
97
+ .map(val => ({
98
+ label: val,
99
+ type: 'value',
100
+ insertText: val + ' ', // Auto-append space for next command
101
+ description: `${key}: ${val}`,
102
+ }));
103
+ }
104
+
105
+ // For NUMERIC types: Suggest operators first
106
+ if (schema.type === 'numeric' && schema.supportsOperators) {
107
+ const operators = ['>', '<', '=', '!=', '>=', '<='];
108
+ const lower = prefix.toLowerCase();
109
+
110
+ // If prefix is empty or starts with operator, suggest operators
111
+ if (!prefix || operators.some(op => op.startsWith(lower))) {
112
+ return operators
113
+ .filter(op => op.startsWith(lower))
114
+ .map(op => ({
115
+ label: `Operator: ${op}`,
116
+ type: 'operator',
117
+ insertText: op,
118
+ description: `${key}: ${op} [value]`,
119
+ }));
120
+ }
121
+
122
+ // If already has operator, suggest example values
123
+ return [
124
+ { label: '0', type: 'value', insertText: '0 ', description: 'Number value' },
125
+ { label: '10', type: 'value', insertText: '10 ', description: 'Number value' },
126
+ { label: '100', type: 'value', insertText: '100 ', description: 'Number value' },
127
+ ];
128
+ }
129
+
130
+ // For DATE types: Similar to numeric
131
+ if (schema.type === 'date' && schema.supportsOperators) {
132
+ const operators = ['>', '<', '='];
133
+ const lower = prefix.toLowerCase();
134
+
135
+ if (!prefix || operators.some(op => op.startsWith(lower))) {
136
+ return operators
137
+ .filter(op => op.startsWith(lower))
138
+ .map(op => ({
139
+ label: `Operator: ${op}`,
140
+ type: 'operator',
141
+ insertText: op,
142
+ description: `${key}: ${op} YYYY-MM-DD`,
143
+ }));
144
+ }
145
+
146
+ // Suggest date format
147
+ const today = new Date();
148
+ const isoToday = today.toISOString().split('T')[0];
149
+ return [
150
+ {
151
+ label: isoToday,
152
+ type: 'value',
153
+ insertText: isoToday + ' ',
154
+ description: 'Today',
155
+ },
156
+ ];
157
+ }
158
+
159
+ // For STRING types: No enum, accept any value
160
+ // Return empty to avoid spam, user can type freely
161
+ return [];
162
+ }
163
+
164
+ /**
165
+ * Parse context from full text and cursor position
166
+ *
167
+ * Determines:
168
+ * - Are we typing a Key or Value?
169
+ * - What's the current word?
170
+ * - If Value, which Key is it for?
171
+ */
172
+ parseContext(fullText: string, cursorIndex: number): ContextInfo {
173
+ // Get text up to cursor
174
+ const leftText = fullText.slice(0, cursorIndex);
175
+
176
+ // Find the start of the current token (after the last space)
177
+ const lastSpaceIndex = leftText.lastIndexOf(' ');
178
+ const currentToken = leftText.slice(lastSpaceIndex + 1);
179
+
180
+ // Check if token contains a colon
181
+ const colonIndex = currentToken.indexOf(':');
182
+
183
+ if (colonIndex === -1) {
184
+ // No colon found: we're typing a KEY
185
+ return {
186
+ word: currentToken,
187
+ isKey: true,
188
+ keyContext: null,
189
+ colonIndex: -1,
190
+ };
191
+ } else {
192
+ // Colon found: we're typing a VALUE
193
+ const key = currentToken.slice(0, colonIndex);
194
+ const valuePart = currentToken.slice(colonIndex + 1);
195
+
196
+ return {
197
+ word: valuePart,
198
+ isKey: false,
199
+ keyContext: key as ValidCommand, // Assume it's valid; schema will filter
200
+ colonIndex,
201
+ };
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Get the text to insert and the resulting cursor position
207
+ *
208
+ * This handles inserting the suggestion and positioning the cursor
209
+ * for the next edit.
210
+ */
211
+ getInsertionInfo(
212
+ fullText: string,
213
+ cursorIndex: number,
214
+ insertText: string
215
+ ): { newText: string; newCursorPos: number } {
216
+ // Find the start of the current token
217
+ const leftText = fullText.slice(0, cursorIndex);
218
+ const lastSpaceIndex = leftText.lastIndexOf(' ');
219
+ const tokenStart = lastSpaceIndex + 1;
220
+
221
+ // Build new text by replacing current token with insertText
222
+ const newText =
223
+ fullText.slice(0, tokenStart) +
224
+ insertText +
225
+ fullText.slice(cursorIndex);
226
+
227
+ const newCursorPos = tokenStart + insertText.length;
228
+
229
+ return { newText, newCursorPos };
230
+ }
231
+ }
@@ -0,0 +1,364 @@
1
+ /**
2
+ * CLI Filter Command Registry
3
+ *
4
+ * Centralized source of truth for valid filter keys, values, and behavior.
5
+ * Used by parser, autocomplete, and validation systems.
6
+ *
7
+ * See docs/CLI_FILTER_COMMANDS.md for the full specification.
8
+ */
9
+
10
+ export type CommandType = 'enum' | 'string' | 'numeric' | 'date' | 'boolean';
11
+
12
+ export interface CommandSchema {
13
+ type: CommandType;
14
+ description: string;
15
+ values?: string[]; // For enum types
16
+ supportsOperators?: boolean; // For numeric/date types
17
+ }
18
+
19
+ export type ValidCommand =
20
+ // Status & Lifecycle
21
+ | 'status'
22
+ | 'stage'
23
+ | 'priority'
24
+ | 'severity'
25
+ // People & Teams
26
+ | 'owner'
27
+ | 'assignee'
28
+ | 'reviewer'
29
+ | 'created-by'
30
+ | 'team'
31
+ // Classification
32
+ | 'type'
33
+ | 'component'
34
+ | 'area'
35
+ | 'tag'
36
+ // Metrics & Comparisons
37
+ | 'score'
38
+ | 'effort'
39
+ | 'duration'
40
+ | 'age'
41
+ | 'count'
42
+ // Time-Based
43
+ | 'created'
44
+ | 'updated'
45
+ | 'due'
46
+ | 'week'
47
+ | 'month'
48
+ // Flags & Booleans
49
+ | 'is'
50
+ | 'has'
51
+ | 'no';
52
+
53
+ /**
54
+ * Full command schema registry
55
+ *
56
+ * Defines the type, valid values, and behavior of each filter key.
57
+ * Update this when adding new filter commands to the system.
58
+ */
59
+ export const COMMAND_SCHEMA: Record<ValidCommand, CommandSchema> = {
60
+ // ============== Status & Lifecycle ==============
61
+ status: {
62
+ type: 'enum',
63
+ description: 'Item lifecycle status',
64
+ values: ['active', 'pending', 'completed', 'archived', 'paused'],
65
+ },
66
+ stage: {
67
+ type: 'enum',
68
+ description: 'Workflow stage',
69
+ values: ['backlog', 'todo', 'in-progress', 'in-review', 'done'],
70
+ },
71
+ priority: {
72
+ type: 'enum',
73
+ description: 'Priority level',
74
+ values: ['critical', 'high', 'medium', 'low', 'none'],
75
+ },
76
+ severity: {
77
+ type: 'enum',
78
+ description: 'Issue severity',
79
+ values: ['blocker', 'major', 'minor', 'trivial', 'info'],
80
+ },
81
+
82
+ // ============== People & Teams ==============
83
+ owner: {
84
+ type: 'string',
85
+ description: 'Person who owns the item',
86
+ },
87
+ assignee: {
88
+ type: 'string',
89
+ description: 'Person assigned to work on item',
90
+ },
91
+ reviewer: {
92
+ type: 'string',
93
+ description: 'Person reviewing the item',
94
+ },
95
+ 'created-by': {
96
+ type: 'string',
97
+ description: 'Person who created the item',
98
+ },
99
+ team: {
100
+ type: 'string',
101
+ description: 'Team responsible for item',
102
+ },
103
+
104
+ // ============== Classification ==============
105
+ type: {
106
+ type: 'enum',
107
+ description: 'Item type or category',
108
+ values: ['bug', 'feature', 'enhancement', 'task', 'spike', 'doc', 'refactor'],
109
+ },
110
+ component: {
111
+ type: 'string',
112
+ description: 'Component or module name',
113
+ },
114
+ area: {
115
+ type: 'string',
116
+ description: 'Area of codebase',
117
+ },
118
+ tag: {
119
+ type: 'string',
120
+ description: 'Custom tag or label',
121
+ },
122
+
123
+ // ============== Metrics & Comparisons ==============
124
+ score: {
125
+ type: 'numeric',
126
+ description: 'Numeric score (supports comparison operators)',
127
+ supportsOperators: true,
128
+ },
129
+ effort: {
130
+ type: 'numeric',
131
+ description: 'Effort estimate (1-10 scale)',
132
+ supportsOperators: true,
133
+ },
134
+ duration: {
135
+ type: 'numeric',
136
+ description: 'Duration in days/hours',
137
+ supportsOperators: true,
138
+ },
139
+ age: {
140
+ type: 'numeric',
141
+ description: 'Age in days',
142
+ supportsOperators: true,
143
+ },
144
+ count: {
145
+ type: 'numeric',
146
+ description: 'Count of items',
147
+ supportsOperators: true,
148
+ },
149
+
150
+ // ============== Time-Based ==============
151
+ created: {
152
+ type: 'date',
153
+ description: 'Creation date (YYYY-MM-DD format)',
154
+ supportsOperators: true,
155
+ },
156
+ updated: {
157
+ type: 'date',
158
+ description: 'Last update date',
159
+ supportsOperators: true,
160
+ },
161
+ due: {
162
+ type: 'date',
163
+ description: 'Due date',
164
+ },
165
+ week: {
166
+ type: 'enum',
167
+ description: 'Week relative to current',
168
+ values: ['current', 'next', 'last'],
169
+ },
170
+ month: {
171
+ type: 'enum',
172
+ description: 'Month relative to current',
173
+ values: ['current', 'next', 'last'],
174
+ },
175
+
176
+ // ============== Flags & Booleans ==============
177
+ is: {
178
+ type: 'enum',
179
+ description: 'State flags',
180
+ values: ['blocked', 'duplicate', 'blocker', 'breaking', 'hotfix'],
181
+ },
182
+ has: {
183
+ type: 'enum',
184
+ description: 'Presence check',
185
+ values: ['label', 'assignee', 'reviewer', 'dependencies'],
186
+ },
187
+ no: {
188
+ type: 'enum',
189
+ description: 'Absence check',
190
+ values: ['owner', 'reviewer', 'description'],
191
+ },
192
+ };
193
+
194
+ /**
195
+ * Get all valid command keys for autocomplete
196
+ */
197
+ export function getValidCommands(): ValidCommand[] {
198
+ return Object.keys(COMMAND_SCHEMA) as ValidCommand[];
199
+ }
200
+
201
+ /**
202
+ * Check if a key is a valid command
203
+ */
204
+ export function isValidCommand(key: string): key is ValidCommand {
205
+ return key in COMMAND_SCHEMA;
206
+ }
207
+
208
+ /**
209
+ * Get the schema for a specific command
210
+ */
211
+ export function getCommandSchema(key: string): CommandSchema | null {
212
+ if (!isValidCommand(key)) return null;
213
+ return COMMAND_SCHEMA[key];
214
+ }
215
+
216
+ /**
217
+ * Get valid values for an enum command
218
+ */
219
+ export function getCommandValues(key: string): string[] {
220
+ const schema = getCommandSchema(key);
221
+ if (schema?.type === 'enum' && schema.values) {
222
+ return schema.values;
223
+ }
224
+ return [];
225
+ }
226
+
227
+ /**
228
+ * Check if a command supports comparison operators
229
+ */
230
+ export function supportsOperators(key: string): boolean {
231
+ const schema = getCommandSchema(key);
232
+ return schema?.supportsOperators ?? false;
233
+ }
234
+
235
+ /**
236
+ * Autocomplete suggestions
237
+ *
238
+ * Returns matching keys based on partial input
239
+ * Used by autocomplete menu to suggest valid commands
240
+ */
241
+ export function autocompleteKeys(prefix: string): ValidCommand[] {
242
+ const lower = prefix.toLowerCase();
243
+ return getValidCommands().filter(key =>
244
+ key.toLowerCase().startsWith(lower)
245
+ );
246
+ }
247
+
248
+ /**
249
+ * Autocomplete values for a given key
250
+ *
251
+ * For enum commands, returns predefined values.
252
+ * For string commands, should be populated from data source (usernames, components, etc.)
253
+ * For numeric/date commands, returns example syntax.
254
+ */
255
+ export function autocompleteValues(key: string, context?: Record<string, any>): string[] {
256
+ const schema = getCommandSchema(key);
257
+
258
+ if (!schema) return [];
259
+
260
+ // Enum types: return predefined values
261
+ if (schema.type === 'enum' && schema.values) {
262
+ return schema.values;
263
+ }
264
+
265
+ // String types: return from context (usernames, components, etc.)
266
+ if (schema.type === 'string') {
267
+ const field = key === 'created-by' ? 'createdBy' : key;
268
+ if (context?.[field]) {
269
+ return Array.isArray(context[field])
270
+ ? context[field]
271
+ : [String(context[field])];
272
+ }
273
+ return [];
274
+ }
275
+
276
+ // Numeric types: return operator hints
277
+ if (schema.type === 'numeric') {
278
+ return ['>', '<', '=', '!=', '>=', '<='];
279
+ }
280
+
281
+ // Date types: return format hint
282
+ if (schema.type === 'date') {
283
+ return ['YYYY-MM-DD', '>', '<'];
284
+ }
285
+
286
+ return [];
287
+ }
288
+
289
+ /**
290
+ * Validation: Check if a value is valid for a command
291
+ *
292
+ * For enum types, checks against predefined list.
293
+ * For string types, always accepts (open-ended).
294
+ * For numeric types, validates number format.
295
+ * For date types, validates YYYY-MM-DD format.
296
+ */
297
+ export function isValidValue(key: string, value: string): boolean {
298
+ const schema = getCommandSchema(key);
299
+ if (!schema) return false;
300
+
301
+ switch (schema.type) {
302
+ case 'enum':
303
+ return schema.values?.includes(value) ?? false;
304
+
305
+ case 'string':
306
+ // All strings are valid
307
+ return true;
308
+
309
+ case 'numeric':
310
+ // Check if value (after stripping operators) is a valid number
311
+ const numMatch = value.match(/[><=!]+(\d+)/);
312
+ return numMatch ? !isNaN(Number(numMatch[1])) : !isNaN(Number(value));
313
+
314
+ case 'date':
315
+ // Validate YYYY-MM-DD format
316
+ return /^\d{4}-\d{2}-\d{2}$/.test(value) || /^[><=!]+\d{4}-\d{2}-\d{2}$/.test(value);
317
+
318
+ case 'boolean':
319
+ return value === 'true' || value === 'false';
320
+
321
+ default:
322
+ return true;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Get autocomplete suggestions for next word
328
+ *
329
+ * Analyzes current input and suggests:
330
+ * - Keys if none selected yet
331
+ * - Values if a key is selected
332
+ * - Operators if key supports them
333
+ */
334
+ export function getContextualSuggestions(
335
+ input: string,
336
+ position: number,
337
+ context?: Record<string, any>
338
+ ): { type: 'key' | 'value' | 'operator'; suggestions: string[] } {
339
+ // Simple heuristic: if input ends with ':', suggest values
340
+ if (input.slice(0, position).endsWith(':')) {
341
+ const keyMatch = input.slice(0, position).match(/([a-z-]+):$/);
342
+ if (keyMatch) {
343
+ const key = keyMatch[1];
344
+ return {
345
+ type: supportsOperators(key) ? 'operator' : 'value',
346
+ suggestions: autocompleteValues(key, context),
347
+ };
348
+ }
349
+ }
350
+
351
+ // Otherwise suggest keys
352
+ const partialKey = input.slice(0, position).split(/\s+/).pop() || '';
353
+ return {
354
+ type: 'key',
355
+ suggestions: autocompleteKeys(partialKey),
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Export schema for testing/debugging
361
+ */
362
+ export function dumpSchema(): object {
363
+ return COMMAND_SCHEMA;
364
+ }