skild 0.10.14 → 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,10 +92,41 @@ 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";
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
+ }
129
+ }
93
130
  function buildSkillTree(skills) {
94
131
  const allNode = createTreeNode("all", "All Skills", 1, false);
95
132
  for (let i = 0; i < skills.length; i++) {
@@ -112,39 +149,6 @@ function buildSkillTree(skills) {
112
149
  collapseIntermediateNodes(allNode);
113
150
  return wrapWithRoot(allNode);
114
151
  }
115
- function buildSyncTree(choices) {
116
- const root = createTreeNode("root", "All targets", 1, false);
117
- const platforms = /* @__PURE__ */ new Map();
118
- for (let i = 0; i < choices.length; i++) {
119
- const choice = choices[i];
120
- let platformNode = platforms.get(choice.targetPlatform);
121
- if (!platformNode) {
122
- platformNode = createTreeNode(choice.targetPlatform, choice.targetPlatform, 2, false);
123
- platforms.set(choice.targetPlatform, platformNode);
124
- root.children.push(platformNode);
125
- }
126
- const skillNode = createTreeNode(
127
- `${choice.targetPlatform}:${choice.skill}`,
128
- choice.displayName,
129
- 3,
130
- true,
131
- [i]
132
- );
133
- platformNode.children.push(skillNode);
134
- platformNode.leafIndices.push(i);
135
- root.leafIndices.push(i);
136
- }
137
- return wrapWithRoot(root);
138
- }
139
- function buildPlatformTree(items) {
140
- const allNode = createTreeNode("all", "All Platforms", 1, false);
141
- for (let i = 0; i < items.length; i++) {
142
- const platform = items[i].platform;
143
- allNode.children.push(createTreeNode(platform, platform, 2, true, [i]));
144
- allNode.leafIndices.push(i);
145
- }
146
- return wrapWithRoot(allNode);
147
- }
148
152
  function buildTreeFromSkillNodes(nodes, totalSkills) {
149
153
  const allNode = createTreeNode("all", "All Skills", 1, false);
150
154
  const attach = (node, depth) => {
@@ -174,280 +178,38 @@ function buildTreeFromSkillNodes(nodes, totalSkills) {
174
178
  }
175
179
  return wrapWithRoot(allNode);
176
180
  }
177
- function createTreeNode(id, name, depth, isLeaf, leafIndices = []) {
178
- return { id, name, depth, children: [], leafIndices, isLeaf };
179
- }
180
- function wrapWithRoot(allNode) {
181
- return { id: "", name: ".", depth: 0, children: [allNode], leafIndices: [...allNode.leafIndices], isLeaf: false };
182
- }
183
- function collapseIntermediateNodes(allNode) {
184
- while (allNode.children.length === 1 && !allNode.children[0].isLeaf && allNode.children[0].children.length > 0) {
185
- const singleChild = allNode.children[0];
186
- for (const grandchild of singleChild.children) {
187
- adjustDepth(grandchild, -1);
188
- }
189
- allNode.children = singleChild.children;
190
- }
191
- }
192
- function adjustDepth(node, delta) {
193
- node.depth += delta;
194
- for (const child of node.children) {
195
- adjustDepth(child, delta);
196
- }
197
- }
198
- function flattenTree(root) {
199
- const result = [];
200
- function walk(node) {
201
- if (node.id !== "") result.push(node);
202
- for (const child of node.children) walk(child);
203
- }
204
- for (const child of root.children) walk(child);
205
- return result;
206
- }
207
- function getNodeSelection(node, selected) {
208
- const total = node.leafIndices.length;
209
- if (total === 0) return { state: "none", selectedCount: 0 };
210
- let selectedCount = 0;
211
- for (const idx of node.leafIndices) {
212
- if (selected.has(idx)) selectedCount++;
213
- }
214
- let state = "none";
215
- if (selectedCount === total) state = "all";
216
- else if (selectedCount > 0) state = "partial";
217
- return { state, selectedCount };
218
- }
219
- function createRenderer(title, subtitle, flatNodes, selected, getCursor, getViewOffset, viewHeight, maxWidth, formatNode) {
220
- function renderContent() {
221
- const lines = [];
222
- lines.push(chalk2.bold.cyan(title));
223
- lines.push(chalk2.dim(subtitle));
224
- lines.push("");
225
- const cursor = getCursor();
226
- const offset = getViewOffset();
227
- const end = Math.min(flatNodes.length, offset + viewHeight);
228
- for (let i = offset; i < end; i++) {
229
- const node = flatNodes[i];
230
- const selection = getNodeSelection(node, selected);
231
- lines.push(formatNode(node, selection, i === cursor, maxWidth));
232
- }
233
- lines.push(chalk2.dim(`
234
- Space toggle \u2022 Enter confirm \u2022 A select all \u2022 Ctrl+C cancel`));
235
- lines.push(chalk2.dim(`Showing ${end - offset}/${flatNodes.length} (offset ${offset + 1})`));
236
- return lines;
237
- }
238
- function getLineCount() {
239
- return 5 + Math.min(viewHeight, flatNodes.length);
240
- }
241
- return { renderContent, getLineCount };
242
- }
243
- function writeToTerminal(stdout, lines) {
244
- for (const line of lines) {
245
- stdout.write(line + "\n");
246
- }
247
- }
248
- function clearAndRerender(stdout, lineCount, lines) {
249
- stdout.write("\x1B[?25l");
250
- stdout.write("\x1B[H");
251
- stdout.write("\x1B[2J");
252
- writeToTerminal(stdout, lines);
253
- }
254
- async function interactiveTreeSelect(items, options) {
255
- const { title, subtitle, buildTree, formatNode, defaultAll, defaultSelected } = options;
256
- const root = buildTree(items);
257
- const flatNodes = flattenTree(root);
258
- if (flatNodes.length === 0) return null;
259
- const stdout = process.stdout;
260
- const cols = typeof stdout.columns === "number" ? stdout.columns : 120;
261
- const rows = typeof stdout.rows === "number" ? stdout.rows : 24;
262
- const viewHeight = Math.max(5, rows - 5);
263
- const maxWidth = Math.max(40, cols - 2);
264
- const selected = /* @__PURE__ */ new Set();
265
- if (defaultSelected) {
266
- for (const idx of defaultSelected) selected.add(idx);
267
- } else if (defaultAll) {
268
- for (let i = 0; i < items.length; i++) selected.add(i);
269
- }
270
- let cursor = 0;
271
- let viewOffset = 0;
272
- const stdin = process.stdin;
273
- if (!stdin.isTTY || !stdout.isTTY) {
274
- return defaultAll ? Array.from(selected) : null;
275
- }
276
- const useAltScreen = true;
277
- const wasRaw = Boolean(stdin.isRaw);
278
- stdin.setRawMode(true);
279
- stdin.resume();
280
- readline.emitKeypressEvents(stdin);
281
- if (useAltScreen) {
282
- stdout.write("\x1B[?1049h");
283
- }
284
- const renderer = createRenderer(
285
- title,
286
- subtitle,
287
- flatNodes,
288
- selected,
289
- () => cursor,
290
- () => viewOffset,
291
- viewHeight,
292
- maxWidth,
293
- (node, selection, isCursor) => formatNode(node, selection, isCursor, maxWidth)
294
- );
295
- writeToTerminal(stdout, renderer.renderContent());
296
- return new Promise((resolve) => {
297
- function cleanup(clear = false) {
298
- if (clear && !useAltScreen) {
299
- const lineCount = renderer.getLineCount();
300
- stdout.write(`\x1B[${lineCount}A`);
301
- stdout.write("\x1B[0J");
302
- }
303
- stdin.setRawMode(wasRaw);
304
- stdin.pause();
305
- stdin.removeListener("keypress", onKeypress);
306
- stdout.write("\x1B[?25h");
307
- if (useAltScreen) {
308
- stdout.write("\x1B[?1049l");
309
- }
310
- }
311
- function rerender() {
312
- clearAndRerender(stdout, renderer.getLineCount(), renderer.renderContent());
313
- }
314
- function toggleNode(node) {
315
- const { state } = getNodeSelection(node, selected);
316
- const shouldSelectAll = state !== "all";
317
- if (shouldSelectAll) {
318
- for (const idx of node.leafIndices) selected.add(idx);
319
- } else {
320
- for (const idx of node.leafIndices) selected.delete(idx);
321
- }
322
- }
323
- function toggleAll() {
324
- if (selected.size === items.length) {
325
- selected.clear();
326
- } else {
327
- for (let i = 0; i < items.length; i++) selected.add(i);
328
- }
329
- }
330
- function onKeypress(_str, key) {
331
- if (key.ctrl && key.name === "c") {
332
- cleanup(true);
333
- resolve(null);
334
- return;
335
- }
336
- if (key.name === "return" || key.name === "enter") {
337
- cleanup(true);
338
- resolve(selected.size > 0 ? Array.from(selected) : null);
339
- return;
340
- }
341
- if (key.name === "up") {
342
- cursor = (cursor - 1 + flatNodes.length) % flatNodes.length;
343
- if (cursor < viewOffset) viewOffset = cursor;
344
- rerender();
345
- return;
346
- }
347
- if (key.name === "down") {
348
- cursor = (cursor + 1) % flatNodes.length;
349
- if (cursor >= viewOffset + viewHeight) viewOffset = cursor - viewHeight + 1;
350
- rerender();
351
- return;
352
- }
353
- if (key.name === "space") {
354
- toggleNode(flatNodes[cursor]);
355
- rerender();
356
- return;
357
- }
358
- if (key.name === "a") {
359
- toggleAll();
360
- rerender();
361
- return;
362
- }
363
- }
364
- stdin.on("keypress", onKeypress);
365
- });
366
- }
367
- var PLATFORM_DISPLAY = {
368
- claude: "Claude",
369
- codex: "Codex",
370
- copilot: "Copilot",
371
- antigravity: "Antigravity",
372
- opencode: "OpenCode",
373
- cursor: "Cursor",
374
- windsurf: "Windsurf"
375
- };
376
- function truncateVisible(value, maxLen) {
377
- let width = 0;
378
- let result = "";
379
- for (const ch of value) {
380
- const w = stringWidth(ch);
381
- if (width + w > maxLen - 1) {
382
- result += "\u2026";
383
- return result;
384
- }
385
- width += w;
386
- 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);
387
187
  }
388
- return result;
188
+ return wrapWithRoot(allNode);
389
189
  }
390
- function truncateMiddleVisible(value, maxLen) {
391
- if (stringWidth(value) <= maxLen) return value;
392
- if (maxLen <= 1) return "\u2026";
393
- const leftMax = Math.max(1, Math.floor((maxLen - 1) / 2));
394
- const rightMax = Math.max(1, maxLen - 1 - leftMax);
395
- const left = truncateVisible(value, leftMax).replace(/…$/, "");
396
- let width = 0;
397
- let right = "";
398
- for (let i = value.length - 1; i >= 0; i--) {
399
- const ch = value[i];
400
- const w = stringWidth(ch);
401
- if (width + w > rightMax) break;
402
- width += w;
403
- right = ch + right;
404
- }
405
- if (!right) right = value[value.length - 1] || "";
406
- return `${left}\u2026${right}`;
407
- }
408
- function formatTreeNode(node, selection, isCursor, options = {}) {
409
- const { state, selectedCount } = selection;
410
- const totalCount = node.leafIndices.length;
411
- const indent = " ".repeat(node.depth - 1);
412
- const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
413
- const baseName = node.name || "";
414
- let name = isCursor ? chalk2.cyan.underline(baseName) : baseName;
415
- const cursorMark = isCursor ? chalk2.cyan("\u203A ") : " ";
416
- let count = "";
417
- if (totalCount > 1) {
418
- count = chalk2.dim(` (${selectedCount}/${totalCount})`);
419
- }
420
- let rawSuffix = options.suffixText || "";
421
- let rawHint = isCursor ? options.hintText || "" : "";
422
- let hint = rawHint ? chalk2.dim(rawHint) : "";
423
- let suffix = rawSuffix ? chalk2.dim(rawSuffix) : "";
424
- const prefix = `${cursorMark}${indent}${checkbox} `;
425
- const maxWidth = options.maxWidth;
426
- if (maxWidth) {
427
- const fixedWidth = stringWidth(prefix) + stringWidth(count);
428
- const fits = (n, s, h) => {
429
- const nStyled = isCursor ? chalk2.cyan.underline(n) : n;
430
- const sStyled = s ? chalk2.dim(s) : "";
431
- const hStyled = h ? chalk2.dim(h) : "";
432
- return stringWidth(`${prefix}${nStyled}${count}${sStyled}${hStyled}`) <= maxWidth;
433
- };
434
- if (rawSuffix && !fits(baseName, rawSuffix, rawHint)) {
435
- const available = Math.max(1, maxWidth - fixedWidth - stringWidth(baseName) - stringWidth(rawHint));
436
- rawSuffix = truncateVisible(rawSuffix, available);
437
- suffix = chalk2.dim(rawSuffix);
438
- }
439
- if (rawHint && !fits(baseName, rawSuffix, rawHint)) {
440
- const available = Math.max(1, maxWidth - fixedWidth - stringWidth(baseName) - stringWidth(rawSuffix));
441
- rawHint = truncateVisible(rawHint, available);
442
- hint = chalk2.dim(rawHint);
443
- }
444
- if (!fits(baseName, rawSuffix, rawHint)) {
445
- const available = Math.max(1, maxWidth - fixedWidth - stringWidth(rawSuffix) - stringWidth(rawHint));
446
- const truncated = truncateMiddleVisible(baseName, available);
447
- 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);
448
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);
449
211
  }
450
- return `${prefix}${name}${count}${suffix}${hint}`;
212
+ return wrapWithRoot(root);
451
213
  }
452
214
  function truncateDescription(value, maxLen) {
453
215
  const trimmed = value.trim();
@@ -459,8 +221,7 @@ function getSkillDescriptionSuffix(skills, node, isCursor) {
459
221
  const skill = skills[node.leafIndices[0]];
460
222
  if (!skill?.description) return "";
461
223
  const description = truncateDescription(skill.description, 72);
462
- if (!description) return "";
463
- return ` - ${description}`;
224
+ return description ? ` - ${description}` : "";
464
225
  }
465
226
  function getPlatformDisplay(platform) {
466
227
  return PLATFORM_DISPLAY[platform] || platform;
@@ -474,6 +235,26 @@ function buildSpaceHint(node, selection) {
474
235
  }
475
236
  return isLeaf ? " (Space: select)" : " (Space: select all)";
476
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
+ }
477
258
  async function promptSkillsInteractive(skills, options = {}) {
478
259
  if (skills.length === 0) return null;
479
260
  const targetPlatforms = options.targetPlatforms || [];
@@ -502,12 +283,10 @@ async function promptSkillsInteractive(skills, options = {}) {
502
283
  }
503
284
  }
504
285
  const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
505
- const formatted = formatTreeNode(node, selection, isCursor, {
286
+ return formatTreeNode(node, selection, isCursor, maxWidth, {
506
287
  suffixText: `${installedSuffixText}${descriptionSuffix}`,
507
- hintText: buildSpaceHint(node, selection),
508
- maxWidth
288
+ hintText: buildSpaceHint(node, selection)
509
289
  });
510
- return formatted;
511
290
  },
512
291
  defaultAll: false,
513
292
  defaultSelected
@@ -515,40 +294,15 @@ async function promptSkillsInteractive(skills, options = {}) {
515
294
  if (!selectedIndices) return null;
516
295
  const selectedSkills = selectedIndices.map((i) => skills[i]);
517
296
  const names = selectedSkills.map((s) => s.relPath === "." ? s.suggestedSource : s.relPath);
518
- console.log(chalk2.green(`
297
+ enqueuePostPromptLog(
298
+ chalk2.green(
299
+ `
519
300
  \u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
520
- `));
301
+ `
302
+ )
303
+ );
521
304
  return selectedSkills;
522
305
  }
523
- async function promptSyncTargetsInteractive(choices) {
524
- if (choices.length === 0) return null;
525
- const selectedIndices = await interactiveTreeSelect(choices, {
526
- title: "Select sync targets",
527
- subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
528
- buildTree: buildSyncTree,
529
- formatNode: (node, selection, isCursor, maxWidth) => {
530
- if (!node.isLeaf && node.depth === 2) {
531
- const display = getPlatformDisplay(node.name);
532
- return formatTreeNode({ ...node, name: display }, selection, isCursor, { hintText: buildSpaceHint(node, selection), maxWidth });
533
- }
534
- if (node.isLeaf && node.leafIndices.length === 1) {
535
- const choice = choices[node.leafIndices[0]];
536
- const platformLabel = getPlatformDisplay(choice.sourcePlatform);
537
- const suffixText = ` [from ${platformLabel} \xB7 ${choice.sourceTypeLabel}]`;
538
- return formatTreeNode(node, selection, isCursor, { suffixText, hintText: buildSpaceHint(node, selection), maxWidth });
539
- }
540
- return formatTreeNode(node, selection, isCursor, { hintText: buildSpaceHint(node, selection), maxWidth });
541
- },
542
- defaultAll: true
543
- });
544
- if (!selectedIndices) return null;
545
- const selected = selectedIndices.map((i) => choices[i]);
546
- const summary = selected.map((c2) => `${c2.displayName}\u2192${getPlatformDisplay(c2.targetPlatform)}`).join(", ");
547
- console.log(chalk2.green(`
548
- \u2713 Syncing ${selected.length} target(s): ${chalk2.cyan(summary)}
549
- `));
550
- return selected;
551
- }
552
306
  async function promptSkillsTreeInteractive(skills, tree, options = {}) {
553
307
  const selectedIndices = await interactiveTreeSelect(skills, {
554
308
  title: "Select skills from markdown",
@@ -556,16 +310,23 @@ async function promptSkillsTreeInteractive(skills, tree, options = {}) {
556
310
  buildTree: () => buildTreeFromSkillNodes(tree, skills.length),
557
311
  formatNode: (node, selection, isCursor, maxWidth) => {
558
312
  const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
559
- 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
+ });
560
317
  },
561
318
  defaultAll: options.defaultAll !== false
562
319
  });
563
320
  if (!selectedIndices) return null;
564
321
  const selectedSkills = selectedIndices.map((i) => skills[i]);
565
322
  const names = selectedSkills.map((s) => s.displayName || s.relPath || s.suggestedSource);
566
- console.log(chalk2.green(`
323
+ enqueuePostPromptLog(
324
+ chalk2.green(
325
+ `
567
326
  \u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
568
- `));
327
+ `
328
+ )
329
+ );
569
330
  return selectedSkills;
570
331
  }
571
332
  async function promptPlatformsInteractive(options = {}) {
@@ -577,16 +338,57 @@ async function promptPlatformsInteractive(options = {}) {
577
338
  buildTree: buildPlatformTree,
578
339
  formatNode: (node, selection, isCursor, maxWidth) => {
579
340
  const displayName = node.name === "All Platforms" ? node.name : PLATFORM_DISPLAY[node.name] || node.name;
580
- const modifiedNode = { ...node, name: displayName };
581
- 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
+ });
582
344
  },
583
345
  defaultAll: options.defaultAll !== false
584
346
  });
585
347
  if (!selectedIndices) return null;
586
348
  const selected = selectedIndices.map((i) => platforms[i]);
587
349
  const names = selected.map((p) => PLATFORM_DISPLAY[p] || p);
588
- console.log(chalk2.green(`
350
+ enqueuePostPromptLog(
351
+ chalk2.green(
352
+ `
589
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)}
590
392
  `));
591
393
  return selected;
592
394
  }
@@ -1522,6 +1324,8 @@ async function promptSelections(ctx) {
1522
1324
  }
1523
1325
  ctx.targets = selectedPlatforms;
1524
1326
  ctx.needsPlatformPrompt = false;
1327
+ flushInteractiveUiNow();
1328
+ if (ctx.spinner) ctx.spinner.start();
1525
1329
  }
1526
1330
  return true;
1527
1331
  }
@@ -1588,6 +1392,7 @@ async function promptSelections(ctx) {
1588
1392
  ctx.targets = selectedPlatforms;
1589
1393
  ctx.needsPlatformPrompt = false;
1590
1394
  }
1395
+ flushInteractiveUiNow();
1591
1396
  if (ctx.spinner) ctx.spinner.start();
1592
1397
  } else {
1593
1398
  ctx.selectedSkills = discoveredSkills;
@@ -1811,7 +1616,7 @@ import chalk5 from "chalk";
1811
1616
  import { PLATFORMS as PLATFORMS3, listAllSkills, listSkills as listSkills2 } from "@skild/core";
1812
1617
 
1813
1618
  // src/utils/table-utils.ts
1814
- import stringWidth2 from "string-width";
1619
+ import stringWidth from "string-width";
1815
1620
  import chalk4 from "chalk";
1816
1621
  var BORDERS = {
1817
1622
  rounded: {
@@ -1855,7 +1660,7 @@ var BORDERS = {
1855
1660
  }
1856
1661
  };
1857
1662
  function getWidth(str) {
1858
- return stringWidth2(str);
1663
+ return stringWidth(str);
1859
1664
  }
1860
1665
  function pad(str, targetWidth, align = "left") {
1861
1666
  const currentWidth = getWidth(str);
@@ -1877,7 +1682,7 @@ function truncate(str, maxWidth) {
1877
1682
  let result = "";
1878
1683
  let width = 0;
1879
1684
  for (const char of plain) {
1880
- const charWidth = stringWidth2(char);
1685
+ const charWidth = stringWidth(char);
1881
1686
  if (width + charWidth + 1 > maxWidth) break;
1882
1687
  result += char;
1883
1688
  width += charWidth;
@@ -2299,9 +2104,9 @@ import chalk11 from "chalk";
2299
2104
  import { fetchWithTimeout as fetchWithTimeout3, resolveRegistryUrl as resolveRegistryUrl2, SkildError as SkildError6 } from "@skild/core";
2300
2105
 
2301
2106
  // src/utils/prompt.ts
2302
- import readline2 from "readline";
2107
+ import readline from "readline";
2303
2108
  async function promptLine(question, defaultValue) {
2304
- 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 });
2305
2110
  try {
2306
2111
  const suffix = defaultValue ? ` (${defaultValue})` : "";
2307
2112
  const answer = await new Promise((resolve) => rl.question(`${question}${suffix}: `, resolve));
@@ -2321,7 +2126,7 @@ async function promptPassword(question) {
2321
2126
  const wasRaw = Boolean(stdin.isRaw);
2322
2127
  stdin.setRawMode(true);
2323
2128
  stdin.resume();
2324
- readline2.emitKeypressEvents(stdin);
2129
+ readline.emitKeypressEvents(stdin);
2325
2130
  const buf = [];
2326
2131
  return await new Promise((resolve, reject) => {
2327
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.14",
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
  }