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.
@@ -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 altScreenRefCount = 0;
94
- var altScreenActive = false;
95
- var altScreenExitTimer = null;
96
- var altScreenStdout = null;
97
- var postPromptLogs = [];
98
- function enqueuePostPromptLog(message) {
99
- postPromptLogs.push(message);
100
- }
101
- function flushPostPromptLogs(stdout) {
102
- if (altScreenActive) return;
103
- if (postPromptLogs.length === 0) return;
104
- for (const msg of postPromptLogs.splice(0)) {
105
- stdout.write(msg.endsWith("\n") ? msg : msg + "\n");
106
- }
107
- }
108
- function enterAltScreen(stdout) {
109
- if (altScreenExitTimer) {
110
- clearTimeout(altScreenExitTimer);
111
- altScreenExitTimer = null;
112
- }
113
- altScreenStdout = stdout;
114
- if (!altScreenActive) {
115
- stdout.write("\x1B[?1049h");
116
- stdout.write("\x1B[H");
117
- stdout.write("\x1B[2J");
118
- altScreenActive = true;
119
- }
120
- altScreenRefCount += 1;
121
- }
122
- function exitAltScreenDeferred() {
123
- if (altScreenExitTimer) return;
124
- const stdout = altScreenStdout;
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 createTreeNode(id, name, depth, isLeaf, leafIndices = []) {
242
- return { id, name, depth, children: [], leafIndices, isLeaf };
243
- }
244
- function wrapWithRoot(allNode) {
245
- return { id: "", name: ".", depth: 0, children: [allNode], leafIndices: [...allNode.leafIndices], isLeaf: false };
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 result;
188
+ return wrapWithRoot(allNode);
451
189
  }
452
- function truncateMiddleVisible(value, maxLen) {
453
- if (stringWidth(value) <= maxLen) return value;
454
- if (maxLen <= 1) return "\u2026";
455
- const leftMax = Math.max(1, Math.floor((maxLen - 1) / 2));
456
- const rightMax = Math.max(1, maxLen - 1 - leftMax);
457
- const left = truncateVisible(value, leftMax).replace(/…$/, "");
458
- let width = 0;
459
- let right = "";
460
- for (let i = value.length - 1; i >= 0; i--) {
461
- const ch = value[i];
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 `${prefix}${name}${count}${suffix}${hint}`;
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
- if (!description) return "";
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
- const formatted = formatTreeNode(node, selection, isCursor, {
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(chalk2.green(`
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, { suffixText: descriptionSuffix, hintText: buildSpaceHint(node, selection), maxWidth });
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(chalk2.green(`
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
- const modifiedNode = { ...node, name: displayName };
643
- return formatTreeNode(modifiedNode, selection, isCursor, { hintText: buildSpaceHint(node, selection), maxWidth });
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(chalk2.green(`
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 stringWidth2 from "string-width";
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 stringWidth2(str);
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 = stringWidth2(char);
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 readline2 from "readline";
2107
+ import readline from "readline";
2368
2108
  async function promptLine(question, defaultValue) {
2369
- const rl = readline2.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
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
- readline2.emitKeypressEvents(stdin);
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 };
@@ -0,0 +1,12 @@
1
+ import {
2
+ enqueuePostPromptLog,
3
+ flushInteractiveUiNow,
4
+ formatInteractiveRow,
5
+ interactiveTreeSelect
6
+ } from "../chunk-U4SVSURE.js";
7
+ export {
8
+ enqueuePostPromptLog,
9
+ flushInteractiveUiNow,
10
+ formatInteractiveRow,
11
+ interactiveTreeSelect
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.10.15",
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.3"
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
  }