skild 0.10.8 → 0.10.10

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.
Files changed (2) hide show
  1. package/dist/index.js +135 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -86,10 +86,31 @@ var logger = {
86
86
  };
87
87
 
88
88
  // src/utils/interactive-select.ts
89
- import { checkbox } from "@inquirer/prompts";
90
89
  import readline from "readline";
91
90
  import chalk2 from "chalk";
92
91
  import { PLATFORMS } from "@skild/core";
92
+ function buildSkillTree(skills) {
93
+ const allNode = createTreeNode("all", "All Skills", 1, false);
94
+ for (let i = 0; i < skills.length; i++) {
95
+ const relPath = skills[i].relPath;
96
+ const parts = relPath === "." ? ["."] : relPath.split("/").filter(Boolean);
97
+ allNode.leafIndices.push(i);
98
+ let current = allNode;
99
+ for (let d = 0; d < parts.length; d++) {
100
+ const part = parts[d];
101
+ const nodeId = parts.slice(0, d + 1).join("/");
102
+ let child = current.children.find((c2) => c2.name === part);
103
+ if (!child) {
104
+ child = createTreeNode(nodeId, part, d + 2, d === parts.length - 1);
105
+ current.children.push(child);
106
+ }
107
+ child.leafIndices.push(i);
108
+ current = child;
109
+ }
110
+ }
111
+ collapseIntermediateNodes(allNode);
112
+ return wrapWithRoot(allNode);
113
+ }
93
114
  function buildSyncTree(choices) {
94
115
  const root = createTreeNode("root", "All targets", 1, false);
95
116
  const platforms = /* @__PURE__ */ new Map();
@@ -123,12 +144,56 @@ function buildPlatformTree(items) {
123
144
  }
124
145
  return wrapWithRoot(allNode);
125
146
  }
147
+ function buildTreeFromSkillNodes(nodes, totalSkills) {
148
+ const allNode = createTreeNode("all", "All Skills", 1, false);
149
+ const attach = (node, depth) => {
150
+ const treeNode = createTreeNode(
151
+ node.id,
152
+ node.label,
153
+ depth,
154
+ Boolean(node.skillIndex != null && (!node.children || node.children.length === 0)),
155
+ node.skillIndex != null ? [node.skillIndex] : []
156
+ );
157
+ if (node.children?.length) {
158
+ for (const child of node.children) {
159
+ const childNode = attach(child, depth + 1);
160
+ treeNode.children.push(childNode);
161
+ treeNode.leafIndices.push(...childNode.leafIndices);
162
+ }
163
+ }
164
+ return treeNode;
165
+ };
166
+ for (const node of nodes) {
167
+ const childNode = attach(node, 2);
168
+ allNode.children.push(childNode);
169
+ allNode.leafIndices.push(...childNode.leafIndices);
170
+ }
171
+ if (allNode.leafIndices.length === 0 && totalSkills > 0) {
172
+ for (let i = 0; i < totalSkills; i++) allNode.leafIndices.push(i);
173
+ }
174
+ return wrapWithRoot(allNode);
175
+ }
126
176
  function createTreeNode(id, name, depth, isLeaf, leafIndices = []) {
127
177
  return { id, name, depth, children: [], leafIndices, isLeaf };
128
178
  }
129
179
  function wrapWithRoot(allNode) {
130
180
  return { id: "", name: ".", depth: 0, children: [allNode], leafIndices: [...allNode.leafIndices], isLeaf: false };
131
181
  }
182
+ function collapseIntermediateNodes(allNode) {
183
+ while (allNode.children.length === 1 && !allNode.children[0].isLeaf && allNode.children[0].children.length > 0) {
184
+ const singleChild = allNode.children[0];
185
+ for (const grandchild of singleChild.children) {
186
+ adjustDepth(grandchild, -1);
187
+ }
188
+ allNode.children = singleChild.children;
189
+ }
190
+ }
191
+ function adjustDepth(node, delta) {
192
+ node.depth += delta;
193
+ for (const child of node.children) {
194
+ adjustDepth(child, delta);
195
+ }
196
+ }
132
197
  function flattenTree(root) {
133
198
  const result = [];
134
199
  function walk(node) {
@@ -150,23 +215,26 @@ function getNodeSelection(node, selected) {
150
215
  else if (selectedCount > 0) state = "partial";
151
216
  return { state, selectedCount };
152
217
  }
153
- function createRenderer(title, subtitle, flatNodes, selected, getCursor, formatNode) {
218
+ function createRenderer(title, subtitle, flatNodes, selected, getCursor, getViewOffset, viewHeight, formatNode) {
154
219
  function renderContent() {
155
220
  const lines = [];
156
221
  lines.push(chalk2.bold.cyan(title));
157
222
  lines.push(chalk2.dim(subtitle));
158
223
  lines.push("");
159
224
  const cursor = getCursor();
160
- for (let i = 0; i < flatNodes.length; i++) {
225
+ const offset = getViewOffset();
226
+ const end = Math.min(flatNodes.length, offset + viewHeight);
227
+ for (let i = offset; i < end; i++) {
161
228
  const node = flatNodes[i];
162
229
  const selection = getNodeSelection(node, selected);
163
230
  lines.push(formatNode(node, selection, i === cursor));
164
231
  }
165
- lines.push("");
232
+ lines.push(chalk2.dim(`
233
+ Showing ${end - offset}/${flatNodes.length} (offset ${offset + 1})`));
166
234
  return lines;
167
235
  }
168
236
  function getLineCount() {
169
- return 4 + flatNodes.length;
237
+ return 4 + Math.min(viewHeight, flatNodes.length);
170
238
  }
171
239
  return { renderContent, getLineCount };
172
240
  }
@@ -186,6 +254,9 @@ async function interactiveTreeSelect(items, options) {
186
254
  const root = buildTree(items);
187
255
  const flatNodes = flattenTree(root);
188
256
  if (flatNodes.length === 0) return null;
257
+ const stdout = process.stdout;
258
+ const rows = typeof stdout.rows === "number" ? stdout.rows : 24;
259
+ const viewHeight = Math.max(5, rows - 5);
189
260
  const selected = /* @__PURE__ */ new Set();
190
261
  if (defaultSelected) {
191
262
  for (const idx of defaultSelected) selected.add(idx);
@@ -193,8 +264,8 @@ async function interactiveTreeSelect(items, options) {
193
264
  for (let i = 0; i < items.length; i++) selected.add(i);
194
265
  }
195
266
  let cursor = 0;
267
+ let viewOffset = 0;
196
268
  const stdin = process.stdin;
197
- const stdout = process.stdout;
198
269
  if (!stdin.isTTY || !stdout.isTTY) {
199
270
  return defaultAll ? Array.from(selected) : null;
200
271
  }
@@ -206,7 +277,16 @@ async function interactiveTreeSelect(items, options) {
206
277
  if (useAltScreen) {
207
278
  stdout.write("\x1B[?1049h");
208
279
  }
209
- const renderer = createRenderer(title, subtitle, flatNodes, selected, () => cursor, formatNode);
280
+ const renderer = createRenderer(
281
+ title,
282
+ subtitle,
283
+ flatNodes,
284
+ selected,
285
+ () => cursor,
286
+ () => viewOffset,
287
+ viewHeight,
288
+ formatNode
289
+ );
210
290
  writeToTerminal(stdout, renderer.renderContent());
211
291
  return new Promise((resolve) => {
212
292
  function cleanup(clear = false) {
@@ -254,11 +334,13 @@ async function interactiveTreeSelect(items, options) {
254
334
  }
255
335
  if (key.name === "up") {
256
336
  cursor = (cursor - 1 + flatNodes.length) % flatNodes.length;
337
+ if (cursor < viewOffset) viewOffset = cursor;
257
338
  rerender();
258
339
  return;
259
340
  }
260
341
  if (key.name === "down") {
261
342
  cursor = (cursor + 1) % flatNodes.length;
343
+ if (cursor >= viewOffset + viewHeight) viewOffset = cursor - viewHeight + 1;
262
344
  rerender();
263
345
  return;
264
346
  }
@@ -289,7 +371,7 @@ function formatTreeNode(node, selection, isCursor, options = {}) {
289
371
  const { state, selectedCount } = selection;
290
372
  const totalCount = node.leafIndices.length;
291
373
  const indent = " ".repeat(node.depth - 1);
292
- const checkbox2 = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
374
+ const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
293
375
  const name = isCursor ? chalk2.cyan.underline(node.name) : node.name;
294
376
  const cursorMark = isCursor ? chalk2.cyan("\u203A ") : " ";
295
377
  let count = "";
@@ -301,13 +383,21 @@ function formatTreeNode(node, selection, isCursor, options = {}) {
301
383
  if (isCursor && totalCount > 0) {
302
384
  hint = state === "all" ? chalk2.dim(" \u2190 Space to deselect") : chalk2.dim(" \u2190 Space to select");
303
385
  }
304
- return `${cursorMark}${indent}${checkbox2} ${name}${count}${suffix}${hint}`;
386
+ return `${cursorMark}${indent}${checkbox} ${name}${count}${suffix}${hint}`;
305
387
  }
306
388
  function truncateDescription(value, maxLen) {
307
389
  const trimmed = value.trim();
308
390
  if (trimmed.length <= maxLen) return trimmed;
309
391
  return `${trimmed.slice(0, maxLen - 3).trimEnd()}...`;
310
392
  }
393
+ function getSkillDescriptionSuffix(skills, node, isCursor) {
394
+ if (!isCursor || !node.isLeaf || node.leafIndices.length !== 1) return "";
395
+ const skill = skills[node.leafIndices[0]];
396
+ if (!skill?.description) return "";
397
+ const description = truncateDescription(skill.description, 72);
398
+ if (!description) return "";
399
+ return chalk2.dim(` - ${description}`);
400
+ }
311
401
  function getPlatformDisplay(platform) {
312
402
  return PLATFORM_DISPLAY[platform] || platform;
313
403
  }
@@ -322,25 +412,33 @@ async function promptSkillsInteractive(skills, options = {}) {
322
412
  const isFullyInstalled = hasInstalledCheck && installedOnTargets.length === targetPlatforms.length;
323
413
  if (options.defaultAll !== false && !isFullyInstalled) defaultSelected.add(i);
324
414
  }
325
- const choices = skills.map((skill, i) => {
326
- const base = skill.relPath === "." ? skill.suggestedSource : skill.relPath;
327
- const desc = skill.description ? ` - ${truncateDescription(skill.description, 72)}` : "";
328
- let installedSuffix = "";
329
- if (skill.installedPlatforms?.length) {
330
- installedSuffix = targetPlatforms.length > 0 && skill.installedPlatforms.length === targetPlatforms.length ? " [installed]" : ` [installed on ${skill.installedPlatforms.length}]`;
331
- }
332
- return {
333
- name: `${base}${installedSuffix ? chalk2.dim(installedSuffix) : ""}${desc ? chalk2.dim(desc) : ""}`,
334
- value: i,
335
- checked: defaultSelected.has(i)
336
- };
337
- });
338
- const selectedIndices = await checkbox({
339
- message: "Select skills to install",
340
- choices,
341
- loop: false,
342
- pageSize: Math.min(20, choices.length)
415
+ const selectedIndices = await interactiveTreeSelect(skills, {
416
+ title: "Select skills to install",
417
+ subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
418
+ buildTree: buildSkillTree,
419
+ formatNode: (node, selection, isCursor) => {
420
+ let installedSuffix = "";
421
+ if (node.isLeaf && node.leafIndices.length === 1) {
422
+ const skill = skills[node.leafIndices[0]];
423
+ if (skill?.installedPlatforms?.length) {
424
+ if (skill.installedPlatforms.length === targetPlatforms.length && targetPlatforms.length > 0) {
425
+ installedSuffix = chalk2.dim(" [installed]");
426
+ } else if (skill.installedPlatforms.length > 0) {
427
+ installedSuffix = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
428
+ }
429
+ }
430
+ }
431
+ const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
432
+ const formatted = formatTreeNode(node, selection, isCursor, { suffix: `${installedSuffix}${descriptionSuffix}` });
433
+ if (isCursor && installedSuffix && selection.state !== "all") {
434
+ return formatted.replace("\u2190 Space to select", "\u2190 Space to reinstall");
435
+ }
436
+ return formatted;
437
+ },
438
+ defaultAll: false,
439
+ defaultSelected
343
440
  });
441
+ if (!selectedIndices) return null;
344
442
  const selectedSkills = selectedIndices.map((i) => skills[i]);
345
443
  const names = selectedSkills.map((s) => s.relPath === "." ? s.suggestedSource : s.relPath);
346
444
  console.log(chalk2.green(`
@@ -378,26 +476,17 @@ async function promptSyncTargetsInteractive(choices) {
378
476
  return selected;
379
477
  }
380
478
  async function promptSkillsTreeInteractive(skills, tree, options = {}) {
381
- if (skills.length === 0) return null;
382
- const defaultSelected = /* @__PURE__ */ new Set();
383
- if (options.defaultAll !== false) {
384
- for (let i = 0; i < skills.length; i++) defaultSelected.add(i);
385
- }
386
- const choices = skills.map((skill, i) => {
387
- const label = skill.displayName || skill.relPath || skill.suggestedSource;
388
- const desc = skill.description ? ` - ${truncateDescription(skill.description, 72)}` : "";
389
- return {
390
- name: `${label}${desc ? chalk2.dim(desc) : ""}`,
391
- value: i,
392
- checked: defaultSelected.has(i)
393
- };
394
- });
395
- const selectedIndices = await checkbox({
396
- message: "Select skills",
397
- choices,
398
- loop: false,
399
- pageSize: Math.min(20, choices.length)
479
+ const selectedIndices = await interactiveTreeSelect(skills, {
480
+ title: "Select skills from markdown",
481
+ subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
482
+ buildTree: () => buildTreeFromSkillNodes(tree, skills.length),
483
+ formatNode: (node, selection, isCursor) => {
484
+ const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
485
+ return formatTreeNode(node, selection, isCursor, { suffix: descriptionSuffix });
486
+ },
487
+ defaultAll: options.defaultAll !== false
400
488
  });
489
+ if (!selectedIndices) return null;
401
490
  const selectedSkills = selectedIndices.map((i) => skills[i]);
402
491
  const names = selectedSkills.map((s) => s.displayName || s.relPath || s.suggestedSource);
403
492
  console.log(chalk2.green(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.10.8",
3
+ "version": "0.10.10",
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",