@compilr-dev/cli 0.6.6 → 0.7.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.
Files changed (35) hide show
  1. package/dist/commands-v2/handlers/core.js +10 -1
  2. package/dist/commands-v2/handlers/index.d.ts +1 -0
  3. package/dist/commands-v2/handlers/index.js +3 -0
  4. package/dist/commands-v2/handlers/settings.js +21 -5
  5. package/dist/commands-v2/handlers/skill.d.ts +10 -0
  6. package/dist/commands-v2/handlers/skill.js +63 -0
  7. package/dist/commands-v2/index.d.ts +1 -1
  8. package/dist/commands-v2/index.js +1 -1
  9. package/dist/commands-v2/registry.d.ts +4 -0
  10. package/dist/commands-v2/registry.js +19 -0
  11. package/dist/compilr-diff-companion.vsix +0 -0
  12. package/dist/index.js +8 -12
  13. package/dist/repl-helpers.d.ts +29 -1
  14. package/dist/repl-helpers.js +77 -7
  15. package/dist/repl-v2.js +29 -3
  16. package/dist/tools/delegation-status.d.ts +3 -12
  17. package/dist/tools/delegation-status.js +4 -123
  18. package/dist/tools/handoff.d.ts +9 -17
  19. package/dist/tools/handoff.js +28 -48
  20. package/dist/ui/conversation.js +1 -2
  21. package/dist/ui/markdown-renderer.d.ts +43 -0
  22. package/dist/ui/markdown-renderer.js +474 -0
  23. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +1 -2
  24. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1 -2
  25. package/dist/ui/overlay/impl/help-overlay-v2.d.ts +7 -1
  26. package/dist/ui/overlay/impl/help-overlay-v2.js +19 -2
  27. package/dist/ui/overlay/impl/skill-detail-overlay-v2.d.ts +91 -0
  28. package/dist/ui/overlay/impl/skill-detail-overlay-v2.js +863 -0
  29. package/dist/ui/overlay/impl/skill-editor-overlay.d.ts +56 -0
  30. package/dist/ui/overlay/impl/skill-editor-overlay.js +493 -0
  31. package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +83 -0
  32. package/dist/ui/overlay/impl/skills-overlay-v2.js +1095 -0
  33. package/dist/utils/skill-paths.d.ts +21 -0
  34. package/dist/utils/skill-paths.js +44 -0
  35. package/package.json +9 -6
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Skill Editor Overlay
3
+ *
4
+ * Fullscreen raw markdown editor for SKILL.md files.
5
+ * Uses alternate screen buffer (same pattern as DocumentDetailOverlayV2).
6
+ *
7
+ * Features:
8
+ * - Raw markdown editing with syntax highlighting
9
+ * - Line numbers, cursor, scroll
10
+ * - Ctrl+S saves to disk + validates + shows warnings
11
+ * - Dirty check with save prompt on exit
12
+ */
13
+ import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
14
+ import type { RenderContext, OverlayAction, KeyEvent } from '../types.js';
15
+ interface SkillEditorState {
16
+ rawLines: string[];
17
+ cursorLine: number;
18
+ cursorColumn: number;
19
+ scrollOffset: number;
20
+ isDirty: boolean;
21
+ originalContent: string;
22
+ showSavePrompt: boolean;
23
+ statusMessage: string | null;
24
+ statusType: 'success' | 'warning' | 'error' | null;
25
+ }
26
+ export interface SkillEditorResult {
27
+ saved: boolean;
28
+ }
29
+ export declare class SkillEditorOverlay extends BaseOverlayV2<SkillEditorState, SkillEditorResult> {
30
+ readonly type: "inline";
31
+ readonly id = "skill-editor-overlay";
32
+ readonly usesAlternateScreen = true;
33
+ private readonly filePath;
34
+ private readonly skillName;
35
+ constructor(filePath: string, skillName: string, content: string);
36
+ onMount(): void;
37
+ onUnmount(): void;
38
+ protected renderContent(context: RenderContext): string[];
39
+ private renderCursorLine;
40
+ private ensureCursorVisible;
41
+ handleKey(key: KeyEvent): OverlayAction<SkillEditorResult>;
42
+ private handleSavePrompt;
43
+ private save;
44
+ private currentLine;
45
+ private moveCursorUp;
46
+ private moveCursorDown;
47
+ private moveCursorLeft;
48
+ private moveCursorRight;
49
+ private pageUp;
50
+ private pageDown;
51
+ private insertText;
52
+ private insertNewline;
53
+ private handleBackspace;
54
+ private handleDelete;
55
+ }
56
+ export {};
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Skill Editor Overlay
3
+ *
4
+ * Fullscreen raw markdown editor for SKILL.md files.
5
+ * Uses alternate screen buffer (same pattern as DocumentDetailOverlayV2).
6
+ *
7
+ * Features:
8
+ * - Raw markdown editing with syntax highlighting
9
+ * - Line numbers, cursor, scroll
10
+ * - Ctrl+S saves to disk + validates + shows warnings
11
+ * - Dirty check with save prompt on exit
12
+ */
13
+ import { promises as fs } from 'node:fs';
14
+ import chalk from 'chalk';
15
+ import { ttyWrite } from '../../terminal.js';
16
+ import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
17
+ import { renderBorder, isCtrlC, isUpArrow, isDownArrow, isLeftArrow, isRightArrow, isHome, isEnd, isPageUp, isPageDown, isBackspace, isDelete, isEnter, isTab, isEscape, getPrintableChar, isPrintable, } from '../../base/index.js';
18
+ import * as terminal from '../../terminal.js';
19
+ import { getCurrentTheme } from '../../../themes/index.js';
20
+ import { parseSkillMarkdown } from '@compilr-dev/sdk';
21
+ // =============================================================================
22
+ // Alternate Screen
23
+ // =============================================================================
24
+ let inAlternateScreen = false;
25
+ function enterAlternateScreen() {
26
+ if (inAlternateScreen)
27
+ return;
28
+ ttyWrite('\x1b[?1049h');
29
+ ttyWrite('\x1b[H');
30
+ inAlternateScreen = true;
31
+ }
32
+ function exitAlternateScreen() {
33
+ if (!inAlternateScreen)
34
+ return;
35
+ ttyWrite('\x1b[?1049l');
36
+ inAlternateScreen = false;
37
+ }
38
+ // =============================================================================
39
+ // Key helpers
40
+ // =============================================================================
41
+ function isCtrlS(data) {
42
+ return data.length === 1 && data[0] === 0x13;
43
+ }
44
+ // =============================================================================
45
+ // Syntax highlighting (simplified — YAML frontmatter + markdown)
46
+ // =============================================================================
47
+ function highlightLine(line, inFrontmatter) {
48
+ const theme = getCurrentTheme();
49
+ const primary = chalk.hex(theme.colors.primary);
50
+ const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
51
+ const muted = chalk.hex(theme.colors.muted);
52
+ if (!line.trim())
53
+ return line;
54
+ // Frontmatter delimiter
55
+ if (line === '---')
56
+ return muted(line);
57
+ // Inside frontmatter — YAML highlighting
58
+ if (inFrontmatter) {
59
+ // Comment
60
+ if (line.trimStart().startsWith('#'))
61
+ return muted(line);
62
+ // Key: value
63
+ const kvMatch = line.match(/^(\s*)([\w-]+)(\s*:\s*)(.*)/);
64
+ if (kvMatch) {
65
+ const [, indent, key, colon, value] = kvMatch;
66
+ return indent + primary(key) + muted(colon) + secondary(value);
67
+ }
68
+ // List item
69
+ if (line.trimStart().startsWith('-'))
70
+ return secondary(line);
71
+ return secondary(line);
72
+ }
73
+ // Markdown highlighting
74
+ const headerMatch = line.match(/^(#{1,6})\s(.*)$/);
75
+ if (headerMatch)
76
+ return primary(headerMatch[1]) + ' ' + primary.bold(headerMatch[2]);
77
+ if (line.match(/^([-*_])\1{2,}\s*$/))
78
+ return muted(line);
79
+ const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)/);
80
+ if (ulMatch)
81
+ return ulMatch[1] + secondary(ulMatch[2]) + ' ' + ulMatch[3];
82
+ const olMatch = line.match(/^(\s*)(\d+\.)\s+(.*)/);
83
+ if (olMatch)
84
+ return olMatch[1] + secondary(olMatch[2]) + ' ' + olMatch[3];
85
+ const bqMatch = line.match(/^(>\s*)(.*)/);
86
+ if (bqMatch)
87
+ return muted(bqMatch[1]) + muted.italic(bqMatch[2]);
88
+ return line;
89
+ }
90
+ // =============================================================================
91
+ // Skill Editor Overlay
92
+ // =============================================================================
93
+ export class SkillEditorOverlay extends BaseOverlayV2 {
94
+ type = 'inline';
95
+ id = 'skill-editor-overlay';
96
+ usesAlternateScreen = true;
97
+ filePath;
98
+ skillName;
99
+ constructor(filePath, skillName, content) {
100
+ super({
101
+ rawLines: content.split('\n'),
102
+ cursorLine: 0,
103
+ cursorColumn: 0,
104
+ scrollOffset: 0,
105
+ isDirty: false,
106
+ originalContent: content,
107
+ showSavePrompt: false,
108
+ statusMessage: null,
109
+ statusType: null,
110
+ });
111
+ this.filePath = filePath;
112
+ this.skillName = skillName;
113
+ }
114
+ onMount() {
115
+ enterAlternateScreen();
116
+ }
117
+ onUnmount() {
118
+ exitAlternateScreen();
119
+ }
120
+ // ===========================================================================
121
+ // Rendering
122
+ // ===========================================================================
123
+ renderContent(context) {
124
+ const s = context.styles;
125
+ const cols = context.width;
126
+ const rows = context.height;
127
+ const border = renderBorder(cols, s);
128
+ const lines = [];
129
+ // Header
130
+ const editIndicator = this.state.isDirty ? s.warning('[EDIT*]') : s.primary('[EDIT]');
131
+ lines.push(border);
132
+ lines.push(` ${s.primaryBold('SKILL')}${s.muted(' │ ')}${chalk.bold(this.skillName)} ${editIndicator}`);
133
+ lines.push(border);
134
+ // Content area
135
+ const headerLines = 3;
136
+ const footerLines = 3;
137
+ const contentHeight = rows - headerLines - footerLines;
138
+ this.ensureCursorVisible(contentHeight);
139
+ const totalLines = this.state.rawLines.length;
140
+ const lineNumWidth = String(totalLines).length + 1;
141
+ // Determine frontmatter state for each visible line
142
+ let inFrontmatter = false;
143
+ let fmCount = 0;
144
+ for (let i = 0; i <= Math.min(this.state.scrollOffset + contentHeight, totalLines - 1); i++) {
145
+ if (this.state.rawLines[i] === '---') {
146
+ fmCount++;
147
+ if (fmCount === 1)
148
+ inFrontmatter = true;
149
+ else if (fmCount === 2)
150
+ inFrontmatter = false;
151
+ }
152
+ }
153
+ // Reset and track frontmatter state for rendering
154
+ inFrontmatter = false;
155
+ fmCount = 0;
156
+ for (let i = 0; i < contentHeight; i++) {
157
+ const lineIndex = this.state.scrollOffset + i;
158
+ if (lineIndex < totalLines) {
159
+ const lineContent = this.state.rawLines[lineIndex];
160
+ const lineNum = String(lineIndex + 1).padStart(lineNumWidth);
161
+ const isCursorLine = lineIndex === this.state.cursorLine;
162
+ // Track frontmatter state
163
+ if (lineContent === '---') {
164
+ fmCount++;
165
+ if (fmCount === 1)
166
+ inFrontmatter = true;
167
+ else if (fmCount === 2)
168
+ inFrontmatter = false;
169
+ }
170
+ if (isCursorLine) {
171
+ const rendered = this.renderCursorLine(lineContent, this.state.cursorColumn, cols - lineNumWidth - 2);
172
+ lines.push(s.muted(`${lineNum}│`) + rendered);
173
+ }
174
+ else {
175
+ const maxLen = cols - lineNumWidth - 2;
176
+ const truncated = lineContent.length > maxLen;
177
+ const display = truncated ? lineContent.slice(0, maxLen - 1) : lineContent;
178
+ const highlighted = highlightLine(display, inFrontmatter && fmCount < 2);
179
+ lines.push(s.muted(`${lineNum}│`) + highlighted + (truncated ? s.muted('…') : ''));
180
+ }
181
+ }
182
+ else {
183
+ const lineNum = ' '.repeat(lineNumWidth);
184
+ lines.push(s.muted(`${lineNum}│`) + s.muted('~'));
185
+ }
186
+ }
187
+ // Footer
188
+ lines.push(border);
189
+ if (this.state.showSavePrompt) {
190
+ lines.push(s.warning(' Unsaved changes. Save? ') + s.primary('(y)') + 'es / ' + s.primary('(n)') + 'o / ' + s.primary('(c)') + 'ancel');
191
+ }
192
+ else if (this.state.statusMessage) {
193
+ const styleFn = this.state.statusType === 'success' ? s.success
194
+ : this.state.statusType === 'warning' ? s.warning
195
+ : this.state.statusType === 'error' ? s.warning
196
+ : s.muted;
197
+ lines.push(styleFn(` ${this.state.statusMessage}`));
198
+ }
199
+ else {
200
+ const cursorPos = `Ln ${String(this.state.cursorLine + 1)}, Col ${String(this.state.cursorColumn + 1)}`;
201
+ const mod = this.state.isDirty ? s.warning('[modified]') : '';
202
+ lines.push(s.primaryBold(' ── INSERT ── ') + s.muted(cursorPos) + ' ' + mod + s.muted(' Ctrl+S Save · Esc Exit'));
203
+ }
204
+ lines.push(border);
205
+ return lines;
206
+ }
207
+ renderCursorLine(line, cursorCol, maxLen) {
208
+ const cursorStyle = chalk.inverse;
209
+ const effectiveCursorCol = Math.min(cursorCol, line.length);
210
+ let viewStart = 0;
211
+ if (effectiveCursorCol >= maxLen - 5) {
212
+ viewStart = effectiveCursorCol - maxLen + 10;
213
+ }
214
+ viewStart = Math.max(0, viewStart);
215
+ const viewEnd = viewStart + maxLen;
216
+ const visibleLine = line.slice(viewStart, viewEnd);
217
+ const cursorPosInView = effectiveCursorCol - viewStart;
218
+ let result = '';
219
+ for (let i = 0; i < visibleLine.length; i++) {
220
+ result += i === cursorPosInView ? cursorStyle(visibleLine[i]) : visibleLine[i];
221
+ }
222
+ if (cursorPosInView >= visibleLine.length) {
223
+ result += cursorStyle(' ');
224
+ }
225
+ const theme = getCurrentTheme();
226
+ if (viewStart > 0) {
227
+ result = chalk.hex(theme.colors.muted)('…') + result.slice(1);
228
+ }
229
+ if (line.length > viewEnd) {
230
+ result = result.slice(0, -1) + chalk.hex(theme.colors.muted)('…');
231
+ }
232
+ return result;
233
+ }
234
+ ensureCursorVisible(contentHeight) {
235
+ if (this.state.cursorLine < this.state.scrollOffset) {
236
+ this.state.scrollOffset = this.state.cursorLine;
237
+ }
238
+ if (this.state.cursorLine >= this.state.scrollOffset + contentHeight) {
239
+ this.state.scrollOffset = this.state.cursorLine - contentHeight + 1;
240
+ }
241
+ }
242
+ // ===========================================================================
243
+ // Key Handling
244
+ // ===========================================================================
245
+ handleKey(key) {
246
+ const data = key.raw;
247
+ // Clear status message on any keypress
248
+ if (this.state.statusMessage && !isCtrlS(data)) {
249
+ this.state.statusMessage = null;
250
+ this.state.statusType = null;
251
+ }
252
+ // Save prompt
253
+ if (this.state.showSavePrompt) {
254
+ return this.handleSavePrompt(data);
255
+ }
256
+ // Ctrl+C — force close
257
+ if (isCtrlC(data)) {
258
+ return { type: 'close', result: { saved: false } };
259
+ }
260
+ // Ctrl+S — save
261
+ if (isCtrlS(data)) {
262
+ void this.save();
263
+ return { type: 'render' };
264
+ }
265
+ // Esc — exit (with dirty check)
266
+ if (isEscape(data)) {
267
+ if (this.state.isDirty) {
268
+ this.state.showSavePrompt = true;
269
+ return { type: 'render' };
270
+ }
271
+ return { type: 'close', result: { saved: false } };
272
+ }
273
+ // Navigation
274
+ if (isUpArrow(data)) {
275
+ this.moveCursorUp();
276
+ return { type: 'render' };
277
+ }
278
+ if (isDownArrow(data)) {
279
+ this.moveCursorDown();
280
+ return { type: 'render' };
281
+ }
282
+ if (isLeftArrow(data)) {
283
+ this.moveCursorLeft();
284
+ return { type: 'render' };
285
+ }
286
+ if (isRightArrow(data)) {
287
+ this.moveCursorRight();
288
+ return { type: 'render' };
289
+ }
290
+ if (isHome(data)) {
291
+ this.state.cursorColumn = 0;
292
+ return { type: 'render' };
293
+ }
294
+ if (isEnd(data)) {
295
+ this.state.cursorColumn = this.currentLine().length;
296
+ return { type: 'render' };
297
+ }
298
+ if (isPageUp(data)) {
299
+ this.pageUp();
300
+ return { type: 'render' };
301
+ }
302
+ if (isPageDown(data)) {
303
+ this.pageDown();
304
+ return { type: 'render' };
305
+ }
306
+ // Editing
307
+ if (isEnter(data)) {
308
+ this.insertNewline();
309
+ return { type: 'render' };
310
+ }
311
+ if (isBackspace(data)) {
312
+ this.handleBackspace();
313
+ return { type: 'render' };
314
+ }
315
+ if (isDelete(data)) {
316
+ this.handleDelete();
317
+ return { type: 'render' };
318
+ }
319
+ if (isTab(data)) {
320
+ this.insertText(' ');
321
+ return { type: 'render' };
322
+ }
323
+ // Printable character
324
+ if (isPrintable(data)) {
325
+ const ch = getPrintableChar(data);
326
+ if (ch) {
327
+ this.insertText(ch);
328
+ return { type: 'render' };
329
+ }
330
+ }
331
+ return { type: 'none' };
332
+ }
333
+ // ===========================================================================
334
+ // Save prompt
335
+ // ===========================================================================
336
+ handleSavePrompt(data) {
337
+ if (isPrintable(data)) {
338
+ const ch = getPrintableChar(data) ?? '';
339
+ if (ch === 'y') {
340
+ void this.save();
341
+ this.state.showSavePrompt = false;
342
+ return { type: 'close', result: { saved: true } };
343
+ }
344
+ if (ch === 'n') {
345
+ this.state.showSavePrompt = false;
346
+ return { type: 'close', result: { saved: false } };
347
+ }
348
+ if (ch === 'c') {
349
+ this.state.showSavePrompt = false;
350
+ return { type: 'render' };
351
+ }
352
+ }
353
+ if (isEscape(data)) {
354
+ this.state.showSavePrompt = false;
355
+ return { type: 'render' };
356
+ }
357
+ return { type: 'none' };
358
+ }
359
+ // ===========================================================================
360
+ // Save + Validate
361
+ // ===========================================================================
362
+ async save() {
363
+ const content = this.state.rawLines.join('\n');
364
+ await fs.writeFile(this.filePath, content);
365
+ this.state.isDirty = false;
366
+ this.state.originalContent = content;
367
+ // Validate
368
+ const parsed = parseSkillMarkdown(content);
369
+ const warnings = [];
370
+ if (!parsed) {
371
+ warnings.push('Invalid frontmatter (required: name, description)');
372
+ }
373
+ else {
374
+ if (parsed.description.length < 60) {
375
+ warnings.push(`Description is ${String(parsed.description.length)} chars (recommend 60+)`);
376
+ }
377
+ if (!parsed.prompt || parsed.prompt.trim().length === 0) {
378
+ warnings.push('Body is empty');
379
+ }
380
+ if (parsed.name !== this.skillName) {
381
+ warnings.push(`Frontmatter name '${parsed.name}' doesn't match folder '${this.skillName}'`);
382
+ }
383
+ }
384
+ if (warnings.length > 0) {
385
+ this.state.statusMessage = `⚠ Saved with ${String(warnings.length)} warning${warnings.length > 1 ? 's' : ''}: ${warnings.join('; ')}`;
386
+ this.state.statusType = 'warning';
387
+ }
388
+ else {
389
+ this.state.statusMessage = '✓ Saved';
390
+ this.state.statusType = 'success';
391
+ }
392
+ this.requestRender();
393
+ }
394
+ // ===========================================================================
395
+ // Cursor movement
396
+ // ===========================================================================
397
+ currentLine() {
398
+ return this.state.rawLines[this.state.cursorLine] ?? '';
399
+ }
400
+ moveCursorUp() {
401
+ if (this.state.cursorLine > 0) {
402
+ this.state.cursorLine--;
403
+ this.state.cursorColumn = Math.min(this.state.cursorColumn, this.currentLine().length);
404
+ }
405
+ }
406
+ moveCursorDown() {
407
+ if (this.state.cursorLine < this.state.rawLines.length - 1) {
408
+ this.state.cursorLine++;
409
+ this.state.cursorColumn = Math.min(this.state.cursorColumn, this.currentLine().length);
410
+ }
411
+ }
412
+ moveCursorLeft() {
413
+ if (this.state.cursorColumn > 0) {
414
+ this.state.cursorColumn--;
415
+ }
416
+ else if (this.state.cursorLine > 0) {
417
+ this.state.cursorLine--;
418
+ this.state.cursorColumn = this.currentLine().length;
419
+ }
420
+ }
421
+ moveCursorRight() {
422
+ if (this.state.cursorColumn < this.currentLine().length) {
423
+ this.state.cursorColumn++;
424
+ }
425
+ else if (this.state.cursorLine < this.state.rawLines.length - 1) {
426
+ this.state.cursorLine++;
427
+ this.state.cursorColumn = 0;
428
+ }
429
+ }
430
+ pageUp() {
431
+ const pageSize = Math.max(1, terminal.getTerminalHeight() - 10);
432
+ this.state.cursorLine = Math.max(0, this.state.cursorLine - pageSize);
433
+ this.state.cursorColumn = Math.min(this.state.cursorColumn, this.currentLine().length);
434
+ }
435
+ pageDown() {
436
+ const pageSize = Math.max(1, terminal.getTerminalHeight() - 10);
437
+ this.state.cursorLine = Math.min(this.state.rawLines.length - 1, this.state.cursorLine + pageSize);
438
+ this.state.cursorColumn = Math.min(this.state.cursorColumn, this.currentLine().length);
439
+ }
440
+ // ===========================================================================
441
+ // Text editing
442
+ // ===========================================================================
443
+ insertText(text) {
444
+ const line = this.currentLine();
445
+ const col = this.state.cursorColumn;
446
+ this.state.rawLines[this.state.cursorLine] = line.slice(0, col) + text + line.slice(col);
447
+ this.state.cursorColumn += text.length;
448
+ this.state.isDirty = true;
449
+ }
450
+ insertNewline() {
451
+ const line = this.currentLine();
452
+ const col = this.state.cursorColumn;
453
+ const before = line.slice(0, col);
454
+ const after = line.slice(col);
455
+ this.state.rawLines[this.state.cursorLine] = before;
456
+ this.state.rawLines.splice(this.state.cursorLine + 1, 0, after);
457
+ this.state.cursorLine++;
458
+ this.state.cursorColumn = 0;
459
+ this.state.isDirty = true;
460
+ }
461
+ handleBackspace() {
462
+ if (this.state.cursorColumn > 0) {
463
+ const line = this.currentLine();
464
+ this.state.rawLines[this.state.cursorLine] =
465
+ line.slice(0, this.state.cursorColumn - 1) + line.slice(this.state.cursorColumn);
466
+ this.state.cursorColumn--;
467
+ this.state.isDirty = true;
468
+ }
469
+ else if (this.state.cursorLine > 0) {
470
+ const currentLine = this.currentLine();
471
+ const prevLine = this.state.rawLines[this.state.cursorLine - 1];
472
+ this.state.cursorColumn = prevLine.length;
473
+ this.state.rawLines[this.state.cursorLine - 1] = prevLine + currentLine;
474
+ this.state.rawLines.splice(this.state.cursorLine, 1);
475
+ this.state.cursorLine--;
476
+ this.state.isDirty = true;
477
+ }
478
+ }
479
+ handleDelete() {
480
+ const line = this.currentLine();
481
+ if (this.state.cursorColumn < line.length) {
482
+ this.state.rawLines[this.state.cursorLine] =
483
+ line.slice(0, this.state.cursorColumn) + line.slice(this.state.cursorColumn + 1);
484
+ this.state.isDirty = true;
485
+ }
486
+ else if (this.state.cursorLine < this.state.rawLines.length - 1) {
487
+ const nextLine = this.state.rawLines[this.state.cursorLine + 1];
488
+ this.state.rawLines[this.state.cursorLine] = line + nextLine;
489
+ this.state.rawLines.splice(this.state.cursorLine + 1, 1);
490
+ this.state.isDirty = true;
491
+ }
492
+ }
493
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Skills Overlay V2
3
+ *
4
+ * Displays custom and built-in skills in a tabbed, searchable list.
5
+ * Uses TabbedListOverlayV2 for consistent overlay UX.
6
+ *
7
+ * Features:
8
+ * - Tabs: Custom (project + user), Built-in (SDK, read-only)
9
+ * - Detail view for custom skills (metadata + body preview + validation)
10
+ * - Enable/disable toggle, delete, rename, validate
11
+ * - New skill creation with name + scope prompts
12
+ * - Edit via alternate screen raw editor
13
+ * - ⚠ validation indicator on skills with issues
14
+ *
15
+ * Spec: project-docs/00-requirements/compilr-dev-cli/skills-overlay-spec.md
16
+ */
17
+ import { TabbedListOverlayV2, BaseScreen } from '../../base/index.js';
18
+ import { type CustomSkill } from '@compilr-dev/sdk';
19
+ import { type SkillScope } from '../../../utils/skill-paths.js';
20
+ /** Unified item for the list — custom skills have full data, built-in have name+description only */
21
+ interface SkillItem {
22
+ name: string;
23
+ description: string;
24
+ scope: 'project' | 'user' | 'sdk' | 'binding';
25
+ enabled: boolean;
26
+ /** Validation issues (empty = valid) */
27
+ issues: string[];
28
+ /** Full CustomSkill data (null for built-in) */
29
+ skill: CustomSkill | null;
30
+ /** File path (null for built-in) */
31
+ filePath: string | null;
32
+ /** Prompt body preview (null for built-in — IP protected) */
33
+ bodyPreview: string | null;
34
+ /** Targeting info */
35
+ targets?: CustomSkill['compilr'];
36
+ /** Commands bound to this skill */
37
+ boundCommands: string[];
38
+ /** forkedFrom marker */
39
+ forkedFrom?: CustomSkill['forkedFrom'];
40
+ /** For binding items: the slash command name */
41
+ bindingCommand?: string;
42
+ /** For binding items: the scope of the binding config */
43
+ bindingScope?: SkillScope;
44
+ }
45
+ export interface SkillsOverlayV2Options {
46
+ /** Project root path (for project-scoped skills) */
47
+ projectPath?: string;
48
+ /** Callback to open the skill detail overlay (view/edit/diff). Returns true if modified. */
49
+ onOpenDetail?: (filePath: string, skillName: string, scope: string, forkedFrom?: import('@compilr-dev/sdk').ForkedFromMarker, boundCommands?: string[]) => Promise<boolean>;
50
+ }
51
+ export interface SkillsOverlayV2Result {
52
+ dismissed: boolean;
53
+ }
54
+ export declare class SkillsOverlayV2 extends TabbedListOverlayV2<SkillItem, SkillsOverlayV2Result> {
55
+ readonly type: "inline";
56
+ readonly id = "skills-overlay-v2";
57
+ private allItems;
58
+ private readonly projectPath?;
59
+ private readonly onOpenDetail?;
60
+ constructor(options: SkillsOverlayV2Options);
61
+ /**
62
+ * Load skills data. Called after construction.
63
+ */
64
+ loadItems(): Promise<void>;
65
+ protected createDetailScreen(item: SkillItem): BaseScreen | null;
66
+ /**
67
+ * Handle custom keys on the list view.
68
+ * Called from the config.handleCustomKey closure.
69
+ */
70
+ onCustomKey(data: Buffer): {
71
+ handled: boolean;
72
+ render?: boolean;
73
+ pushScreen?: BaseScreen;
74
+ };
75
+ private createSkill;
76
+ private toggleSkill;
77
+ private deleteSkill;
78
+ private renameSkill;
79
+ private revalidateSkill;
80
+ private forkSkill;
81
+ private removeOneBinding;
82
+ }
83
+ export {};