@skilly-hand/skilly-hand 0.18.0 → 0.19.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/CHANGELOG.md +19 -0
- package/README.md +9 -3
- package/catalog/skills/project-security/assets/generic-ci-security-gate.sh +1 -28
- package/catalog/skills/project-security/assets/github-actions-security-gate.yml +38 -0
- package/catalog/skills/project-security/assets/pre-commit.sample.sh +1 -1
- package/catalog/skills/project-security/assets/pre-push.sample.sh +1 -30
- package/catalog/skills/project-security/assets/run-security-check.shared.sh +33 -0
- package/package.json +5 -3
- package/packages/catalog/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/bin.js +126 -161
- package/packages/cli/src/ink-ui.js +692 -0
- package/packages/core/package.json +1 -1
- package/packages/core/src/terminal.js +16 -5
- package/packages/core/src/ui/layout.js +193 -42
- package/packages/detectors/package.json +1 -1
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Box, Text, render, useApp, useInput } from "ink";
|
|
3
|
+
import { getBrand } from "../../core/src/ui/brand.js";
|
|
4
|
+
|
|
5
|
+
function wrapText(text, width) {
|
|
6
|
+
const safeWidth = Math.max(20, width || 80);
|
|
7
|
+
const input = String(text || "").split("\n");
|
|
8
|
+
const lines = [];
|
|
9
|
+
for (const rawLine of input) {
|
|
10
|
+
const line = rawLine.trimEnd();
|
|
11
|
+
if (!line) {
|
|
12
|
+
lines.push("");
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
let rest = line;
|
|
16
|
+
while (rest.length > safeWidth) {
|
|
17
|
+
let split = rest.lastIndexOf(" ", safeWidth);
|
|
18
|
+
if (split <= 0) split = safeWidth;
|
|
19
|
+
lines.push(rest.slice(0, split));
|
|
20
|
+
rest = rest.slice(split).trimStart();
|
|
21
|
+
}
|
|
22
|
+
lines.push(rest);
|
|
23
|
+
}
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function truncateLine(text, maxWidth) {
|
|
28
|
+
const safeWidth = Math.max(4, maxWidth || 40);
|
|
29
|
+
const chars = Array.from(String(text || ""));
|
|
30
|
+
if (chars.length <= safeWidth) return chars.join("");
|
|
31
|
+
if (safeWidth <= 4) return chars.slice(0, safeWidth).join("");
|
|
32
|
+
return `${chars.slice(0, safeWidth - 1).join("")}…`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatConfirmPreviewLines(preview, width) {
|
|
36
|
+
const rawLines = String(preview || "").split("\n");
|
|
37
|
+
const bodyStart = rawLines.findIndex((line) =>
|
|
38
|
+
/Install Preflight|Detection Summary|Catalog Summary|Doctor Summary|Findings|Skill Plan/.test(line)
|
|
39
|
+
);
|
|
40
|
+
const headEnd = bodyStart > 0 ? bodyStart : Math.min(12, rawLines.length);
|
|
41
|
+
const head = rawLines.slice(0, headEnd).map((line) => truncateLine(line, width));
|
|
42
|
+
const body = rawLines.slice(headEnd).flatMap((line) => wrapText(line, width));
|
|
43
|
+
return [...head, ...body];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clamp(value, min, max) {
|
|
47
|
+
return Math.min(Math.max(value, min), max);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function computeWindow(total, visible, cursor) {
|
|
51
|
+
if (total <= 0) return { start: 0, end: 0 };
|
|
52
|
+
const safeVisible = Math.max(1, Math.min(visible, total));
|
|
53
|
+
const half = Math.floor(safeVisible / 2);
|
|
54
|
+
let start = cursor - half;
|
|
55
|
+
start = clamp(start, 0, Math.max(0, total - safeVisible));
|
|
56
|
+
return { start, end: start + safeVisible };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fitWindowByLineBudget(blocks, cursor, availableLines) {
|
|
60
|
+
const total = blocks.length;
|
|
61
|
+
if (total <= 0) return { start: 0, end: 0, usedLines: 0 };
|
|
62
|
+
|
|
63
|
+
const safeCursor = clamp(cursor, 0, total - 1);
|
|
64
|
+
const budget = Math.max(1, availableLines);
|
|
65
|
+
|
|
66
|
+
let start = safeCursor;
|
|
67
|
+
const focusLines = Math.max(1, blocks[safeCursor].lineCount);
|
|
68
|
+
const desiredTopLines = Math.max(0, Math.floor((budget - focusLines) / 2));
|
|
69
|
+
|
|
70
|
+
let consumedTop = 0;
|
|
71
|
+
while (start > 0) {
|
|
72
|
+
const next = Math.max(1, blocks[start - 1].lineCount);
|
|
73
|
+
if (consumedTop + next > desiredTopLines) break;
|
|
74
|
+
start -= 1;
|
|
75
|
+
consumedTop += next;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let end = start;
|
|
79
|
+
let usedLines = 0;
|
|
80
|
+
while (end < total) {
|
|
81
|
+
const next = Math.max(1, blocks[end].lineCount);
|
|
82
|
+
if (usedLines + next > budget && end > start) break;
|
|
83
|
+
usedLines += next;
|
|
84
|
+
end += 1;
|
|
85
|
+
if (usedLines >= budget && end > start) break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (safeCursor >= end) {
|
|
89
|
+
start = safeCursor;
|
|
90
|
+
end = safeCursor;
|
|
91
|
+
usedLines = 0;
|
|
92
|
+
while (end < total) {
|
|
93
|
+
const next = Math.max(1, blocks[end].lineCount);
|
|
94
|
+
if (usedLines + next > budget && end > start) break;
|
|
95
|
+
usedLines += next;
|
|
96
|
+
end += 1;
|
|
97
|
+
if (usedLines >= budget && end > start) break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
while (start > 0) {
|
|
102
|
+
const next = Math.max(1, blocks[start - 1].lineCount);
|
|
103
|
+
if (usedLines + next > budget) break;
|
|
104
|
+
start -= 1;
|
|
105
|
+
usedLines += next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { start, end, usedLines };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function useAsyncAction() {
|
|
112
|
+
const [busy, setBusy] = useState(false);
|
|
113
|
+
|
|
114
|
+
async function run(action) {
|
|
115
|
+
setBusy(true);
|
|
116
|
+
try {
|
|
117
|
+
return await action();
|
|
118
|
+
} finally {
|
|
119
|
+
setBusy(false);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { busy, run };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Header({ appVersion }) {
|
|
127
|
+
const brand = getBrand();
|
|
128
|
+
const logo = brand.logo.unicode;
|
|
129
|
+
|
|
130
|
+
return React.createElement(
|
|
131
|
+
Box,
|
|
132
|
+
{ flexDirection: "column", marginBottom: 1 },
|
|
133
|
+
...logo.map((line, idx) => React.createElement(Text, { key: `logo-${idx}`, color: "cyan" }, line)),
|
|
134
|
+
React.createElement(
|
|
135
|
+
Box,
|
|
136
|
+
{ marginTop: 0 },
|
|
137
|
+
React.createElement(Text, { color: "cyan", bold: true }, `${brand.name}`),
|
|
138
|
+
React.createElement(Text, { color: "gray" }, appVersion ? ` v${appVersion}` : ""),
|
|
139
|
+
React.createElement(Text, { color: "gray" }, ` ${brand.tagline}`)
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function MenuPanel({ selectedIndex, menuItems }) {
|
|
145
|
+
return React.createElement(
|
|
146
|
+
Box,
|
|
147
|
+
{ flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, width: 32 },
|
|
148
|
+
React.createElement(Text, { color: "cyan", bold: true }, "Command Hub"),
|
|
149
|
+
...menuItems.map((item, idx) => {
|
|
150
|
+
const active = idx === selectedIndex;
|
|
151
|
+
return React.createElement(
|
|
152
|
+
Text,
|
|
153
|
+
{ key: item.value, color: active ? "black" : "white", backgroundColor: active ? "cyan" : undefined },
|
|
154
|
+
`${active ? "›" : " "} ${item.label}`
|
|
155
|
+
);
|
|
156
|
+
}),
|
|
157
|
+
React.createElement(Text, { color: "gray" }, ""),
|
|
158
|
+
React.createElement(Text, { color: "gray" }, "↑↓ move Enter select q quit")
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ResultPanel({ title, lines, busy, maxLines, offset }) {
|
|
163
|
+
const viewportLines = Math.max(6, maxLines - 4);
|
|
164
|
+
const maxOffset = Math.max(0, lines.length - viewportLines);
|
|
165
|
+
const safeOffset = clamp(offset, 0, maxOffset);
|
|
166
|
+
const visible = lines.slice(safeOffset, safeOffset + viewportLines);
|
|
167
|
+
const from = lines.length ? safeOffset + 1 : 0;
|
|
168
|
+
const to = safeOffset + visible.length;
|
|
169
|
+
|
|
170
|
+
return React.createElement(
|
|
171
|
+
Box,
|
|
172
|
+
{ flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
|
|
173
|
+
React.createElement(Text, { color: "cyan", bold: true }, title),
|
|
174
|
+
busy ? React.createElement(Text, { color: "yellow" }, "Working...") : null,
|
|
175
|
+
...visible.map((line, idx) =>
|
|
176
|
+
React.createElement(Text, { key: `line-${idx}`, color: idx === 0 ? "white" : "gray" }, line)
|
|
177
|
+
),
|
|
178
|
+
React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
|
|
179
|
+
React.createElement(
|
|
180
|
+
Text,
|
|
181
|
+
{ color: "gray" },
|
|
182
|
+
`Lines ${from}-${to} of ${lines.length} | ↑/↓ or j/k scroll`
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function computeSkillBlock({ skill, focused, chosen, contentWidth }) {
|
|
188
|
+
const safeWidth = Math.max(20, contentWidth);
|
|
189
|
+
const tagsText = ` tags: ${skill.tags.join(", ") || "none"}`;
|
|
190
|
+
const agentsText = ` agents: ${skill.agentSupport.join(", ") || "none"}`;
|
|
191
|
+
const tagsLines = wrapText(tagsText, safeWidth);
|
|
192
|
+
const agentsLines = wrapText(agentsText, safeWidth);
|
|
193
|
+
const header = `${focused ? "›" : " "} [${chosen ? "x" : " "}] ${skill.id} | ${skill.title}`;
|
|
194
|
+
const lines = [header, ...tagsLines, ...agentsLines];
|
|
195
|
+
return {
|
|
196
|
+
skill,
|
|
197
|
+
lines,
|
|
198
|
+
lineCount: lines.length
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function buildSkillBlocksForRender({ skills, cursor, selectedIds, contentWidth }) {
|
|
203
|
+
return skills.map((skill, idx) =>
|
|
204
|
+
computeSkillBlock({
|
|
205
|
+
skill,
|
|
206
|
+
focused: idx === cursor,
|
|
207
|
+
chosen: selectedIds.has(skill.id),
|
|
208
|
+
contentWidth
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function computeSkillWindow({ blocks, cursor, availableLines }) {
|
|
214
|
+
return fitWindowByLineBudget(blocks, cursor, availableLines);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function SkillPickerPanel({ skills, cursor, selectedIds, maxLines, width }) {
|
|
218
|
+
const contentWidth = Math.max(20, (width || 80) - 6);
|
|
219
|
+
const blocks = buildSkillBlocksForRender({
|
|
220
|
+
skills,
|
|
221
|
+
cursor,
|
|
222
|
+
selectedIds,
|
|
223
|
+
contentWidth
|
|
224
|
+
});
|
|
225
|
+
const availableLines = Math.max(1, maxLines - 5);
|
|
226
|
+
const window = computeSkillWindow({ blocks, cursor, availableLines });
|
|
227
|
+
const chunk = blocks.slice(window.start, window.end);
|
|
228
|
+
|
|
229
|
+
return React.createElement(
|
|
230
|
+
Box,
|
|
231
|
+
{ flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
|
|
232
|
+
React.createElement(Text, { color: "cyan", bold: true }, "Install: Select Skills"),
|
|
233
|
+
...chunk.map((block, localIdx) => {
|
|
234
|
+
const idx = window.start + localIdx;
|
|
235
|
+
return React.createElement(
|
|
236
|
+
Box,
|
|
237
|
+
{ key: block.skill.id, flexDirection: "column" },
|
|
238
|
+
...block.lines.map((line, lineIdx) =>
|
|
239
|
+
React.createElement(Text, {
|
|
240
|
+
key: `${block.skill.id}-${lineIdx}`,
|
|
241
|
+
color: lineIdx === 0 ? (idx === cursor ? "black" : "white") : "gray",
|
|
242
|
+
backgroundColor: lineIdx === 0 && idx === cursor ? "cyan" : undefined
|
|
243
|
+
}, line)
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
}),
|
|
247
|
+
React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
|
|
248
|
+
React.createElement(
|
|
249
|
+
Text,
|
|
250
|
+
{ color: "gray" },
|
|
251
|
+
`Skills ${window.start + 1}-${window.end} of ${skills.length}`
|
|
252
|
+
),
|
|
253
|
+
React.createElement(Text, { color: "gray" }, "↑/↓ or j/k move, Space toggle, Enter continue, Backspace cancel")
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function AgentPickerPanel({ agents, cursor, selected, maxLines }) {
|
|
258
|
+
const visibleAgents = Math.max(1, maxLines - 5);
|
|
259
|
+
const window = computeWindow(agents.length, visibleAgents, cursor);
|
|
260
|
+
const chunk = agents.slice(window.start, window.end);
|
|
261
|
+
|
|
262
|
+
return React.createElement(
|
|
263
|
+
Box,
|
|
264
|
+
{ flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
|
|
265
|
+
React.createElement(Text, { color: "cyan", bold: true }, "Install: Select Assistants"),
|
|
266
|
+
...chunk.map((agent, localIdx) => {
|
|
267
|
+
const idx = window.start + localIdx;
|
|
268
|
+
const focused = idx === cursor;
|
|
269
|
+
const chosen = selected.has(agent);
|
|
270
|
+
return React.createElement(
|
|
271
|
+
Text,
|
|
272
|
+
{ key: agent, color: focused ? "black" : "white", backgroundColor: focused ? "cyan" : undefined },
|
|
273
|
+
`${focused ? "›" : " "} [${chosen ? "x" : " "}] ${agent}`
|
|
274
|
+
);
|
|
275
|
+
}),
|
|
276
|
+
React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
|
|
277
|
+
React.createElement(
|
|
278
|
+
Text,
|
|
279
|
+
{ color: "gray" },
|
|
280
|
+
`Assistants ${window.start + 1}-${window.end} of ${agents.length}`
|
|
281
|
+
),
|
|
282
|
+
React.createElement(Text, { color: "gray" }, "↑/↓ or j/k move, Space toggle, Enter continue, Backspace back")
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ConfirmPanel({ title, preview, options, selectedIndex, width, maxLines, offset }) {
|
|
287
|
+
const previewWidth = Math.max(24, (width || 80) - 10);
|
|
288
|
+
const previewLines = formatConfirmPreviewLines(preview, previewWidth);
|
|
289
|
+
const viewport = Math.max(5, maxLines - 14);
|
|
290
|
+
const maxOffset = Math.max(0, previewLines.length - viewport);
|
|
291
|
+
const safeOffset = clamp(offset, 0, maxOffset);
|
|
292
|
+
const visible = previewLines.slice(safeOffset, safeOffset + viewport);
|
|
293
|
+
const from = previewLines.length ? safeOffset + 1 : 0;
|
|
294
|
+
const to = safeOffset + visible.length;
|
|
295
|
+
|
|
296
|
+
return React.createElement(
|
|
297
|
+
Box,
|
|
298
|
+
{ flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
|
|
299
|
+
React.createElement(Text, { color: "cyan", bold: true }, title),
|
|
300
|
+
React.createElement(Text, { color: "cyan" }, "Action"),
|
|
301
|
+
...options.map((opt, idx) => {
|
|
302
|
+
const active = idx === selectedIndex;
|
|
303
|
+
return React.createElement(
|
|
304
|
+
Text,
|
|
305
|
+
{ key: opt, color: active ? "black" : "white", backgroundColor: active ? "cyan" : undefined },
|
|
306
|
+
`${active ? "›" : " "} ${opt}`
|
|
307
|
+
);
|
|
308
|
+
}),
|
|
309
|
+
React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
|
|
310
|
+
React.createElement(Text, { color: "cyan" }, "Install Snapshot"),
|
|
311
|
+
React.createElement(
|
|
312
|
+
Box,
|
|
313
|
+
{ flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, paddingY: 0 },
|
|
314
|
+
...visible.map((line, idx) => React.createElement(Text, { key: `preview-${idx}`, color: "gray" }, line))
|
|
315
|
+
),
|
|
316
|
+
React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
|
|
317
|
+
React.createElement(Text, { color: "gray" }, `Preview ${from}-${to} of ${previewLines.length}`),
|
|
318
|
+
React.createElement(Text, { color: "gray" }, "↑/↓ choose, Enter confirm, Backspace cancel, j/k scroll")
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function App({ appVersion, actions, onResolve }) {
|
|
323
|
+
const menuItems = [
|
|
324
|
+
{ value: "install", label: "Install" },
|
|
325
|
+
{ value: "detect", label: "Detect" },
|
|
326
|
+
{ value: "list", label: "List" },
|
|
327
|
+
{ value: "doctor", label: "Doctor" },
|
|
328
|
+
{ value: "uninstall", label: "Uninstall" },
|
|
329
|
+
{ value: "exit", label: "Exit" }
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const { exit } = useApp();
|
|
333
|
+
const [mode, setMode] = useState("menu");
|
|
334
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
335
|
+
const [resultTitle, setResultTitle] = useState("Ready");
|
|
336
|
+
const [resultBody, setResultBody] = useState("Choose a command from the left.");
|
|
337
|
+
const [resultOffset, setResultOffset] = useState(0);
|
|
338
|
+
|
|
339
|
+
const [installSkills, setInstallSkills] = useState([]);
|
|
340
|
+
const [installAgents, setInstallAgents] = useState([]);
|
|
341
|
+
const [selectedSkills, setSelectedSkills] = useState(new Set());
|
|
342
|
+
const [selectedAgents, setSelectedAgents] = useState(new Set());
|
|
343
|
+
const [cursorIndex, setCursorIndex] = useState(0);
|
|
344
|
+
const [confirmChoice, setConfirmChoice] = useState(0);
|
|
345
|
+
const [installPreview, setInstallPreview] = useState("");
|
|
346
|
+
const [confirmPreviewOffset, setConfirmPreviewOffset] = useState(0);
|
|
347
|
+
|
|
348
|
+
const { busy, run } = useAsyncAction();
|
|
349
|
+
const stdoutWidth = Number(process.stdout?.columns || 120);
|
|
350
|
+
const stdoutRows = Number(process.stdout?.rows || 40);
|
|
351
|
+
const contentWidth = Math.max(60, stdoutWidth - 36);
|
|
352
|
+
const panelMaxLines = Math.max(12, stdoutRows - 10);
|
|
353
|
+
const resultLines = useMemo(() => wrapText(resultBody, contentWidth - 6), [resultBody, contentWidth]);
|
|
354
|
+
const resultViewport = Math.max(6, panelMaxLines - 4);
|
|
355
|
+
const resultMaxOffset = Math.max(0, resultLines.length - resultViewport);
|
|
356
|
+
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
return () => onResolve?.();
|
|
359
|
+
}, [onResolve]);
|
|
360
|
+
|
|
361
|
+
async function runMenuAction(value) {
|
|
362
|
+
if (value === "exit") {
|
|
363
|
+
onResolve?.();
|
|
364
|
+
exit();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (value === "install") {
|
|
369
|
+
await run(async () => {
|
|
370
|
+
const context = await actions.prepareInstall();
|
|
371
|
+
const preSkills = new Set(context.skills.filter((s) => s.checked).map((s) => s.id));
|
|
372
|
+
const preAgents = new Set(context.agents.filter((a) => a.checked).map((a) => a.value));
|
|
373
|
+
setInstallSkills(context.skills);
|
|
374
|
+
setInstallAgents(context.agents.map((a) => a.value));
|
|
375
|
+
setSelectedSkills(preSkills);
|
|
376
|
+
setSelectedAgents(preAgents);
|
|
377
|
+
setCursorIndex(0);
|
|
378
|
+
setConfirmPreviewOffset(0);
|
|
379
|
+
setMode("install-skills");
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (value === "uninstall") {
|
|
385
|
+
setConfirmChoice(1);
|
|
386
|
+
setMode("confirm-uninstall");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await run(async () => {
|
|
391
|
+
const body = await actions.runCommand(value);
|
|
392
|
+
setResultTitle(value[0].toUpperCase() + value.slice(1));
|
|
393
|
+
setResultBody(body);
|
|
394
|
+
setResultOffset(0);
|
|
395
|
+
setMode("result");
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
useInput((input, key) => {
|
|
400
|
+
if (input === "q") {
|
|
401
|
+
onResolve?.();
|
|
402
|
+
exit();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (busy) return;
|
|
407
|
+
|
|
408
|
+
if (mode === "menu") {
|
|
409
|
+
if (key.upArrow) {
|
|
410
|
+
setMenuIndex((current) => (current - 1 + menuItems.length) % menuItems.length);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (key.downArrow) {
|
|
414
|
+
setMenuIndex((current) => (current + 1) % menuItems.length);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (key.return) {
|
|
418
|
+
void runMenuAction(menuItems[menuIndex].value);
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (mode === "result") {
|
|
424
|
+
if (key.upArrow) {
|
|
425
|
+
setMenuIndex((current) => (current - 1 + menuItems.length) % menuItems.length);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (key.downArrow) {
|
|
429
|
+
setMenuIndex((current) => (current + 1) % menuItems.length);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (key.return) {
|
|
433
|
+
setMode("menu");
|
|
434
|
+
void runMenuAction(menuItems[menuIndex].value);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (key.backspace || key.escape) {
|
|
438
|
+
setMode("menu");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (input === "j") {
|
|
442
|
+
setResultOffset((current) => clamp(current + 1, 0, resultMaxOffset));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (input === "k") {
|
|
446
|
+
setResultOffset((current) => clamp(current - 1, 0, resultMaxOffset));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (mode === "install-skills") {
|
|
453
|
+
if (key.upArrow || input === "k") setCursorIndex((i) => clamp(i - 1, 0, Math.max(0, installSkills.length - 1)));
|
|
454
|
+
else if (key.downArrow || input === "j") setCursorIndex((i) => clamp(i + 1, 0, Math.max(0, installSkills.length - 1)));
|
|
455
|
+
else if (input === " ") {
|
|
456
|
+
setSelectedSkills((current) => {
|
|
457
|
+
const next = new Set(current);
|
|
458
|
+
const id = installSkills[cursorIndex]?.id;
|
|
459
|
+
if (!id) return next;
|
|
460
|
+
if (next.has(id)) next.delete(id);
|
|
461
|
+
else next.add(id);
|
|
462
|
+
return next;
|
|
463
|
+
});
|
|
464
|
+
} else if (key.return) {
|
|
465
|
+
setCursorIndex(0);
|
|
466
|
+
setMode("install-agents");
|
|
467
|
+
} else if (key.backspace || key.escape) {
|
|
468
|
+
setCursorIndex(0);
|
|
469
|
+
setMode("menu");
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (mode === "install-agents") {
|
|
475
|
+
if (key.upArrow || input === "k") setCursorIndex((i) => Math.max(0, i - 1));
|
|
476
|
+
else if (key.downArrow || input === "j") setCursorIndex((i) => Math.min(installAgents.length - 1, i + 1));
|
|
477
|
+
else if (input === " ") {
|
|
478
|
+
setSelectedAgents((current) => {
|
|
479
|
+
const next = new Set(current);
|
|
480
|
+
const id = installAgents[cursorIndex];
|
|
481
|
+
if (!id) return next;
|
|
482
|
+
if (next.has(id)) next.delete(id);
|
|
483
|
+
else next.add(id);
|
|
484
|
+
return next;
|
|
485
|
+
});
|
|
486
|
+
} else if (key.return) {
|
|
487
|
+
void run(async () => {
|
|
488
|
+
const preview = await actions.previewInstall({
|
|
489
|
+
selectedSkillIds: [...selectedSkills],
|
|
490
|
+
selectedAgents: [...selectedAgents]
|
|
491
|
+
});
|
|
492
|
+
setInstallPreview(preview);
|
|
493
|
+
setConfirmPreviewOffset(0);
|
|
494
|
+
setConfirmChoice(0);
|
|
495
|
+
setMode("confirm-install");
|
|
496
|
+
});
|
|
497
|
+
} else if (key.backspace || key.escape) {
|
|
498
|
+
setCursorIndex(0);
|
|
499
|
+
setConfirmPreviewOffset(0);
|
|
500
|
+
setMode("install-skills");
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (mode === "confirm-install") {
|
|
506
|
+
if (key.upArrow || key.downArrow) setConfirmChoice((current) => (current === 0 ? 1 : 0));
|
|
507
|
+
else if (key.return) {
|
|
508
|
+
if (confirmChoice === 1) {
|
|
509
|
+
setMode("menu");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
void run(async () => {
|
|
513
|
+
const output = await actions.applyInstall({
|
|
514
|
+
selectedSkillIds: [...selectedSkills],
|
|
515
|
+
selectedAgents: [...selectedAgents]
|
|
516
|
+
});
|
|
517
|
+
setResultTitle("Install");
|
|
518
|
+
setResultBody(output);
|
|
519
|
+
setResultOffset(0);
|
|
520
|
+
setMode("result");
|
|
521
|
+
});
|
|
522
|
+
} else if (key.backspace || key.escape) {
|
|
523
|
+
setMode("install-agents");
|
|
524
|
+
} else if (input === "j") {
|
|
525
|
+
setConfirmPreviewOffset((current) => current + 1);
|
|
526
|
+
} else if (input === "k") {
|
|
527
|
+
setConfirmPreviewOffset((current) => Math.max(0, current - 1));
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (mode === "confirm-uninstall") {
|
|
533
|
+
if (key.upArrow || key.downArrow) setConfirmChoice((current) => (current === 0 ? 1 : 0));
|
|
534
|
+
else if (key.return) {
|
|
535
|
+
if (confirmChoice === 1) {
|
|
536
|
+
setMode("menu");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
void run(async () => {
|
|
540
|
+
const output = await actions.runCommand("uninstall");
|
|
541
|
+
setResultTitle("Uninstall");
|
|
542
|
+
setResultBody(output);
|
|
543
|
+
setResultOffset(0);
|
|
544
|
+
setMode("result");
|
|
545
|
+
});
|
|
546
|
+
} else if (key.backspace || key.escape) {
|
|
547
|
+
setMode("menu");
|
|
548
|
+
} else if (input === "j") {
|
|
549
|
+
setConfirmPreviewOffset((current) => current + 1);
|
|
550
|
+
} else if (input === "k") {
|
|
551
|
+
setConfirmPreviewOffset((current) => Math.max(0, current - 1));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
let rightPanel = React.createElement(ResultPanel, {
|
|
557
|
+
title: resultTitle,
|
|
558
|
+
lines: resultLines,
|
|
559
|
+
busy,
|
|
560
|
+
maxLines: panelMaxLines,
|
|
561
|
+
offset: resultOffset
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (mode === "install-skills") {
|
|
565
|
+
rightPanel = React.createElement(SkillPickerPanel, {
|
|
566
|
+
skills: installSkills,
|
|
567
|
+
cursor: cursorIndex,
|
|
568
|
+
selectedIds: selectedSkills,
|
|
569
|
+
maxLines: panelMaxLines,
|
|
570
|
+
width: contentWidth
|
|
571
|
+
});
|
|
572
|
+
} else if (mode === "install-agents") {
|
|
573
|
+
rightPanel = React.createElement(AgentPickerPanel, {
|
|
574
|
+
agents: installAgents,
|
|
575
|
+
cursor: cursorIndex,
|
|
576
|
+
selected: selectedAgents,
|
|
577
|
+
maxLines: panelMaxLines
|
|
578
|
+
});
|
|
579
|
+
} else if (mode === "confirm-install") {
|
|
580
|
+
rightPanel = React.createElement(ConfirmPanel, {
|
|
581
|
+
title: "Confirm Installation",
|
|
582
|
+
preview: installPreview,
|
|
583
|
+
options: ["Apply changes", "Cancel"],
|
|
584
|
+
selectedIndex: confirmChoice,
|
|
585
|
+
width: contentWidth,
|
|
586
|
+
maxLines: panelMaxLines,
|
|
587
|
+
offset: confirmPreviewOffset
|
|
588
|
+
});
|
|
589
|
+
} else if (mode === "confirm-uninstall") {
|
|
590
|
+
rightPanel = React.createElement(ConfirmPanel, {
|
|
591
|
+
title: "Confirm Uninstall",
|
|
592
|
+
preview: "Remove the skilly-hand installation from this project?",
|
|
593
|
+
options: ["Remove installation", "Cancel"],
|
|
594
|
+
selectedIndex: confirmChoice,
|
|
595
|
+
width: contentWidth,
|
|
596
|
+
maxLines: panelMaxLines,
|
|
597
|
+
offset: confirmPreviewOffset
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return React.createElement(
|
|
602
|
+
Box,
|
|
603
|
+
{ flexDirection: "column", paddingX: 1, paddingY: 0 },
|
|
604
|
+
React.createElement(Header, { appVersion }),
|
|
605
|
+
React.createElement(
|
|
606
|
+
Box,
|
|
607
|
+
{ flexDirection: "row" },
|
|
608
|
+
React.createElement(MenuPanel, { selectedIndex: menuIndex, menuItems }),
|
|
609
|
+
React.createElement(Box, { width: 1 }, React.createElement(Text, null, " ")),
|
|
610
|
+
rightPanel
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export async function launchInkApp({ appVersion, actions }) {
|
|
616
|
+
return new Promise((resolve, reject) => {
|
|
617
|
+
let resolved = false;
|
|
618
|
+
const done = () => {
|
|
619
|
+
if (resolved) return;
|
|
620
|
+
resolved = true;
|
|
621
|
+
resolve();
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
render(
|
|
626
|
+
React.createElement(App, {
|
|
627
|
+
appVersion,
|
|
628
|
+
actions,
|
|
629
|
+
onResolve: done
|
|
630
|
+
}),
|
|
631
|
+
{
|
|
632
|
+
exitOnCtrlC: true
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
reject(error);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function confirmWithInk({ message, defaultValue = false }) {
|
|
642
|
+
return new Promise((resolve, reject) => {
|
|
643
|
+
function ConfirmApp() {
|
|
644
|
+
const { exit } = useApp();
|
|
645
|
+
const [choice, setChoice] = useState(defaultValue ? 0 : 1);
|
|
646
|
+
useInput((_, key) => {
|
|
647
|
+
if (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow) {
|
|
648
|
+
setChoice((current) => (current === 0 ? 1 : 0));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (key.return) {
|
|
652
|
+
resolve(choice === 0);
|
|
653
|
+
exit();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (key.escape) {
|
|
657
|
+
resolve(false);
|
|
658
|
+
exit();
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
return React.createElement(
|
|
663
|
+
Box,
|
|
664
|
+
{ flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 },
|
|
665
|
+
React.createElement(Text, { color: "cyan", bold: true }, "Confirmation"),
|
|
666
|
+
React.createElement(Text, null, String(message)),
|
|
667
|
+
React.createElement(Text, { color: "gray" }, "Use arrows to choose, Enter to confirm."),
|
|
668
|
+
React.createElement(
|
|
669
|
+
Box,
|
|
670
|
+
{ marginTop: 1 },
|
|
671
|
+
React.createElement(
|
|
672
|
+
Text,
|
|
673
|
+
{ color: choice === 0 ? "black" : "white", backgroundColor: choice === 0 ? "cyan" : undefined },
|
|
674
|
+
" Yes "
|
|
675
|
+
),
|
|
676
|
+
React.createElement(Text, null, " "),
|
|
677
|
+
React.createElement(
|
|
678
|
+
Text,
|
|
679
|
+
{ color: choice === 1 ? "black" : "white", backgroundColor: choice === 1 ? "cyan" : undefined },
|
|
680
|
+
" No "
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
render(React.createElement(ConfirmApp), { exitOnCtrlC: true });
|
|
688
|
+
} catch (error) {
|
|
689
|
+
reject(error);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|