skild 0.10.15 → 0.10.16
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/chunk-U4SVSURE.js +299 -0
- package/dist/index.js +155 -415
- package/dist/ui/interactive-tree-prompt.d.ts +38 -0
- package/dist/ui/interactive-tree-prompt.js +12 -0
- package/package.json +4 -3
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// src/ui/interactive-tree-prompt.ts
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import stringWidth from "string-width";
|
|
5
|
+
var altScreenRefCount = 0;
|
|
6
|
+
var altScreenActive = false;
|
|
7
|
+
var altScreenExitTimer = null;
|
|
8
|
+
var altScreenStdout = null;
|
|
9
|
+
var postPromptLogs = [];
|
|
10
|
+
function enqueuePostPromptLog(message) {
|
|
11
|
+
postPromptLogs.push(message);
|
|
12
|
+
}
|
|
13
|
+
function flushPostPromptLogs(stdout) {
|
|
14
|
+
if (altScreenActive) return;
|
|
15
|
+
if (postPromptLogs.length === 0) return;
|
|
16
|
+
for (const msg of postPromptLogs.splice(0)) {
|
|
17
|
+
stdout.write(msg.endsWith("\n") ? msg : msg + "\n");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function enterAltScreen(stdout) {
|
|
21
|
+
if (altScreenExitTimer) {
|
|
22
|
+
clearTimeout(altScreenExitTimer);
|
|
23
|
+
altScreenExitTimer = null;
|
|
24
|
+
}
|
|
25
|
+
altScreenStdout = stdout;
|
|
26
|
+
if (!altScreenActive) {
|
|
27
|
+
stdout.write("\x1B[?1049h");
|
|
28
|
+
stdout.write("\x1B[H");
|
|
29
|
+
stdout.write("\x1B[2J");
|
|
30
|
+
altScreenActive = true;
|
|
31
|
+
}
|
|
32
|
+
altScreenRefCount += 1;
|
|
33
|
+
}
|
|
34
|
+
function exitAltScreenDeferred() {
|
|
35
|
+
if (altScreenExitTimer) return;
|
|
36
|
+
const stdout = altScreenStdout;
|
|
37
|
+
if (!stdout) return;
|
|
38
|
+
altScreenExitTimer = setTimeout(() => {
|
|
39
|
+
altScreenExitTimer = null;
|
|
40
|
+
if (altScreenRefCount !== 0) return;
|
|
41
|
+
if (!altScreenActive) return;
|
|
42
|
+
stdout.write("\x1B[?1049l");
|
|
43
|
+
altScreenActive = false;
|
|
44
|
+
flushPostPromptLogs(stdout);
|
|
45
|
+
}, 200);
|
|
46
|
+
}
|
|
47
|
+
function leaveAltScreen(stdout) {
|
|
48
|
+
if (altScreenStdout && altScreenStdout !== stdout) {
|
|
49
|
+
altScreenStdout.write("\x1B[?1049l");
|
|
50
|
+
altScreenActive = false;
|
|
51
|
+
altScreenStdout = stdout;
|
|
52
|
+
}
|
|
53
|
+
altScreenRefCount = Math.max(0, altScreenRefCount - 1);
|
|
54
|
+
if (altScreenRefCount === 0) exitAltScreenDeferred();
|
|
55
|
+
}
|
|
56
|
+
function flushInteractiveUiNow() {
|
|
57
|
+
const stdout = altScreenStdout || process.stdout;
|
|
58
|
+
if (altScreenExitTimer) {
|
|
59
|
+
clearTimeout(altScreenExitTimer);
|
|
60
|
+
altScreenExitTimer = null;
|
|
61
|
+
}
|
|
62
|
+
if (altScreenRefCount !== 0) return;
|
|
63
|
+
if (altScreenActive) {
|
|
64
|
+
stdout.write("\x1B[?1049l");
|
|
65
|
+
altScreenActive = false;
|
|
66
|
+
}
|
|
67
|
+
flushPostPromptLogs(stdout);
|
|
68
|
+
}
|
|
69
|
+
function getNodeSelection(node, selected) {
|
|
70
|
+
const total = node.leafIndices.length;
|
|
71
|
+
if (total === 0) return { state: "none", selectedCount: 0 };
|
|
72
|
+
let selectedCount = 0;
|
|
73
|
+
for (const idx of node.leafIndices) {
|
|
74
|
+
if (selected.has(idx)) selectedCount += 1;
|
|
75
|
+
}
|
|
76
|
+
let state = "none";
|
|
77
|
+
if (selectedCount === total) state = "all";
|
|
78
|
+
else if (selectedCount > 0) state = "partial";
|
|
79
|
+
return { state, selectedCount };
|
|
80
|
+
}
|
|
81
|
+
function flattenTree(root) {
|
|
82
|
+
const result = [];
|
|
83
|
+
function walk(node) {
|
|
84
|
+
if (node.id !== "") result.push(node);
|
|
85
|
+
for (const child of node.children) walk(child);
|
|
86
|
+
}
|
|
87
|
+
for (const child of root.children) walk(child);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
function createRenderer(title, subtitle, flatNodes, selected, getCursor, getViewOffset, viewHeight, maxWidth, formatNode) {
|
|
91
|
+
function renderContent() {
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(chalk.bold.cyan(title));
|
|
94
|
+
lines.push(chalk.dim(subtitle));
|
|
95
|
+
lines.push("");
|
|
96
|
+
const cursor = getCursor();
|
|
97
|
+
const offset = getViewOffset();
|
|
98
|
+
const end = Math.min(flatNodes.length, offset + viewHeight);
|
|
99
|
+
for (let i = offset; i < end; i++) {
|
|
100
|
+
const node = flatNodes[i];
|
|
101
|
+
const selection = getNodeSelection(node, selected);
|
|
102
|
+
lines.push(formatNode(node, selection, i === cursor, maxWidth));
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push(chalk.dim(`Space toggle \u2022 Enter confirm \u2022 A select all \u2022 Ctrl+C cancel`));
|
|
106
|
+
lines.push(chalk.dim(`Showing ${end - offset}/${flatNodes.length} (offset ${offset + 1})`));
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
function getLineCount() {
|
|
110
|
+
return 6 + Math.min(viewHeight, flatNodes.length);
|
|
111
|
+
}
|
|
112
|
+
return { renderContent, getLineCount };
|
|
113
|
+
}
|
|
114
|
+
function writeToTerminal(stdout, lines) {
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
stdout.write(line + "\n");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function clearAndRerender(stdout, lines) {
|
|
120
|
+
stdout.write("\x1B[?25l");
|
|
121
|
+
stdout.write("\x1B[H");
|
|
122
|
+
stdout.write("\x1B[2J");
|
|
123
|
+
writeToTerminal(stdout, lines);
|
|
124
|
+
}
|
|
125
|
+
async function interactiveTreeSelect(items, options) {
|
|
126
|
+
const { title, subtitle, buildTree, formatNode, defaultAll, defaultSelected } = options;
|
|
127
|
+
const root = buildTree(items);
|
|
128
|
+
const flatNodes = flattenTree(root);
|
|
129
|
+
if (flatNodes.length === 0) return null;
|
|
130
|
+
const stdin = process.stdin;
|
|
131
|
+
const stdout = process.stdout;
|
|
132
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
133
|
+
if (defaultSelected) return Array.from(defaultSelected);
|
|
134
|
+
return defaultAll ? items.map((_, i) => i) : null;
|
|
135
|
+
}
|
|
136
|
+
const cols = typeof stdout.columns === "number" ? stdout.columns : 120;
|
|
137
|
+
const rows = typeof stdout.rows === "number" ? stdout.rows : 24;
|
|
138
|
+
const viewHeight = Math.max(5, rows - 8);
|
|
139
|
+
const maxWidth = Math.max(40, cols - 2);
|
|
140
|
+
const selected = /* @__PURE__ */ new Set();
|
|
141
|
+
if (defaultSelected) {
|
|
142
|
+
for (const idx of defaultSelected) selected.add(idx);
|
|
143
|
+
} else if (defaultAll) {
|
|
144
|
+
for (let i = 0; i < items.length; i++) selected.add(i);
|
|
145
|
+
}
|
|
146
|
+
let cursor = 0;
|
|
147
|
+
let viewOffset = 0;
|
|
148
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
149
|
+
stdin.setRawMode(true);
|
|
150
|
+
stdin.resume();
|
|
151
|
+
readline.emitKeypressEvents(stdin);
|
|
152
|
+
enterAltScreen(stdout);
|
|
153
|
+
const renderer = createRenderer(
|
|
154
|
+
title,
|
|
155
|
+
subtitle,
|
|
156
|
+
flatNodes,
|
|
157
|
+
selected,
|
|
158
|
+
() => cursor,
|
|
159
|
+
() => viewOffset,
|
|
160
|
+
viewHeight,
|
|
161
|
+
maxWidth,
|
|
162
|
+
formatNode
|
|
163
|
+
);
|
|
164
|
+
clearAndRerender(stdout, renderer.renderContent());
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
function cleanup() {
|
|
167
|
+
stdin.setRawMode(wasRaw);
|
|
168
|
+
stdin.pause();
|
|
169
|
+
stdin.removeListener("keypress", onKeypress);
|
|
170
|
+
stdout.write("\x1B[?25h");
|
|
171
|
+
leaveAltScreen(stdout);
|
|
172
|
+
}
|
|
173
|
+
function rerender() {
|
|
174
|
+
clearAndRerender(stdout, renderer.renderContent());
|
|
175
|
+
}
|
|
176
|
+
function toggleNode(node) {
|
|
177
|
+
const { state } = getNodeSelection(node, selected);
|
|
178
|
+
const shouldSelectAll = state !== "all";
|
|
179
|
+
if (shouldSelectAll) {
|
|
180
|
+
for (const idx of node.leafIndices) selected.add(idx);
|
|
181
|
+
} else {
|
|
182
|
+
for (const idx of node.leafIndices) selected.delete(idx);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function toggleAll() {
|
|
186
|
+
if (selected.size === items.length) {
|
|
187
|
+
selected.clear();
|
|
188
|
+
} else {
|
|
189
|
+
for (let i = 0; i < items.length; i++) selected.add(i);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function onKeypress(_str, key) {
|
|
193
|
+
if (key.ctrl && key.name === "c") {
|
|
194
|
+
cleanup();
|
|
195
|
+
resolve(null);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (key.name === "return" || key.name === "enter") {
|
|
199
|
+
cleanup();
|
|
200
|
+
resolve(selected.size > 0 ? Array.from(selected) : null);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (key.name === "up") {
|
|
204
|
+
if (cursor === 0) return;
|
|
205
|
+
cursor -= 1;
|
|
206
|
+
if (cursor < viewOffset) viewOffset = cursor;
|
|
207
|
+
rerender();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (key.name === "down") {
|
|
211
|
+
if (cursor === flatNodes.length - 1) return;
|
|
212
|
+
cursor += 1;
|
|
213
|
+
if (cursor >= viewOffset + viewHeight) viewOffset = cursor - viewHeight + 1;
|
|
214
|
+
rerender();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (key.name === "space") {
|
|
218
|
+
toggleNode(flatNodes[cursor]);
|
|
219
|
+
rerender();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (key.name === "a") {
|
|
223
|
+
toggleAll();
|
|
224
|
+
rerender();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
stdin.on("keypress", onKeypress);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function truncateVisible(value, maxLen) {
|
|
232
|
+
let width = 0;
|
|
233
|
+
let result = "";
|
|
234
|
+
for (const ch of value) {
|
|
235
|
+
const w = stringWidth(ch);
|
|
236
|
+
if (width + w > maxLen - 1) {
|
|
237
|
+
result += "\u2026";
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
width += w;
|
|
241
|
+
result += ch;
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
function truncateMiddleVisible(value, maxLen) {
|
|
246
|
+
if (stringWidth(value) <= maxLen) return value;
|
|
247
|
+
if (maxLen <= 1) return "\u2026";
|
|
248
|
+
const leftMax = Math.max(1, Math.floor((maxLen - 1) / 2));
|
|
249
|
+
const rightMax = Math.max(1, maxLen - 1 - leftMax);
|
|
250
|
+
const left = truncateVisible(value, leftMax).replace(/…$/, "");
|
|
251
|
+
let width = 0;
|
|
252
|
+
let right = "";
|
|
253
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
254
|
+
const ch = value[i];
|
|
255
|
+
const w = stringWidth(ch);
|
|
256
|
+
if (width + w > rightMax) break;
|
|
257
|
+
width += w;
|
|
258
|
+
right = ch + right;
|
|
259
|
+
}
|
|
260
|
+
if (!right) right = value[value.length - 1] || "";
|
|
261
|
+
return `${left}\u2026${right}`;
|
|
262
|
+
}
|
|
263
|
+
function formatInteractiveRow(input) {
|
|
264
|
+
const styleName = input.styleName || ((s) => s);
|
|
265
|
+
const prefix = `${input.cursorMark}${input.indent}${input.glyph} `;
|
|
266
|
+
const fixedWidth = stringWidth(prefix) + stringWidth(input.count);
|
|
267
|
+
let rawName = input.name;
|
|
268
|
+
let rawSuffix = input.suffixText || "";
|
|
269
|
+
let rawHint = input.hintText || "";
|
|
270
|
+
const fits = (n, s, h) => {
|
|
271
|
+
const nStyled = input.isCursor ? styleName(n) : n;
|
|
272
|
+
const sStyled = s ? chalk.dim(s) : "";
|
|
273
|
+
const hStyled = h ? chalk.dim(h) : "";
|
|
274
|
+
return stringWidth(`${prefix}${nStyled}${input.count}${sStyled}${hStyled}`) <= input.maxWidth;
|
|
275
|
+
};
|
|
276
|
+
if (rawSuffix && !fits(rawName, rawSuffix, rawHint)) {
|
|
277
|
+
const available = Math.max(1, input.maxWidth - fixedWidth - stringWidth(rawName) - stringWidth(rawHint));
|
|
278
|
+
rawSuffix = truncateVisible(rawSuffix, available);
|
|
279
|
+
}
|
|
280
|
+
if (rawHint && !fits(rawName, rawSuffix, rawHint)) {
|
|
281
|
+
const available = Math.max(1, input.maxWidth - fixedWidth - stringWidth(rawName) - stringWidth(rawSuffix));
|
|
282
|
+
rawHint = truncateVisible(rawHint, available);
|
|
283
|
+
}
|
|
284
|
+
if (!fits(rawName, rawSuffix, rawHint)) {
|
|
285
|
+
const available = Math.max(1, input.maxWidth - fixedWidth - stringWidth(rawSuffix) - stringWidth(rawHint));
|
|
286
|
+
rawName = truncateMiddleVisible(rawName, available);
|
|
287
|
+
}
|
|
288
|
+
const finalName = input.isCursor ? styleName(rawName) : rawName;
|
|
289
|
+
const suffix = rawSuffix ? chalk.dim(rawSuffix) : "";
|
|
290
|
+
const hint = rawHint ? chalk.dim(rawHint) : "";
|
|
291
|
+
return `${prefix}${finalName}${input.count}${suffix}${hint}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export {
|
|
295
|
+
enqueuePostPromptLog,
|
|
296
|
+
flushInteractiveUiNow,
|
|
297
|
+
interactiveTreeSelect,
|
|
298
|
+
formatInteractiveRow
|
|
299
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
enqueuePostPromptLog,
|
|
4
|
+
flushInteractiveUiNow,
|
|
5
|
+
formatInteractiveRow,
|
|
6
|
+
interactiveTreeSelect
|
|
7
|
+
} from "./chunk-U4SVSURE.js";
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
10
|
import { Command } from "commander";
|
|
@@ -86,73 +92,40 @@ var logger = {
|
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
// src/utils/interactive-select.ts
|
|
89
|
-
import readline from "readline";
|
|
90
|
-
import stringWidth from "string-width";
|
|
91
95
|
import chalk2 from "chalk";
|
|
92
96
|
import { PLATFORMS } from "@skild/core";
|
|
93
|
-
var
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!stdout) return;
|
|
126
|
-
altScreenExitTimer = setTimeout(() => {
|
|
127
|
-
altScreenExitTimer = null;
|
|
128
|
-
if (altScreenRefCount !== 0) return;
|
|
129
|
-
if (!altScreenActive) return;
|
|
130
|
-
stdout.write("\x1B[?1049l");
|
|
131
|
-
altScreenActive = false;
|
|
132
|
-
flushPostPromptLogs(stdout);
|
|
133
|
-
}, 200);
|
|
134
|
-
}
|
|
135
|
-
function leaveAltScreen(stdout) {
|
|
136
|
-
if (altScreenStdout && altScreenStdout !== stdout) {
|
|
137
|
-
altScreenStdout.write("\x1B[?1049l");
|
|
138
|
-
altScreenActive = false;
|
|
139
|
-
altScreenStdout = stdout;
|
|
140
|
-
}
|
|
141
|
-
altScreenRefCount = Math.max(0, altScreenRefCount - 1);
|
|
142
|
-
if (altScreenRefCount === 0) exitAltScreenDeferred();
|
|
143
|
-
}
|
|
144
|
-
function flushInteractiveUiNow() {
|
|
145
|
-
const stdout = altScreenStdout || process.stdout;
|
|
146
|
-
if (altScreenExitTimer) {
|
|
147
|
-
clearTimeout(altScreenExitTimer);
|
|
148
|
-
altScreenExitTimer = null;
|
|
149
|
-
}
|
|
150
|
-
if (altScreenRefCount !== 0) return;
|
|
151
|
-
if (altScreenActive) {
|
|
152
|
-
stdout.write("\x1B[?1049l");
|
|
153
|
-
altScreenActive = false;
|
|
154
|
-
}
|
|
155
|
-
flushPostPromptLogs(stdout);
|
|
97
|
+
var PLATFORM_DISPLAY = {
|
|
98
|
+
claude: "Claude",
|
|
99
|
+
codex: "Codex",
|
|
100
|
+
copilot: "Copilot",
|
|
101
|
+
antigravity: "Antigravity",
|
|
102
|
+
opencode: "OpenCode",
|
|
103
|
+
cursor: "Cursor",
|
|
104
|
+
windsurf: "Windsurf"
|
|
105
|
+
};
|
|
106
|
+
function createTreeNode(id, name, depth, isLeaf, leafIndices = []) {
|
|
107
|
+
return { id, name, depth, children: [], leafIndices, isLeaf };
|
|
108
|
+
}
|
|
109
|
+
function wrapWithRoot(allNode) {
|
|
110
|
+
return {
|
|
111
|
+
id: "",
|
|
112
|
+
name: ".",
|
|
113
|
+
depth: 0,
|
|
114
|
+
children: [allNode],
|
|
115
|
+
leafIndices: [...allNode.leafIndices],
|
|
116
|
+
isLeaf: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function adjustDepth(node, delta) {
|
|
120
|
+
node.depth += delta;
|
|
121
|
+
for (const child of node.children) adjustDepth(child, delta);
|
|
122
|
+
}
|
|
123
|
+
function collapseIntermediateNodes(allNode) {
|
|
124
|
+
while (allNode.children.length === 1 && !allNode.children[0].isLeaf && allNode.children[0].children.length > 0) {
|
|
125
|
+
const singleChild = allNode.children[0];
|
|
126
|
+
for (const grandchild of singleChild.children) adjustDepth(grandchild, -1);
|
|
127
|
+
allNode.children = singleChild.children;
|
|
128
|
+
}
|
|
156
129
|
}
|
|
157
130
|
function buildSkillTree(skills) {
|
|
158
131
|
const allNode = createTreeNode("all", "All Skills", 1, false);
|
|
@@ -176,39 +149,6 @@ function buildSkillTree(skills) {
|
|
|
176
149
|
collapseIntermediateNodes(allNode);
|
|
177
150
|
return wrapWithRoot(allNode);
|
|
178
151
|
}
|
|
179
|
-
function buildSyncTree(choices) {
|
|
180
|
-
const root = createTreeNode("root", "All targets", 1, false);
|
|
181
|
-
const platforms = /* @__PURE__ */ new Map();
|
|
182
|
-
for (let i = 0; i < choices.length; i++) {
|
|
183
|
-
const choice = choices[i];
|
|
184
|
-
let platformNode = platforms.get(choice.targetPlatform);
|
|
185
|
-
if (!platformNode) {
|
|
186
|
-
platformNode = createTreeNode(choice.targetPlatform, choice.targetPlatform, 2, false);
|
|
187
|
-
platforms.set(choice.targetPlatform, platformNode);
|
|
188
|
-
root.children.push(platformNode);
|
|
189
|
-
}
|
|
190
|
-
const skillNode = createTreeNode(
|
|
191
|
-
`${choice.targetPlatform}:${choice.skill}`,
|
|
192
|
-
choice.displayName,
|
|
193
|
-
3,
|
|
194
|
-
true,
|
|
195
|
-
[i]
|
|
196
|
-
);
|
|
197
|
-
platformNode.children.push(skillNode);
|
|
198
|
-
platformNode.leafIndices.push(i);
|
|
199
|
-
root.leafIndices.push(i);
|
|
200
|
-
}
|
|
201
|
-
return wrapWithRoot(root);
|
|
202
|
-
}
|
|
203
|
-
function buildPlatformTree(items) {
|
|
204
|
-
const allNode = createTreeNode("all", "All Platforms", 1, false);
|
|
205
|
-
for (let i = 0; i < items.length; i++) {
|
|
206
|
-
const platform = items[i].platform;
|
|
207
|
-
allNode.children.push(createTreeNode(platform, platform, 2, true, [i]));
|
|
208
|
-
allNode.leafIndices.push(i);
|
|
209
|
-
}
|
|
210
|
-
return wrapWithRoot(allNode);
|
|
211
|
-
}
|
|
212
152
|
function buildTreeFromSkillNodes(nodes, totalSkills) {
|
|
213
153
|
const allNode = createTreeNode("all", "All Skills", 1, false);
|
|
214
154
|
const attach = (node, depth) => {
|
|
@@ -238,278 +178,38 @@ function buildTreeFromSkillNodes(nodes, totalSkills) {
|
|
|
238
178
|
}
|
|
239
179
|
return wrapWithRoot(allNode);
|
|
240
180
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
function collapseIntermediateNodes(allNode) {
|
|
248
|
-
while (allNode.children.length === 1 && !allNode.children[0].isLeaf && allNode.children[0].children.length > 0) {
|
|
249
|
-
const singleChild = allNode.children[0];
|
|
250
|
-
for (const grandchild of singleChild.children) {
|
|
251
|
-
adjustDepth(grandchild, -1);
|
|
252
|
-
}
|
|
253
|
-
allNode.children = singleChild.children;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function adjustDepth(node, delta) {
|
|
257
|
-
node.depth += delta;
|
|
258
|
-
for (const child of node.children) {
|
|
259
|
-
adjustDepth(child, delta);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
function flattenTree(root) {
|
|
263
|
-
const result = [];
|
|
264
|
-
function walk(node) {
|
|
265
|
-
if (node.id !== "") result.push(node);
|
|
266
|
-
for (const child of node.children) walk(child);
|
|
267
|
-
}
|
|
268
|
-
for (const child of root.children) walk(child);
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
function getNodeSelection(node, selected) {
|
|
272
|
-
const total = node.leafIndices.length;
|
|
273
|
-
if (total === 0) return { state: "none", selectedCount: 0 };
|
|
274
|
-
let selectedCount = 0;
|
|
275
|
-
for (const idx of node.leafIndices) {
|
|
276
|
-
if (selected.has(idx)) selectedCount++;
|
|
277
|
-
}
|
|
278
|
-
let state = "none";
|
|
279
|
-
if (selectedCount === total) state = "all";
|
|
280
|
-
else if (selectedCount > 0) state = "partial";
|
|
281
|
-
return { state, selectedCount };
|
|
282
|
-
}
|
|
283
|
-
function createRenderer(title, subtitle, flatNodes, selected, getCursor, getViewOffset, viewHeight, maxWidth, formatNode) {
|
|
284
|
-
function renderContent() {
|
|
285
|
-
const lines = [];
|
|
286
|
-
lines.push(chalk2.bold.cyan(title));
|
|
287
|
-
lines.push(chalk2.dim(subtitle));
|
|
288
|
-
lines.push("");
|
|
289
|
-
const cursor = getCursor();
|
|
290
|
-
const offset = getViewOffset();
|
|
291
|
-
const end = Math.min(flatNodes.length, offset + viewHeight);
|
|
292
|
-
for (let i = offset; i < end; i++) {
|
|
293
|
-
const node = flatNodes[i];
|
|
294
|
-
const selection = getNodeSelection(node, selected);
|
|
295
|
-
lines.push(formatNode(node, selection, i === cursor, maxWidth));
|
|
296
|
-
}
|
|
297
|
-
lines.push("");
|
|
298
|
-
lines.push(chalk2.dim(`Space toggle \u2022 Enter confirm \u2022 A select all \u2022 Ctrl+C cancel`));
|
|
299
|
-
lines.push(chalk2.dim(`Showing ${end - offset}/${flatNodes.length} (offset ${offset + 1})`));
|
|
300
|
-
return lines;
|
|
301
|
-
}
|
|
302
|
-
function getLineCount() {
|
|
303
|
-
return 6 + Math.min(viewHeight, flatNodes.length);
|
|
304
|
-
}
|
|
305
|
-
return { renderContent, getLineCount };
|
|
306
|
-
}
|
|
307
|
-
function writeToTerminal(stdout, lines) {
|
|
308
|
-
for (const line of lines) {
|
|
309
|
-
stdout.write(line + "\n");
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function clearAndRerender(stdout, lineCount, lines) {
|
|
313
|
-
stdout.write("\x1B[?25l");
|
|
314
|
-
stdout.write("\x1B[H");
|
|
315
|
-
stdout.write("\x1B[2J");
|
|
316
|
-
writeToTerminal(stdout, lines);
|
|
317
|
-
}
|
|
318
|
-
async function interactiveTreeSelect(items, options) {
|
|
319
|
-
const { title, subtitle, buildTree, formatNode, defaultAll, defaultSelected } = options;
|
|
320
|
-
const root = buildTree(items);
|
|
321
|
-
const flatNodes = flattenTree(root);
|
|
322
|
-
if (flatNodes.length === 0) return null;
|
|
323
|
-
const stdout = process.stdout;
|
|
324
|
-
const cols = typeof stdout.columns === "number" ? stdout.columns : 120;
|
|
325
|
-
const rows = typeof stdout.rows === "number" ? stdout.rows : 24;
|
|
326
|
-
const viewHeight = Math.max(5, rows - 5);
|
|
327
|
-
const maxWidth = Math.max(40, cols - 2);
|
|
328
|
-
const selected = /* @__PURE__ */ new Set();
|
|
329
|
-
if (defaultSelected) {
|
|
330
|
-
for (const idx of defaultSelected) selected.add(idx);
|
|
331
|
-
} else if (defaultAll) {
|
|
332
|
-
for (let i = 0; i < items.length; i++) selected.add(i);
|
|
333
|
-
}
|
|
334
|
-
let cursor = 0;
|
|
335
|
-
let viewOffset = 0;
|
|
336
|
-
const stdin = process.stdin;
|
|
337
|
-
if (!stdin.isTTY || !stdout.isTTY) {
|
|
338
|
-
return defaultAll ? Array.from(selected) : null;
|
|
339
|
-
}
|
|
340
|
-
const useAltScreen = true;
|
|
341
|
-
const wasRaw = Boolean(stdin.isRaw);
|
|
342
|
-
stdin.setRawMode(true);
|
|
343
|
-
stdin.resume();
|
|
344
|
-
readline.emitKeypressEvents(stdin);
|
|
345
|
-
if (useAltScreen) enterAltScreen(stdout);
|
|
346
|
-
const renderer = createRenderer(
|
|
347
|
-
title,
|
|
348
|
-
subtitle,
|
|
349
|
-
flatNodes,
|
|
350
|
-
selected,
|
|
351
|
-
() => cursor,
|
|
352
|
-
() => viewOffset,
|
|
353
|
-
viewHeight,
|
|
354
|
-
maxWidth,
|
|
355
|
-
(node, selection, isCursor) => formatNode(node, selection, isCursor, maxWidth)
|
|
356
|
-
);
|
|
357
|
-
clearAndRerender(stdout, renderer.getLineCount(), renderer.renderContent());
|
|
358
|
-
return new Promise((resolve) => {
|
|
359
|
-
function cleanup(clear = false) {
|
|
360
|
-
if (clear && !useAltScreen) {
|
|
361
|
-
const lineCount = renderer.getLineCount();
|
|
362
|
-
stdout.write(`\x1B[${lineCount}A`);
|
|
363
|
-
stdout.write("\x1B[0J");
|
|
364
|
-
}
|
|
365
|
-
stdin.setRawMode(wasRaw);
|
|
366
|
-
stdin.pause();
|
|
367
|
-
stdin.removeListener("keypress", onKeypress);
|
|
368
|
-
stdout.write("\x1B[?25h");
|
|
369
|
-
if (useAltScreen) {
|
|
370
|
-
leaveAltScreen(stdout);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function rerender() {
|
|
374
|
-
clearAndRerender(stdout, renderer.getLineCount(), renderer.renderContent());
|
|
375
|
-
}
|
|
376
|
-
function toggleNode(node) {
|
|
377
|
-
const { state } = getNodeSelection(node, selected);
|
|
378
|
-
const shouldSelectAll = state !== "all";
|
|
379
|
-
if (shouldSelectAll) {
|
|
380
|
-
for (const idx of node.leafIndices) selected.add(idx);
|
|
381
|
-
} else {
|
|
382
|
-
for (const idx of node.leafIndices) selected.delete(idx);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
function toggleAll() {
|
|
386
|
-
if (selected.size === items.length) {
|
|
387
|
-
selected.clear();
|
|
388
|
-
} else {
|
|
389
|
-
for (let i = 0; i < items.length; i++) selected.add(i);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
function onKeypress(_str, key) {
|
|
393
|
-
if (key.ctrl && key.name === "c") {
|
|
394
|
-
cleanup(true);
|
|
395
|
-
resolve(null);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
if (key.name === "return" || key.name === "enter") {
|
|
399
|
-
cleanup(true);
|
|
400
|
-
resolve(selected.size > 0 ? Array.from(selected) : null);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
if (key.name === "up") {
|
|
404
|
-
cursor = (cursor - 1 + flatNodes.length) % flatNodes.length;
|
|
405
|
-
if (cursor < viewOffset) viewOffset = cursor;
|
|
406
|
-
rerender();
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
if (key.name === "down") {
|
|
410
|
-
cursor = (cursor + 1) % flatNodes.length;
|
|
411
|
-
if (cursor >= viewOffset + viewHeight) viewOffset = cursor - viewHeight + 1;
|
|
412
|
-
rerender();
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
if (key.name === "space") {
|
|
416
|
-
toggleNode(flatNodes[cursor]);
|
|
417
|
-
rerender();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
if (key.name === "a") {
|
|
421
|
-
toggleAll();
|
|
422
|
-
rerender();
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
stdin.on("keypress", onKeypress);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
var PLATFORM_DISPLAY = {
|
|
430
|
-
claude: "Claude",
|
|
431
|
-
codex: "Codex",
|
|
432
|
-
copilot: "Copilot",
|
|
433
|
-
antigravity: "Antigravity",
|
|
434
|
-
opencode: "OpenCode",
|
|
435
|
-
cursor: "Cursor",
|
|
436
|
-
windsurf: "Windsurf"
|
|
437
|
-
};
|
|
438
|
-
function truncateVisible(value, maxLen) {
|
|
439
|
-
let width = 0;
|
|
440
|
-
let result = "";
|
|
441
|
-
for (const ch of value) {
|
|
442
|
-
const w = stringWidth(ch);
|
|
443
|
-
if (width + w > maxLen - 1) {
|
|
444
|
-
result += "\u2026";
|
|
445
|
-
return result;
|
|
446
|
-
}
|
|
447
|
-
width += w;
|
|
448
|
-
result += ch;
|
|
181
|
+
function buildPlatformTree(items) {
|
|
182
|
+
const allNode = createTreeNode("all", "All Platforms", 1, false);
|
|
183
|
+
for (let i = 0; i < items.length; i++) {
|
|
184
|
+
const platform = items[i].platform;
|
|
185
|
+
allNode.children.push(createTreeNode(platform, platform, 2, true, [i]));
|
|
186
|
+
allNode.leafIndices.push(i);
|
|
449
187
|
}
|
|
450
|
-
return
|
|
188
|
+
return wrapWithRoot(allNode);
|
|
451
189
|
}
|
|
452
|
-
function
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const w = stringWidth(ch);
|
|
463
|
-
if (width + w > rightMax) break;
|
|
464
|
-
width += w;
|
|
465
|
-
right = ch + right;
|
|
466
|
-
}
|
|
467
|
-
if (!right) right = value[value.length - 1] || "";
|
|
468
|
-
return `${left}\u2026${right}`;
|
|
469
|
-
}
|
|
470
|
-
function formatTreeNode(node, selection, isCursor, options = {}) {
|
|
471
|
-
const { state, selectedCount } = selection;
|
|
472
|
-
const totalCount = node.leafIndices.length;
|
|
473
|
-
const indent = " ".repeat(node.depth - 1);
|
|
474
|
-
const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
|
|
475
|
-
const baseName = node.name || "";
|
|
476
|
-
let name = isCursor ? chalk2.cyan.underline(baseName) : baseName;
|
|
477
|
-
const cursorMark = isCursor ? chalk2.cyan("\u203A ") : " ";
|
|
478
|
-
let count = "";
|
|
479
|
-
if (totalCount > 1) {
|
|
480
|
-
count = chalk2.dim(` (${selectedCount}/${totalCount})`);
|
|
481
|
-
}
|
|
482
|
-
let rawSuffix = options.suffixText || "";
|
|
483
|
-
let rawHint = isCursor ? options.hintText || "" : "";
|
|
484
|
-
let hint = rawHint ? chalk2.dim(rawHint) : "";
|
|
485
|
-
let suffix = rawSuffix ? chalk2.dim(rawSuffix) : "";
|
|
486
|
-
const prefix = `${cursorMark}${indent}${checkbox} `;
|
|
487
|
-
const maxWidth = options.maxWidth;
|
|
488
|
-
if (maxWidth) {
|
|
489
|
-
const fixedWidth = stringWidth(prefix) + stringWidth(count);
|
|
490
|
-
const fits = (n, s, h) => {
|
|
491
|
-
const nStyled = isCursor ? chalk2.cyan.underline(n) : n;
|
|
492
|
-
const sStyled = s ? chalk2.dim(s) : "";
|
|
493
|
-
const hStyled = h ? chalk2.dim(h) : "";
|
|
494
|
-
return stringWidth(`${prefix}${nStyled}${count}${sStyled}${hStyled}`) <= maxWidth;
|
|
495
|
-
};
|
|
496
|
-
if (rawSuffix && !fits(baseName, rawSuffix, rawHint)) {
|
|
497
|
-
const available = Math.max(1, maxWidth - fixedWidth - stringWidth(baseName) - stringWidth(rawHint));
|
|
498
|
-
rawSuffix = truncateVisible(rawSuffix, available);
|
|
499
|
-
suffix = chalk2.dim(rawSuffix);
|
|
500
|
-
}
|
|
501
|
-
if (rawHint && !fits(baseName, rawSuffix, rawHint)) {
|
|
502
|
-
const available = Math.max(1, maxWidth - fixedWidth - stringWidth(baseName) - stringWidth(rawSuffix));
|
|
503
|
-
rawHint = truncateVisible(rawHint, available);
|
|
504
|
-
hint = chalk2.dim(rawHint);
|
|
505
|
-
}
|
|
506
|
-
if (!fits(baseName, rawSuffix, rawHint)) {
|
|
507
|
-
const available = Math.max(1, maxWidth - fixedWidth - stringWidth(rawSuffix) - stringWidth(rawHint));
|
|
508
|
-
const truncated = truncateMiddleVisible(baseName, available);
|
|
509
|
-
name = isCursor ? chalk2.cyan.underline(truncated) : truncated;
|
|
190
|
+
function buildSyncTree(choices) {
|
|
191
|
+
const root = createTreeNode("root", "All targets", 1, false);
|
|
192
|
+
const platforms = /* @__PURE__ */ new Map();
|
|
193
|
+
for (let i = 0; i < choices.length; i++) {
|
|
194
|
+
const choice = choices[i];
|
|
195
|
+
let platformNode = platforms.get(choice.targetPlatform);
|
|
196
|
+
if (!platformNode) {
|
|
197
|
+
platformNode = createTreeNode(choice.targetPlatform, choice.targetPlatform, 2, false);
|
|
198
|
+
platforms.set(choice.targetPlatform, platformNode);
|
|
199
|
+
root.children.push(platformNode);
|
|
510
200
|
}
|
|
201
|
+
const skillNode = createTreeNode(
|
|
202
|
+
`${choice.targetPlatform}:${choice.skill}`,
|
|
203
|
+
choice.displayName,
|
|
204
|
+
3,
|
|
205
|
+
true,
|
|
206
|
+
[i]
|
|
207
|
+
);
|
|
208
|
+
platformNode.children.push(skillNode);
|
|
209
|
+
platformNode.leafIndices.push(i);
|
|
210
|
+
root.leafIndices.push(i);
|
|
511
211
|
}
|
|
512
|
-
return
|
|
212
|
+
return wrapWithRoot(root);
|
|
513
213
|
}
|
|
514
214
|
function truncateDescription(value, maxLen) {
|
|
515
215
|
const trimmed = value.trim();
|
|
@@ -521,8 +221,7 @@ function getSkillDescriptionSuffix(skills, node, isCursor) {
|
|
|
521
221
|
const skill = skills[node.leafIndices[0]];
|
|
522
222
|
if (!skill?.description) return "";
|
|
523
223
|
const description = truncateDescription(skill.description, 72);
|
|
524
|
-
|
|
525
|
-
return ` - ${description}`;
|
|
224
|
+
return description ? ` - ${description}` : "";
|
|
526
225
|
}
|
|
527
226
|
function getPlatformDisplay(platform) {
|
|
528
227
|
return PLATFORM_DISPLAY[platform] || platform;
|
|
@@ -536,6 +235,26 @@ function buildSpaceHint(node, selection) {
|
|
|
536
235
|
}
|
|
537
236
|
return isLeaf ? " (Space: select)" : " (Space: select all)";
|
|
538
237
|
}
|
|
238
|
+
function formatTreeNode(node, selection, isCursor, maxWidth, options = {}) {
|
|
239
|
+
const { state, selectedCount } = selection;
|
|
240
|
+
const totalCount = node.leafIndices.length;
|
|
241
|
+
const indent = " ".repeat(Math.max(0, node.depth - 1));
|
|
242
|
+
const glyph = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
|
|
243
|
+
const cursorMark = isCursor ? chalk2.cyan("\u203A ") : " ";
|
|
244
|
+
const count = totalCount > 1 ? chalk2.dim(` (${selectedCount}/${totalCount})`) : "";
|
|
245
|
+
return formatInteractiveRow({
|
|
246
|
+
cursorMark,
|
|
247
|
+
indent,
|
|
248
|
+
glyph,
|
|
249
|
+
name: node.name || "",
|
|
250
|
+
count,
|
|
251
|
+
suffixText: options.suffixText || "",
|
|
252
|
+
hintText: isCursor ? options.hintText || "" : "",
|
|
253
|
+
isCursor,
|
|
254
|
+
maxWidth,
|
|
255
|
+
styleName: (s) => chalk2.cyan.underline(s)
|
|
256
|
+
});
|
|
257
|
+
}
|
|
539
258
|
async function promptSkillsInteractive(skills, options = {}) {
|
|
540
259
|
if (skills.length === 0) return null;
|
|
541
260
|
const targetPlatforms = options.targetPlatforms || [];
|
|
@@ -564,12 +283,10 @@ async function promptSkillsInteractive(skills, options = {}) {
|
|
|
564
283
|
}
|
|
565
284
|
}
|
|
566
285
|
const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
|
|
567
|
-
|
|
286
|
+
return formatTreeNode(node, selection, isCursor, maxWidth, {
|
|
568
287
|
suffixText: `${installedSuffixText}${descriptionSuffix}`,
|
|
569
|
-
hintText: buildSpaceHint(node, selection)
|
|
570
|
-
maxWidth
|
|
288
|
+
hintText: buildSpaceHint(node, selection)
|
|
571
289
|
});
|
|
572
|
-
return formatted;
|
|
573
290
|
},
|
|
574
291
|
defaultAll: false,
|
|
575
292
|
defaultSelected
|
|
@@ -577,40 +294,15 @@ async function promptSkillsInteractive(skills, options = {}) {
|
|
|
577
294
|
if (!selectedIndices) return null;
|
|
578
295
|
const selectedSkills = selectedIndices.map((i) => skills[i]);
|
|
579
296
|
const names = selectedSkills.map((s) => s.relPath === "." ? s.suggestedSource : s.relPath);
|
|
580
|
-
enqueuePostPromptLog(
|
|
297
|
+
enqueuePostPromptLog(
|
|
298
|
+
chalk2.green(
|
|
299
|
+
`
|
|
581
300
|
\u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
|
|
582
|
-
`
|
|
301
|
+
`
|
|
302
|
+
)
|
|
303
|
+
);
|
|
583
304
|
return selectedSkills;
|
|
584
305
|
}
|
|
585
|
-
async function promptSyncTargetsInteractive(choices) {
|
|
586
|
-
if (choices.length === 0) return null;
|
|
587
|
-
const selectedIndices = await interactiveTreeSelect(choices, {
|
|
588
|
-
title: "Select sync targets",
|
|
589
|
-
subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
|
|
590
|
-
buildTree: buildSyncTree,
|
|
591
|
-
formatNode: (node, selection, isCursor, maxWidth) => {
|
|
592
|
-
if (!node.isLeaf && node.depth === 2) {
|
|
593
|
-
const display = getPlatformDisplay(node.name);
|
|
594
|
-
return formatTreeNode({ ...node, name: display }, selection, isCursor, { hintText: buildSpaceHint(node, selection), maxWidth });
|
|
595
|
-
}
|
|
596
|
-
if (node.isLeaf && node.leafIndices.length === 1) {
|
|
597
|
-
const choice = choices[node.leafIndices[0]];
|
|
598
|
-
const platformLabel = getPlatformDisplay(choice.sourcePlatform);
|
|
599
|
-
const suffixText = ` [from ${platformLabel} \xB7 ${choice.sourceTypeLabel}]`;
|
|
600
|
-
return formatTreeNode(node, selection, isCursor, { suffixText, hintText: buildSpaceHint(node, selection), maxWidth });
|
|
601
|
-
}
|
|
602
|
-
return formatTreeNode(node, selection, isCursor, { hintText: buildSpaceHint(node, selection), maxWidth });
|
|
603
|
-
},
|
|
604
|
-
defaultAll: true
|
|
605
|
-
});
|
|
606
|
-
if (!selectedIndices) return null;
|
|
607
|
-
const selected = selectedIndices.map((i) => choices[i]);
|
|
608
|
-
const summary = selected.map((c2) => `${c2.displayName}\u2192${getPlatformDisplay(c2.targetPlatform)}`).join(", ");
|
|
609
|
-
enqueuePostPromptLog(chalk2.green(`
|
|
610
|
-
\u2713 Syncing ${selected.length} target(s): ${chalk2.cyan(summary)}
|
|
611
|
-
`));
|
|
612
|
-
return selected;
|
|
613
|
-
}
|
|
614
306
|
async function promptSkillsTreeInteractive(skills, tree, options = {}) {
|
|
615
307
|
const selectedIndices = await interactiveTreeSelect(skills, {
|
|
616
308
|
title: "Select skills from markdown",
|
|
@@ -618,16 +310,23 @@ async function promptSkillsTreeInteractive(skills, tree, options = {}) {
|
|
|
618
310
|
buildTree: () => buildTreeFromSkillNodes(tree, skills.length),
|
|
619
311
|
formatNode: (node, selection, isCursor, maxWidth) => {
|
|
620
312
|
const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
|
|
621
|
-
return formatTreeNode(node, selection, isCursor,
|
|
313
|
+
return formatTreeNode(node, selection, isCursor, maxWidth, {
|
|
314
|
+
suffixText: descriptionSuffix,
|
|
315
|
+
hintText: buildSpaceHint(node, selection)
|
|
316
|
+
});
|
|
622
317
|
},
|
|
623
318
|
defaultAll: options.defaultAll !== false
|
|
624
319
|
});
|
|
625
320
|
if (!selectedIndices) return null;
|
|
626
321
|
const selectedSkills = selectedIndices.map((i) => skills[i]);
|
|
627
322
|
const names = selectedSkills.map((s) => s.displayName || s.relPath || s.suggestedSource);
|
|
628
|
-
enqueuePostPromptLog(
|
|
323
|
+
enqueuePostPromptLog(
|
|
324
|
+
chalk2.green(
|
|
325
|
+
`
|
|
629
326
|
\u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
|
|
630
|
-
`
|
|
327
|
+
`
|
|
328
|
+
)
|
|
329
|
+
);
|
|
631
330
|
return selectedSkills;
|
|
632
331
|
}
|
|
633
332
|
async function promptPlatformsInteractive(options = {}) {
|
|
@@ -639,16 +338,57 @@ async function promptPlatformsInteractive(options = {}) {
|
|
|
639
338
|
buildTree: buildPlatformTree,
|
|
640
339
|
formatNode: (node, selection, isCursor, maxWidth) => {
|
|
641
340
|
const displayName = node.name === "All Platforms" ? node.name : PLATFORM_DISPLAY[node.name] || node.name;
|
|
642
|
-
|
|
643
|
-
|
|
341
|
+
return formatTreeNode({ ...node, name: displayName }, selection, isCursor, maxWidth, {
|
|
342
|
+
hintText: buildSpaceHint(node, selection)
|
|
343
|
+
});
|
|
644
344
|
},
|
|
645
345
|
defaultAll: options.defaultAll !== false
|
|
646
346
|
});
|
|
647
347
|
if (!selectedIndices) return null;
|
|
648
348
|
const selected = selectedIndices.map((i) => platforms[i]);
|
|
649
349
|
const names = selected.map((p) => PLATFORM_DISPLAY[p] || p);
|
|
650
|
-
enqueuePostPromptLog(
|
|
350
|
+
enqueuePostPromptLog(
|
|
351
|
+
chalk2.green(
|
|
352
|
+
`
|
|
651
353
|
\u2713 Installing to ${selected.length} platform${selected.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
|
|
354
|
+
`
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
return selected;
|
|
358
|
+
}
|
|
359
|
+
async function promptSyncTargetsInteractive(choices) {
|
|
360
|
+
if (choices.length === 0) return null;
|
|
361
|
+
const selectedIndices = await interactiveTreeSelect(choices, {
|
|
362
|
+
title: "Select sync targets",
|
|
363
|
+
subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
|
|
364
|
+
buildTree: buildSyncTree,
|
|
365
|
+
formatNode: (node, selection, isCursor, maxWidth) => {
|
|
366
|
+
if (!node.isLeaf && node.depth === 2) {
|
|
367
|
+
const display = getPlatformDisplay(node.name);
|
|
368
|
+
return formatTreeNode({ ...node, name: display }, selection, isCursor, maxWidth, {
|
|
369
|
+
hintText: buildSpaceHint(node, selection)
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (node.isLeaf && node.leafIndices.length === 1) {
|
|
373
|
+
const choice = choices[node.leafIndices[0]];
|
|
374
|
+
const platformLabel = getPlatformDisplay(choice.sourcePlatform);
|
|
375
|
+
const suffixText = ` [from ${platformLabel} \xB7 ${choice.sourceTypeLabel}]`;
|
|
376
|
+
return formatTreeNode(node, selection, isCursor, maxWidth, {
|
|
377
|
+
suffixText,
|
|
378
|
+
hintText: buildSpaceHint(node, selection)
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return formatTreeNode(node, selection, isCursor, maxWidth, {
|
|
382
|
+
hintText: buildSpaceHint(node, selection)
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
defaultAll: true
|
|
386
|
+
});
|
|
387
|
+
if (!selectedIndices) return null;
|
|
388
|
+
const selected = selectedIndices.map((i) => choices[i]);
|
|
389
|
+
const summary = selected.map((c2) => `${c2.displayName}\u2192${getPlatformDisplay(c2.targetPlatform)}`).join(", ");
|
|
390
|
+
enqueuePostPromptLog(chalk2.green(`
|
|
391
|
+
\u2713 Syncing ${selected.length} target(s): ${chalk2.cyan(summary)}
|
|
652
392
|
`));
|
|
653
393
|
return selected;
|
|
654
394
|
}
|
|
@@ -1876,7 +1616,7 @@ import chalk5 from "chalk";
|
|
|
1876
1616
|
import { PLATFORMS as PLATFORMS3, listAllSkills, listSkills as listSkills2 } from "@skild/core";
|
|
1877
1617
|
|
|
1878
1618
|
// src/utils/table-utils.ts
|
|
1879
|
-
import
|
|
1619
|
+
import stringWidth from "string-width";
|
|
1880
1620
|
import chalk4 from "chalk";
|
|
1881
1621
|
var BORDERS = {
|
|
1882
1622
|
rounded: {
|
|
@@ -1920,7 +1660,7 @@ var BORDERS = {
|
|
|
1920
1660
|
}
|
|
1921
1661
|
};
|
|
1922
1662
|
function getWidth(str) {
|
|
1923
|
-
return
|
|
1663
|
+
return stringWidth(str);
|
|
1924
1664
|
}
|
|
1925
1665
|
function pad(str, targetWidth, align = "left") {
|
|
1926
1666
|
const currentWidth = getWidth(str);
|
|
@@ -1942,7 +1682,7 @@ function truncate(str, maxWidth) {
|
|
|
1942
1682
|
let result = "";
|
|
1943
1683
|
let width = 0;
|
|
1944
1684
|
for (const char of plain) {
|
|
1945
|
-
const charWidth =
|
|
1685
|
+
const charWidth = stringWidth(char);
|
|
1946
1686
|
if (width + charWidth + 1 > maxWidth) break;
|
|
1947
1687
|
result += char;
|
|
1948
1688
|
width += charWidth;
|
|
@@ -2364,9 +2104,9 @@ import chalk11 from "chalk";
|
|
|
2364
2104
|
import { fetchWithTimeout as fetchWithTimeout3, resolveRegistryUrl as resolveRegistryUrl2, SkildError as SkildError6 } from "@skild/core";
|
|
2365
2105
|
|
|
2366
2106
|
// src/utils/prompt.ts
|
|
2367
|
-
import
|
|
2107
|
+
import readline from "readline";
|
|
2368
2108
|
async function promptLine(question, defaultValue) {
|
|
2369
|
-
const rl =
|
|
2109
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
2370
2110
|
try {
|
|
2371
2111
|
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
2372
2112
|
const answer = await new Promise((resolve) => rl.question(`${question}${suffix}: `, resolve));
|
|
@@ -2386,7 +2126,7 @@ async function promptPassword(question) {
|
|
|
2386
2126
|
const wasRaw = Boolean(stdin.isRaw);
|
|
2387
2127
|
stdin.setRawMode(true);
|
|
2388
2128
|
stdin.resume();
|
|
2389
|
-
|
|
2129
|
+
readline.emitKeypressEvents(stdin);
|
|
2390
2130
|
const buf = [];
|
|
2391
2131
|
return await new Promise((resolve, reject) => {
|
|
2392
2132
|
function cleanup() {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type SelectionState = 'all' | 'none' | 'partial';
|
|
2
|
+
interface SelectionInfo {
|
|
3
|
+
state: SelectionState;
|
|
4
|
+
selectedCount: number;
|
|
5
|
+
}
|
|
6
|
+
type TreeNode = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
depth: number;
|
|
10
|
+
children: TreeNode[];
|
|
11
|
+
leafIndices: number[];
|
|
12
|
+
isLeaf: boolean;
|
|
13
|
+
};
|
|
14
|
+
interface TreeSelectOptions<T> {
|
|
15
|
+
title: string;
|
|
16
|
+
subtitle: string;
|
|
17
|
+
buildTree: (items: T[]) => TreeNode;
|
|
18
|
+
formatNode: (node: TreeNode, selection: SelectionInfo, isCursor: boolean, maxWidth: number) => string;
|
|
19
|
+
defaultAll: boolean;
|
|
20
|
+
defaultSelected?: Set<number>;
|
|
21
|
+
}
|
|
22
|
+
declare function enqueuePostPromptLog(message: string): void;
|
|
23
|
+
declare function flushInteractiveUiNow(): void;
|
|
24
|
+
declare function interactiveTreeSelect<T>(items: T[], options: TreeSelectOptions<T>): Promise<number[] | null>;
|
|
25
|
+
declare function formatInteractiveRow(input: {
|
|
26
|
+
cursorMark: string;
|
|
27
|
+
indent: string;
|
|
28
|
+
glyph: string;
|
|
29
|
+
name: string;
|
|
30
|
+
count: string;
|
|
31
|
+
suffixText?: string;
|
|
32
|
+
hintText?: string;
|
|
33
|
+
isCursor: boolean;
|
|
34
|
+
maxWidth: number;
|
|
35
|
+
styleName?: (s: string) => string;
|
|
36
|
+
}): string;
|
|
37
|
+
|
|
38
|
+
export { type SelectionInfo, type SelectionState, type TreeNode, type TreeSelectOptions, enqueuePostPromptLog, flushInteractiveUiNow, formatInteractiveRow, interactiveTreeSelect };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skild",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.16",
|
|
4
4
|
"description": "The npm for Agent Skills — Discover, install, manage, and publish AI Agent Skills with ease.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"string-width": "^8.1.0",
|
|
44
44
|
"tar": "^7.4.3",
|
|
45
45
|
"unified": "^11.0.4",
|
|
46
|
-
"@skild/core": "^0.10.
|
|
46
|
+
"@skild/core": "^0.10.16"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/mdast": "^4.0.4",
|
|
@@ -55,9 +55,10 @@
|
|
|
55
55
|
"node": ">=18"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
|
-
"build": "tsup src/index.ts --format esm --dts --clean --silent",
|
|
58
|
+
"build": "tsup src/index.ts src/ui/interactive-tree-prompt.ts --format esm --dts --clean --silent",
|
|
59
59
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
60
60
|
"start": "pnpm exec node dist/index.js",
|
|
61
|
+
"selfcheck:ui": "pnpm build && node scripts/interactive-tree-prompt-selfcheck.mjs",
|
|
61
62
|
"typecheck": "pnpm -C ../core build && tsc -p tsconfig.json --noEmit"
|
|
62
63
|
}
|
|
63
64
|
}
|