@compilr-dev/cli 0.6.6 → 0.7.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.
- package/dist/commands-v2/handlers/core.js +10 -1
- package/dist/commands-v2/handlers/index.d.ts +1 -0
- package/dist/commands-v2/handlers/index.js +3 -0
- package/dist/commands-v2/handlers/settings.js +21 -5
- package/dist/commands-v2/handlers/skill.d.ts +10 -0
- package/dist/commands-v2/handlers/skill.js +63 -0
- package/dist/commands-v2/index.d.ts +1 -1
- package/dist/commands-v2/index.js +1 -1
- package/dist/commands-v2/registry.d.ts +4 -0
- package/dist/commands-v2/registry.js +19 -0
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/index.js +8 -12
- package/dist/repl-helpers.d.ts +29 -1
- package/dist/repl-helpers.js +77 -7
- package/dist/repl-v2.js +29 -3
- package/dist/ui/conversation.js +1 -2
- package/dist/ui/markdown-renderer.d.ts +43 -0
- package/dist/ui/markdown-renderer.js +474 -0
- package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +1 -2
- package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1 -2
- package/dist/ui/overlay/impl/help-overlay-v2.d.ts +7 -1
- package/dist/ui/overlay/impl/help-overlay-v2.js +19 -2
- package/dist/ui/overlay/impl/skill-detail-overlay-v2.d.ts +91 -0
- package/dist/ui/overlay/impl/skill-detail-overlay-v2.js +863 -0
- package/dist/ui/overlay/impl/skill-editor-overlay.d.ts +56 -0
- package/dist/ui/overlay/impl/skill-editor-overlay.js +493 -0
- package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +83 -0
- package/dist/ui/overlay/impl/skills-overlay-v2.js +1095 -0
- package/dist/utils/skill-paths.d.ts +21 -0
- package/dist/utils/skill-paths.js +44 -0
- package/package.json +7 -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 {};
|