skillbox 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { registerRemove } from "./commands/remove.js";
12
12
  import { registerStatus } from "./commands/status.js";
13
13
  import { registerUpdate } from "./commands/update.js";
14
14
  const program = new Command();
15
- program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.3.3");
15
+ program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.3.4");
16
16
  registerAdd(program);
17
17
  registerAgent(program);
18
18
  registerConfig(program);
@@ -4,7 +4,7 @@ import { loadConfig } from "../lib/config.js";
4
4
  import { parseRepoRef } from "../lib/github.js";
5
5
  import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
6
6
  import { recordInstallPaths } from "../lib/installs.js";
7
- import { printInfo, printJson } from "../lib/output.js";
7
+ import { printFailure, printInfo, printJson, printSkipped, printSuccess, startSpinner, stopSpinner, } from "../lib/output.js";
8
8
  import { buildProjectAgentPaths } from "../lib/project-paths.js";
9
9
  import { fetchRepoFile, listRepoSkills, normalizeRepoRef, writeRepoSkillDirectory, } from "../lib/repo-skills.js";
10
10
  import { ensureProjectRegistered, resolveRuntime } from "../lib/runtime.js";
@@ -84,16 +84,27 @@ export async function handleRepoInstall(input, options) {
84
84
  }
85
85
  const summary = { installed: [], updated: [], skipped: [], failed: [] };
86
86
  const index = await loadIndex();
87
- for (const skill of skills) {
88
- if (!selected.includes(skill.name)) {
89
- continue;
90
- }
87
+ const showProgress = !options.json;
88
+ const selectedSkills = skills.filter((s) => selected.includes(s.name));
89
+ const total = selectedSkills.length;
90
+ if (showProgress && total > 0) {
91
+ printInfo(`Adding ${total} skill${total === 1 ? "" : "s"} from ${ref.owner}/${ref.repo}...\n`);
92
+ }
93
+ for (let i = 0; i < selectedSkills.length; i++) {
94
+ const skill = selectedSkills[i];
95
+ const progress = `(${i + 1}/${total})`;
91
96
  const alreadyInstalled = index.skills.some((entry) => entry.name === skill.name);
97
+ if (showProgress) {
98
+ startSpinner(`${skill.name} ${progress}`);
99
+ }
92
100
  try {
93
101
  const skillMarkdown = await fetchRepoFile(ref, ref.path ? `${ref.path}/${skill.skillFile}` : skill.skillFile);
94
102
  const parsed = parseSkillMarkdown(skillMarkdown);
95
103
  if (!parsed.description) {
96
104
  summary.skipped.push(skill.name);
105
+ if (showProgress) {
106
+ printSkipped(skill.name, "missing description");
107
+ }
97
108
  continue;
98
109
  }
99
110
  await writeRepoSkillDirectory(ref, skill.path, skill.name);
@@ -124,56 +135,49 @@ export async function handleRepoInstall(input, options) {
124
135
  index.skills = nextIndex.skills;
125
136
  if (alreadyInstalled) {
126
137
  summary.updated.push(skill.name);
138
+ if (showProgress) {
139
+ printSuccess(skill.name, "updated");
140
+ }
127
141
  }
128
142
  else {
129
143
  summary.installed.push(skill.name);
144
+ if (showProgress) {
145
+ printSuccess(skill.name);
146
+ }
130
147
  }
131
148
  }
132
149
  catch (error) {
133
150
  const message = getErrorMessage(error, "unknown");
134
151
  summary.failed.push({ name: skill.name, reason: message });
152
+ if (showProgress) {
153
+ printFailure(skill.name, message);
154
+ }
135
155
  }
136
156
  }
157
+ if (showProgress) {
158
+ stopSpinner();
159
+ }
137
160
  await saveIndex(sortIndex(index));
138
161
  if (options.json) {
139
162
  printJson({ ok: true, command: "add", data: { repo: `${ref.owner}/${ref.repo}`, ...summary } });
140
163
  return;
141
164
  }
142
- printInfo(`Skills Added from: ${ref.owner}/${ref.repo}`);
143
- printInfo("");
144
- printInfo("Source: git");
145
- printInfo(` ${ref.owner}/${ref.repo}${ref.path ? `/${ref.path}` : ""} (${ref.ref})`);
146
- if (summary.installed.length > 0) {
147
- printInfo("");
148
- printInfo(`Installed (${summary.installed.length}):`);
149
- for (const name of summary.installed) {
150
- printInfo(` ✓ ${name}`);
151
- }
165
+ // Summary line
166
+ const added = summary.installed.length + summary.updated.length;
167
+ const failed = summary.failed.length;
168
+ const skipped = summary.skipped.length;
169
+ if (added > 0 && failed === 0 && skipped === 0) {
170
+ printInfo(`\nAdded ${added} skill${added === 1 ? "" : "s"} from ${ref.owner}/${ref.repo}.`);
152
171
  }
153
- if (summary.updated.length > 0) {
154
- printInfo("");
155
- printInfo(`Updated (${summary.updated.length}):`);
156
- for (const name of summary.updated) {
157
- printInfo(` ✓ ${name}`);
158
- }
159
- }
160
- if (summary.skipped.length > 0) {
161
- printInfo("");
162
- printInfo(`Skipped (${summary.skipped.length}):`);
163
- for (const name of summary.skipped) {
164
- printInfo(` - ${name} (missing description)`);
165
- }
172
+ else if (added > 0) {
173
+ const parts = [];
174
+ if (failed > 0)
175
+ parts.push(`${failed} failed`);
176
+ if (skipped > 0)
177
+ parts.push(`${skipped} skipped`);
178
+ printInfo(`\nAdded ${added} skill${added === 1 ? "" : "s"} (${parts.join(", ")}).`);
166
179
  }
167
- if (summary.failed.length > 0) {
168
- printInfo("");
169
- printInfo(`Failed (${summary.failed.length}):`);
170
- for (const failure of summary.failed) {
171
- printInfo(` ✗ ${failure.name} (${failure.reason})`);
172
- }
173
- }
174
- const total = summary.installed.length + summary.updated.length;
175
- if (total === 0) {
176
- printInfo("");
177
- printInfo("No skills were added.");
180
+ else {
181
+ printInfo("\nNo skills were added.");
178
182
  }
179
183
  }
@@ -5,7 +5,7 @@ import { fetchText } from "../lib/fetcher.js";
5
5
  import { collect } from "../lib/fs-utils.js";
6
6
  import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
7
7
  import { recordInstallPaths } from "../lib/installs.js";
8
- import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
8
+ import { isJsonEnabled, printInfo, printJson, printSuccess, startSpinner, stopSpinner, } from "../lib/output.js";
9
9
  import { buildProjectAgentPaths } from "../lib/project-paths.js";
10
10
  import { ensureProjectRegistered, resolveRuntime } from "../lib/runtime.js";
11
11
  import { buildMetadata, inferNameFromUrl, parseSkillMarkdown } from "../lib/skill-parser.js";
@@ -34,17 +34,28 @@ export function registerAdd(program) {
34
34
  });
35
35
  return;
36
36
  }
37
+ const showProgress = !isJsonEnabled(options);
38
+ const inferred = inferNameFromUrl(url);
39
+ const displayName = options.name ?? inferred ?? "skill";
40
+ if (showProgress) {
41
+ startSpinner(`Adding ${displayName}`);
42
+ }
37
43
  const skillMarkdown = await fetchText(url);
38
44
  const parsed = parseSkillMarkdown(skillMarkdown);
39
- const inferred = inferNameFromUrl(url);
40
45
  const skillName = options.name ?? inferred ?? parsed.name;
41
46
  if (!skillName) {
47
+ if (showProgress)
48
+ stopSpinner();
42
49
  throw new Error("Unable to infer skill name. Use --name to specify it.");
43
50
  }
44
51
  if (!parsed.name && !options.name) {
52
+ if (showProgress)
53
+ stopSpinner();
45
54
  throw new Error("Skill frontmatter missing name. Provide --name to continue.");
46
55
  }
47
56
  if (!parsed.description) {
57
+ if (showProgress)
58
+ stopSpinner();
48
59
  throw new Error("Skill frontmatter missing description. Convert the source into a valid skill.");
49
60
  }
50
61
  const metadata = buildMetadata(parsed, { type: "url", url }, skillName);
@@ -102,6 +113,7 @@ export function registerAdd(program) {
102
113
  });
103
114
  await saveIndex(sortIndex(nextIndex));
104
115
  if (isJsonEnabled(options)) {
116
+ stopSpinner();
105
117
  printJson({
106
118
  ok: true,
107
119
  command: "add",
@@ -114,22 +126,8 @@ export function registerAdd(program) {
114
126
  });
115
127
  return;
116
128
  }
117
- printInfo(`Skill Added: ${skillName}`);
118
- printInfo("");
119
- printInfo("Source: url");
120
- printInfo(` ${url}`);
121
- if (installs.length > 0) {
122
- printInfo("");
123
- printInfo("Installed to:");
124
- for (const install of installs) {
125
- const scopeLabel = install.scope === "project" ? `project:${install.projectRoot}` : "user";
126
- printInfo(` ✓ ${scopeLabel}/${install.agent}`);
127
- }
128
- }
129
- else {
130
- printInfo("");
131
- printInfo("No agent targets were updated.");
132
- }
129
+ printSuccess(skillName);
130
+ printInfo(`\nAdded skill from ${url}.`);
133
131
  }
134
132
  catch (error) {
135
133
  handleCommandError(options, "add", error);
@@ -172,6 +172,14 @@ function filterByAgents(skills, agents) {
172
172
  const agentSet = new Set(agents);
173
173
  return skills.filter((skill) => skill.installs?.some((install) => install.agent && agentSet.has(install.agent)));
174
174
  }
175
+ function filterUserScope(skills) {
176
+ return skills
177
+ .filter((skill) => skill.installs?.some((install) => install.scope === "user") ?? !skill.installs?.length)
178
+ .map((skill) => ({
179
+ ...skill,
180
+ installs: skill.installs?.filter((install) => install.scope === "user"),
181
+ }));
182
+ }
175
183
  export function registerList(program) {
176
184
  program
177
185
  .command("list")
@@ -186,8 +194,9 @@ export function registerList(program) {
186
194
  const indexedSkills = options.agents
187
195
  ? filterByAgents(index.skills, runtime.agentList)
188
196
  : index.skills;
189
- const allSkills = [...indexedSkills, ...globalSkills];
190
- const enrichedSkills = await enrichWithSubcommands(allSkills);
197
+ const scopedSkills = options.global ? filterUserScope(indexedSkills) : indexedSkills;
198
+ const mergedSkills = [...scopedSkills, ...globalSkills];
199
+ const enrichedSkills = await enrichWithSubcommands(mergedSkills);
191
200
  if (isJsonEnabled(options)) {
192
201
  printJson({
193
202
  ok: true,
@@ -4,7 +4,7 @@ import { loadConfig } from "../lib/config.js";
4
4
  import { fetchText } from "../lib/fetcher.js";
5
5
  import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
6
6
  import { getInstallPaths } from "../lib/installs.js";
7
- import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
7
+ import { isJsonEnabled, printFailure, printInfo, printJson, printSkipped, printSuccess, startSpinner, stopSpinner, } from "../lib/output.js";
8
8
  import { fetchRepoFile, normalizeRepoRef, writeRepoSkillDirectory } from "../lib/repo-skills.js";
9
9
  import { buildMetadata, parseSkillMarkdown } from "../lib/skill-parser.js";
10
10
  import { ensureSkillsDir, writeSkillFiles, writeSkillMetadata } from "../lib/skill-store.js";
@@ -21,31 +21,6 @@ function groupBySource(results) {
21
21
  failedCount: items.filter((r) => r.status === "failed").length,
22
22
  }));
23
23
  }
24
- function formatSourceHeader(group) {
25
- const count = group.results.length;
26
- const skillWord = count === 1 ? "skill" : "skills";
27
- if (group.source === "local") {
28
- return `${group.source} (${count} ${skillWord} - skipped)`;
29
- }
30
- if (group.failedCount > 0) {
31
- return `${group.source} (${count} ${skillWord}, ${group.failedCount} failed)`;
32
- }
33
- return `${group.source} (${count} ${skillWord})`;
34
- }
35
- function printSourceGroup(group) {
36
- printInfo(formatSourceHeader(group));
37
- for (const result of group.results) {
38
- if (result.status === "skipped") {
39
- printInfo(` - ${result.name}`);
40
- }
41
- else if (result.status === "failed") {
42
- printInfo(` ✗ ${result.name} (${result.error ?? "failed"})`);
43
- }
44
- else {
45
- printInfo(` ✓ ${result.name}`);
46
- }
47
- }
48
- }
49
24
  async function updateUrlSkill(skill, index, projectRoot, config) {
50
25
  if (!skill.source.url) {
51
26
  return;
@@ -129,39 +104,72 @@ export function registerUpdate(program) {
129
104
  const config = await loadConfig();
130
105
  const projectRoot = options.project ? path.resolve(options.project) : null;
131
106
  const results = [];
132
- for (const skill of targets) {
107
+ const showProgress = !isJsonEnabled(options);
108
+ if (showProgress && targets.length > 0) {
109
+ printInfo(`Updating ${targets.length} skill${targets.length === 1 ? "" : "s"}...\n`);
110
+ }
111
+ const total = targets.length;
112
+ for (let i = 0; i < targets.length; i++) {
113
+ const skill = targets[i];
114
+ const progress = `(${i + 1}/${total})`;
133
115
  if (skill.source.type === "url") {
116
+ if (showProgress) {
117
+ startSpinner(`${skill.name} ${progress}`);
118
+ }
134
119
  try {
135
120
  await updateUrlSkill(skill, index, projectRoot, config);
136
121
  results.push({ name: skill.name, source: "url", status: "updated" });
122
+ if (showProgress) {
123
+ printSuccess(skill.name);
124
+ }
137
125
  }
138
126
  catch (err) {
127
+ const errorMsg = err instanceof Error ? err.message : "unknown error";
139
128
  results.push({
140
129
  name: skill.name,
141
130
  source: "url",
142
131
  status: "failed",
143
- error: err instanceof Error ? err.message : "unknown error",
132
+ error: errorMsg,
144
133
  });
134
+ if (showProgress) {
135
+ printFailure(skill.name, errorMsg);
136
+ }
145
137
  }
146
138
  }
147
139
  else if (skill.source.type === "git") {
140
+ if (showProgress) {
141
+ startSpinner(`${skill.name} ${progress}`);
142
+ }
148
143
  try {
149
144
  await updateGitSkill(skill, index, projectRoot, config);
150
145
  results.push({ name: skill.name, source: "git", status: "updated" });
146
+ if (showProgress) {
147
+ printSuccess(skill.name);
148
+ }
151
149
  }
152
150
  catch (err) {
151
+ const errorMsg = err instanceof Error ? err.message : "unknown error";
153
152
  results.push({
154
153
  name: skill.name,
155
154
  source: "git",
156
155
  status: "failed",
157
- error: err instanceof Error ? err.message : "unknown error",
156
+ error: errorMsg,
158
157
  });
158
+ if (showProgress) {
159
+ printFailure(skill.name, errorMsg);
160
+ }
159
161
  }
160
162
  }
161
163
  else {
162
164
  results.push({ name: skill.name, source: skill.source.type, status: "skipped" });
165
+ if (showProgress) {
166
+ printSkipped(skill.name, "skipped");
167
+ }
163
168
  }
164
169
  }
170
+ if (showProgress) {
171
+ stopSpinner();
172
+ }
165
173
  await saveIndex(sortIndex(index));
166
174
  const sourceGroups = groupBySource(results);
167
175
  const totalUpdated = results.filter((r) => r.status === "updated").length;
@@ -189,21 +197,15 @@ export function registerUpdate(program) {
189
197
  printInfo("No skills to update.");
190
198
  return;
191
199
  }
192
- printInfo("Skill Update");
193
- for (const group of sourceGroups) {
194
- printInfo("");
195
- printSourceGroup(group);
196
- }
197
200
  // Summary line
198
- printInfo("");
199
201
  if (totalFailed > 0) {
200
- printInfo(`Updated ${totalUpdated} of ${totalTrackable} trackable skills (${totalFailed} failed).`);
202
+ printInfo(`\nUpdated ${totalUpdated} of ${totalTrackable} trackable skill${totalTrackable === 1 ? "" : "s"} (${totalFailed} failed).`);
201
203
  }
202
204
  else if (totalUpdated > 0) {
203
- printInfo(`Updated ${totalUpdated} of ${totalTrackable} trackable skills.`);
205
+ printInfo(`\nUpdated ${totalUpdated} of ${totalTrackable} trackable skill${totalTrackable === 1 ? "" : "s"}.`);
204
206
  }
205
207
  else if (totalSkipped > 0 && totalTrackable === 0) {
206
- printInfo("No trackable skills to update.");
208
+ printInfo("\nNo trackable skills to update.");
207
209
  }
208
210
  }
209
211
  catch (error) {
@@ -2,6 +2,48 @@ import chalk from "chalk";
2
2
  export function isJsonEnabled(options) {
3
3
  return Boolean(options.json);
4
4
  }
5
+ // Progress indicator support
6
+ const isTTY = process.stdout.isTTY ?? false;
7
+ // Braille spinner frames (single character, smooth animation)
8
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
+ let spinnerInterval = null;
10
+ let spinnerFrame = 0;
11
+ export function startSpinner(message) {
12
+ if (!isTTY)
13
+ return;
14
+ spinnerFrame = 0;
15
+ const render = () => {
16
+ const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
17
+ process.stdout.write(`\r\x1b[K ${frame} ${message}`);
18
+ spinnerFrame++;
19
+ };
20
+ render();
21
+ spinnerInterval = setInterval(render, 80);
22
+ }
23
+ export function stopSpinner() {
24
+ if (spinnerInterval) {
25
+ clearInterval(spinnerInterval);
26
+ spinnerInterval = null;
27
+ }
28
+ if (isTTY) {
29
+ process.stdout.write(`\r\x1b[K`);
30
+ }
31
+ }
32
+ export function printProgressResult(message) {
33
+ stopSpinner();
34
+ process.stdout.write(`${message}\n`);
35
+ }
36
+ // Common result formatters for progress output
37
+ export function printSuccess(name, suffix) {
38
+ const msg = suffix ? ` ✓ ${name} (${suffix})` : ` ✓ ${name}`;
39
+ printProgressResult(msg);
40
+ }
41
+ export function printFailure(name, error) {
42
+ printProgressResult(` ✗ ${name} (${error})`);
43
+ }
44
+ export function printSkipped(name, reason) {
45
+ printProgressResult(` - ${name} (${reason})`);
46
+ }
5
47
  export function printJson(result) {
6
48
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
7
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillbox",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Local-first, agent-agnostic skills manager",
5
5
  "license": "MIT",
6
6
  "author": "Christian Anagnostou",
@@ -33,6 +33,9 @@
33
33
  "lint:ci": "oxlint --deny-warnings",
34
34
  "format": "oxfmt",
35
35
  "format:check": "oxfmt --check",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "test:ci": "vitest run --reporter=verbose --reporter=junit --outputFile=test-results.xml",
36
39
  "prepublishOnly": "npm run lint:ci && npm run format:check && npm run build"
37
40
  },
38
41
  "dependencies": {
@@ -43,9 +46,11 @@
43
46
  },
44
47
  "devDependencies": {
45
48
  "@types/node": "^20.11.19",
49
+ "execa": "^9.6.1",
46
50
  "oxfmt": "^0.16.0",
47
51
  "oxlint": "^0.16.0",
48
52
  "ts-node": "^10.9.2",
49
- "typescript": "^5.4.2"
53
+ "typescript": "^5.4.2",
54
+ "vitest": "^4.0.18"
50
55
  }
51
56
  }