skillbox 0.2.1 → 0.3.0

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 (40) hide show
  1. package/README.md +73 -105
  2. package/dist/cli.js +15 -13
  3. package/dist/commands/add-repo.js +176 -0
  4. package/dist/commands/add.js +59 -29
  5. package/dist/commands/agent.js +5 -11
  6. package/dist/commands/config.js +11 -9
  7. package/dist/commands/convert.js +21 -6
  8. package/dist/commands/import.js +67 -77
  9. package/dist/commands/list.js +129 -92
  10. package/dist/commands/meta.js +5 -7
  11. package/dist/commands/project.js +24 -39
  12. package/dist/commands/remove.js +101 -0
  13. package/dist/commands/status.js +110 -101
  14. package/dist/commands/update.js +172 -43
  15. package/dist/lib/agent-detect.js +4 -13
  16. package/dist/lib/agents.js +64 -45
  17. package/dist/lib/command.js +6 -3
  18. package/dist/lib/config.js +19 -12
  19. package/dist/lib/discovery.js +3 -11
  20. package/dist/lib/fetcher.js +3 -3
  21. package/dist/lib/fs-utils.js +17 -0
  22. package/dist/lib/github.js +43 -0
  23. package/dist/lib/global-skills.js +23 -34
  24. package/dist/lib/grouping.js +6 -6
  25. package/dist/lib/index.js +13 -11
  26. package/dist/lib/installs.js +20 -13
  27. package/dist/lib/onboarding.js +31 -32
  28. package/dist/lib/options.js +4 -4
  29. package/dist/lib/output.js +13 -11
  30. package/dist/lib/paths.js +12 -4
  31. package/dist/lib/project-paths.js +2 -2
  32. package/dist/lib/project-root.js +3 -12
  33. package/dist/lib/projects.js +13 -11
  34. package/dist/lib/repo-skills.js +124 -0
  35. package/dist/lib/runtime.js +12 -12
  36. package/dist/lib/skill-parser.js +12 -12
  37. package/dist/lib/skill-store.js +13 -13
  38. package/dist/lib/source-grouping.js +49 -0
  39. package/dist/lib/sync.js +39 -16
  40. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { handleCommandError } from "../lib/command.js";
4
+ import { loadIndex, saveIndex, sortIndex } from "../lib/index.js";
5
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
6
+ import { skillDir } from "../lib/skill-store.js";
7
+ async function removePaths(paths) {
8
+ for (const target of paths) {
9
+ await fs.rm(target, { recursive: true, force: true });
10
+ }
11
+ }
12
+ function groupInstallsByScope(installs) {
13
+ const groups = new Map();
14
+ for (const install of installs) {
15
+ const key = install.scope === "project" ? `project:${install.projectRoot}` : "user";
16
+ const existing = groups.get(key) ?? [];
17
+ existing.push(install);
18
+ groups.set(key, existing);
19
+ }
20
+ return groups;
21
+ }
22
+ function printRemovedInstalls(installs) {
23
+ const groups = groupInstallsByScope(installs);
24
+ // Sort: user scope first, then projects
25
+ const sortedKeys = Array.from(groups.keys()).sort((a, b) => {
26
+ if (a === "user")
27
+ return -1;
28
+ if (b === "user")
29
+ return 1;
30
+ return a.localeCompare(b);
31
+ });
32
+ for (const key of sortedKeys) {
33
+ const groupInstalls = groups.get(key) ?? [];
34
+ const label = key === "user" ? "user" : key.replace("project:", "project: ");
35
+ for (const install of groupInstalls) {
36
+ printInfo(` ✓ ${label}/${install.agent}`);
37
+ }
38
+ }
39
+ }
40
+ export function registerRemove(program) {
41
+ program
42
+ .command("remove")
43
+ .argument("<name>", "Skill name")
44
+ .option("--project <path>", "Only remove installs for a project")
45
+ .option("--json", "JSON output")
46
+ .action(async (name, options) => {
47
+ try {
48
+ const index = await loadIndex();
49
+ const skill = index.skills.find((entry) => entry.name === name);
50
+ if (!skill) {
51
+ throw new Error(`Skill not found: ${name}`);
52
+ }
53
+ const projectRoot = options.project ? path.resolve(options.project) : null;
54
+ const installs = skill.installs ?? [];
55
+ const isProjectInstall = (install) => install.scope === "project" &&
56
+ Boolean(install.projectRoot) &&
57
+ install.projectRoot === projectRoot;
58
+ const toRemove = projectRoot ? installs.filter(isProjectInstall) : installs;
59
+ if (projectRoot && toRemove.length === 0) {
60
+ throw new Error(`No installs found for ${name} in ${projectRoot}.`);
61
+ }
62
+ const removedPaths = toRemove.map((install) => install.path);
63
+ await removePaths(removedPaths);
64
+ let removedCanonical = false;
65
+ if (projectRoot) {
66
+ const remaining = installs.filter((install) => !isProjectInstall(install));
67
+ index.skills = index.skills.map((entry) => entry.name === name
68
+ ? { ...entry, installs: remaining.length > 0 ? remaining : undefined }
69
+ : entry);
70
+ }
71
+ else {
72
+ index.skills = index.skills.filter((entry) => entry.name !== name);
73
+ await fs.rm(skillDir(name), { recursive: true, force: true });
74
+ removedCanonical = true;
75
+ }
76
+ await saveIndex(sortIndex(index));
77
+ if (isJsonEnabled(options)) {
78
+ printJson({
79
+ ok: true,
80
+ command: "remove",
81
+ data: {
82
+ name,
83
+ project: projectRoot,
84
+ removed: toRemove,
85
+ removedCanonical,
86
+ },
87
+ });
88
+ return;
89
+ }
90
+ printInfo(`Skill Removal: ${name}`);
91
+ if (toRemove.length > 0) {
92
+ printInfo("");
93
+ printInfo("Removed from:");
94
+ printRemovedInstalls(toRemove);
95
+ }
96
+ }
97
+ catch (error) {
98
+ handleCommandError(options, "remove", error);
99
+ }
100
+ });
101
+ }
@@ -1,129 +1,138 @@
1
- import { isJsonEnabled, printInfo, printJson, printList } from "../lib/output.js";
2
- import { loadIndex, saveIndex } from "../lib/index.js";
1
+ import { handleCommandError } from "../lib/command.js";
3
2
  import { fetchText } from "../lib/fetcher.js";
3
+ import { loadIndex, saveIndex } from "../lib/index.js";
4
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
4
5
  import { hashContent } from "../lib/skill-store.js";
5
- import { groupStatusByKey } from "../lib/grouping.js";
6
- import { loadConfig } from "../lib/config.js";
7
- import { handleCommandError } from "../lib/command.js";
8
- export const registerStatus = (program) => {
6
+ import { groupAndSort, sortByName } from "../lib/source-grouping.js";
7
+ async function checkSkillStatus(skill) {
8
+ const isTrackable = skill.source.type === "url" || skill.source.type === "git";
9
+ if (!isTrackable || skill.source.type !== "url" || !skill.source.url) {
10
+ return {
11
+ name: skill.name,
12
+ source: skill.source.type,
13
+ trackable: isTrackable,
14
+ outdated: false,
15
+ localChecksum: skill.checksum,
16
+ };
17
+ }
18
+ try {
19
+ const remoteText = await fetchText(skill.source.url);
20
+ const remoteChecksum = hashContent(remoteText);
21
+ const outdated = remoteChecksum !== skill.checksum;
22
+ return {
23
+ name: skill.name,
24
+ source: skill.source.type,
25
+ trackable: true,
26
+ outdated,
27
+ localChecksum: skill.checksum,
28
+ remoteChecksum,
29
+ };
30
+ }
31
+ catch (err) {
32
+ return {
33
+ name: skill.name,
34
+ source: skill.source.type,
35
+ trackable: true,
36
+ outdated: false,
37
+ localChecksum: skill.checksum,
38
+ error: err instanceof Error ? err.message : "Failed to check",
39
+ };
40
+ }
41
+ }
42
+ // Sort sources: url first, then git, then local (for status command - trackable first)
43
+ const STATUS_SOURCE_ORDER = ["url", "git", "local"];
44
+ function groupBySource(statuses) {
45
+ const grouped = groupAndSort(statuses, (s) => s.source, STATUS_SOURCE_ORDER, sortByName);
46
+ return grouped.map(({ key, items }) => {
47
+ const trackable = items.some((s) => s.trackable);
48
+ const outdatedCount = items.filter((s) => s.outdated).length;
49
+ const upToDateCount = items.filter((s) => s.trackable && !s.outdated && !s.error).length;
50
+ return {
51
+ source: key,
52
+ skills: items,
53
+ trackable,
54
+ outdatedCount,
55
+ upToDateCount,
56
+ };
57
+ });
58
+ }
59
+ function formatSourceHeader(group) {
60
+ const count = group.skills.length;
61
+ const skillWord = count === 1 ? "skill" : "skills";
62
+ if (!group.trackable) {
63
+ return `${group.source} (${count} ${skillWord} - not tracked)`;
64
+ }
65
+ if (group.outdatedCount > 0) {
66
+ return `${group.source} (${count} ${skillWord}, ${group.outdatedCount} outdated)`;
67
+ }
68
+ return `${group.source} (${count} ${skillWord})`;
69
+ }
70
+ function printSourceGroup(group) {
71
+ printInfo(formatSourceHeader(group));
72
+ for (const skill of group.skills) {
73
+ if (!group.trackable) {
74
+ printInfo(` ${skill.name}`);
75
+ }
76
+ else if (skill.error) {
77
+ printInfo(` ? ${skill.name} (${skill.error})`);
78
+ }
79
+ else if (skill.outdated) {
80
+ printInfo(` ✗ ${skill.name} (outdated)`);
81
+ }
82
+ else {
83
+ printInfo(` ✓ ${skill.name}`);
84
+ }
85
+ }
86
+ }
87
+ export function registerStatus(program) {
9
88
  program
10
89
  .command("status")
11
- .option("--group <group>", "Group by project or source")
12
90
  .option("--json", "JSON output")
13
91
  .action(async (options) => {
14
92
  try {
15
93
  const index = await loadIndex();
16
- const config = await loadConfig();
17
- const results = [];
94
+ const statuses = [];
18
95
  for (const skill of index.skills) {
19
- const projects = Array.from(new Set((skill.installs ?? [])
20
- .filter((install) => install.scope === "project" && install.projectRoot)
21
- .map((install) => install.projectRoot)));
22
- const allowSystem = config.manageSystem;
23
- const isSystem = (skill.installs ?? []).some((install) => install.scope === "system");
24
- if (isSystem && !allowSystem) {
25
- results.push({
26
- name: skill.name,
27
- source: skill.source.type,
28
- outdated: false,
29
- localChecksum: skill.checksum,
30
- projects,
31
- system: true,
32
- });
33
- continue;
34
- }
35
- if (skill.source.type !== "url" || !skill.source.url) {
36
- results.push({
37
- name: skill.name,
38
- source: skill.source.type,
39
- outdated: false,
40
- localChecksum: skill.checksum,
41
- projects,
42
- system: isSystem,
43
- });
44
- continue;
96
+ const status = await checkSkillStatus(skill);
97
+ statuses.push(status);
98
+ // Update lastChecked for trackable skills
99
+ if (status.trackable && !status.error) {
100
+ skill.lastChecked = new Date().toISOString();
45
101
  }
46
- const remoteText = await fetchText(skill.source.url);
47
- const remoteChecksum = hashContent(remoteText);
48
- const outdated = remoteChecksum !== skill.checksum;
49
- skill.lastChecked = new Date().toISOString();
50
- results.push({
51
- name: skill.name,
52
- source: skill.source.type,
53
- outdated,
54
- localChecksum: skill.checksum,
55
- remoteChecksum,
56
- projects,
57
- system: false,
58
- });
59
102
  }
60
103
  await saveIndex(index);
61
- const outdated = results.filter((entry) => entry.outdated).map((entry) => entry.name);
62
- const upToDate = results.filter((entry) => !entry.outdated).map((entry) => entry.name);
63
- const groupedProjects = groupByProject(results);
64
- const groupedSources = groupBySource(results);
104
+ const sourceGroups = groupBySource(statuses);
105
+ const totalOutdated = statuses.filter((s) => s.outdated).length;
106
+ const totalTrackable = statuses.filter((s) => s.trackable).length;
107
+ const totalUpToDate = statuses.filter((s) => s.trackable && !s.outdated && !s.error).length;
65
108
  if (isJsonEnabled(options)) {
66
109
  printJson({
67
110
  ok: true,
68
111
  command: "status",
69
112
  data: {
70
- group: options.group ?? null,
71
- outdated,
72
- upToDate,
73
- results,
74
- projects: options.group === "project" ? groupedProjects : undefined,
75
- sources: options.group === "source" ? groupedSources : undefined,
113
+ total: statuses.length,
114
+ outdated: totalOutdated,
115
+ upToDate: totalUpToDate,
116
+ trackable: totalTrackable,
117
+ skills: statuses,
118
+ bySource: sourceGroups,
76
119
  },
77
120
  });
78
121
  return;
79
122
  }
80
- if (options.group === "project") {
81
- printInfo(`Projects: ${groupedProjects.length}`);
82
- for (const project of groupedProjects) {
83
- printInfo(`- ${project.root}`);
84
- if (project.outdated.length > 0) {
85
- printList(" Outdated", project.outdated, " - ");
86
- }
87
- if (project.upToDate.length > 0) {
88
- printList(" Up to date", project.upToDate, " - ");
89
- }
90
- }
91
- return;
123
+ printInfo("Skill Status");
124
+ for (const group of sourceGroups) {
125
+ printInfo("");
126
+ printSourceGroup(group);
92
127
  }
93
- if (options.group === "source") {
94
- printInfo(`Sources: ${groupedSources.length}`);
95
- for (const source of groupedSources) {
96
- printInfo(`- ${source.source}`);
97
- if (source.outdated.length > 0) {
98
- printList(" Outdated", source.outdated, " - ");
99
- }
100
- if (source.upToDate.length > 0) {
101
- printList(" Up to date", source.upToDate, " - ");
102
- }
103
- }
104
- return;
128
+ // Summary line if there are outdated skills
129
+ if (totalOutdated > 0) {
130
+ printInfo("");
131
+ printInfo(`Run 'skillbox update' to update ${totalOutdated} outdated skill(s).`);
105
132
  }
106
- printList("Outdated", outdated);
107
- printList("Up to date", upToDate);
108
133
  }
109
134
  catch (error) {
110
135
  handleCommandError(options, "status", error);
111
136
  }
112
137
  });
113
- };
114
- const groupByProject = (results) => {
115
- const grouped = groupStatusByKey(results, (result) => result.name, (result) => result.outdated, (result) => result.projects);
116
- return grouped.map((group) => ({
117
- root: group.key,
118
- outdated: group.outdated,
119
- upToDate: group.upToDate,
120
- }));
121
- };
122
- const groupBySource = (results) => {
123
- const grouped = groupStatusByKey(results, (result) => result.name, (result) => result.outdated, (result) => [result.source]);
124
- return grouped.map((group) => ({
125
- source: group.key,
126
- outdated: group.outdated,
127
- upToDate: group.upToDate,
128
- }));
129
- };
138
+ }
@@ -1,84 +1,213 @@
1
- import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
2
- import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
3
- import { fetchText } from "../lib/fetcher.js";
4
- import { parseSkillMarkdown, buildMetadata } from "../lib/skill-parser.js";
5
- import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
6
- import { copySkillToInstallPaths } from "../lib/sync.js";
7
1
  import path from "node:path";
8
- import { loadConfig } from "../lib/config.js";
9
2
  import { handleCommandError } from "../lib/command.js";
10
- export const registerUpdate = (program) => {
3
+ import { loadConfig } from "../lib/config.js";
4
+ import { fetchText } from "../lib/fetcher.js";
5
+ import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
6
+ import { getInstallPaths } from "../lib/installs.js";
7
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
8
+ import { fetchRepoFile, normalizeRepoRef, writeRepoSkillDirectory } from "../lib/repo-skills.js";
9
+ import { buildMetadata, parseSkillMarkdown } from "../lib/skill-parser.js";
10
+ import { ensureSkillsDir, writeSkillFiles, writeSkillMetadata } from "../lib/skill-store.js";
11
+ import { groupAndSort, sortByName } from "../lib/source-grouping.js";
12
+ import { installSkillToTargets } from "../lib/sync.js";
13
+ // Sort sources: url first, then git (trackable sources first)
14
+ const UPDATE_SOURCE_ORDER = ["url", "git", "local"];
15
+ function groupBySource(results) {
16
+ const grouped = groupAndSort(results, (r) => r.source, UPDATE_SOURCE_ORDER, sortByName);
17
+ return grouped.map(({ key, items }) => ({
18
+ source: key,
19
+ results: items,
20
+ updatedCount: items.filter((r) => r.status === "updated").length,
21
+ failedCount: items.filter((r) => r.status === "failed").length,
22
+ }));
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
+ async function updateUrlSkill(skill, index, projectRoot, config) {
50
+ if (!skill.source.url) {
51
+ return;
52
+ }
53
+ const markdown = await fetchText(skill.source.url);
54
+ const parsed = parseSkillMarkdown(markdown);
55
+ if (!parsed.description) {
56
+ throw new Error(`Skill ${skill.name} is missing a description after update.`);
57
+ }
58
+ const metadata = buildMetadata(parsed, { type: "url", url: skill.source.url }, skill.name);
59
+ await writeSkillFiles(skill.name, markdown, metadata);
60
+ const installPaths = getInstallPaths(skill, projectRoot);
61
+ if (installPaths.length > 0) {
62
+ await installSkillToTargets(skill.name, installPaths, config);
63
+ }
64
+ const nextIndex = upsertSkill(index, {
65
+ name: skill.name,
66
+ source: { type: "url", url: skill.source.url },
67
+ checksum: parsed.checksum,
68
+ updatedAt: metadata.updatedAt,
69
+ lastSync: new Date().toISOString(),
70
+ });
71
+ index.skills = nextIndex.skills;
72
+ }
73
+ async function updateGitSkill(skill, index, projectRoot, config) {
74
+ if (!skill.source.repo) {
75
+ return;
76
+ }
77
+ const [owner, repo] = skill.source.repo.split("/");
78
+ if (!owner || !repo) {
79
+ return;
80
+ }
81
+ const skillPath = skill.source.path?.replace(/\/$/, "") ?? "";
82
+ const ref = await normalizeRepoRef({
83
+ owner,
84
+ repo,
85
+ ref: skill.source.ref ?? "main",
86
+ });
87
+ const skillFilePath = skillPath ? `${skillPath}/SKILL.md` : "SKILL.md";
88
+ const markdown = await fetchRepoFile(ref, skillFilePath);
89
+ const parsed = parseSkillMarkdown(markdown);
90
+ if (!parsed.description) {
91
+ throw new Error(`Skill ${skill.name} is missing a description after update.`);
92
+ }
93
+ await writeRepoSkillDirectory(ref, skillPath, skill.name);
94
+ const source = {
95
+ type: "git",
96
+ repo: skill.source.repo,
97
+ path: skillPath || undefined,
98
+ ref: ref.ref,
99
+ };
100
+ const metadata = buildMetadata(parsed, source, skill.name);
101
+ await writeSkillMetadata(skill.name, metadata);
102
+ const installPaths = getInstallPaths(skill, projectRoot);
103
+ if (installPaths.length > 0) {
104
+ await installSkillToTargets(skill.name, installPaths, config);
105
+ }
106
+ const nextIndex = upsertSkill(index, {
107
+ name: skill.name,
108
+ source,
109
+ checksum: parsed.checksum,
110
+ updatedAt: metadata.updatedAt,
111
+ lastSync: new Date().toISOString(),
112
+ });
113
+ index.skills = nextIndex.skills;
114
+ }
115
+ export function registerUpdate(program) {
11
116
  program
12
117
  .command("update")
13
118
  .argument("[name]", "Skill name")
14
- .option("--system", "Allow system-scope updates")
15
119
  .option("--project <path>", "Only update installs for a project")
16
120
  .option("--json", "JSON output")
17
121
  .action(async (name, options) => {
18
122
  try {
19
123
  const index = await loadIndex();
20
- const config = await loadConfig();
21
124
  const targets = name ? index.skills.filter((skill) => skill.name === name) : index.skills;
22
125
  if (name && targets.length === 0) {
23
126
  throw new Error(`Skill not found: ${name}`);
24
127
  }
25
- const updated = [];
26
128
  await ensureSkillsDir();
129
+ const config = await loadConfig();
27
130
  const projectRoot = options.project ? path.resolve(options.project) : null;
131
+ const results = [];
28
132
  for (const skill of targets) {
29
- if (skill.source.type !== "url" || !skill.source.url) {
30
- continue;
133
+ if (skill.source.type === "url") {
134
+ try {
135
+ await updateUrlSkill(skill, index, projectRoot, config);
136
+ results.push({ name: skill.name, source: "url", status: "updated" });
137
+ }
138
+ catch (err) {
139
+ results.push({
140
+ name: skill.name,
141
+ source: "url",
142
+ status: "failed",
143
+ error: err instanceof Error ? err.message : "unknown error",
144
+ });
145
+ }
31
146
  }
32
- const markdown = await fetchText(skill.source.url);
33
- const parsed = parseSkillMarkdown(markdown);
34
- if (!parsed.description) {
35
- throw new Error(`Skill ${skill.name} is missing a description after update.`);
147
+ else if (skill.source.type === "git") {
148
+ try {
149
+ await updateGitSkill(skill, index, projectRoot, config);
150
+ results.push({ name: skill.name, source: "git", status: "updated" });
151
+ }
152
+ catch (err) {
153
+ results.push({
154
+ name: skill.name,
155
+ source: "git",
156
+ status: "failed",
157
+ error: err instanceof Error ? err.message : "unknown error",
158
+ });
159
+ }
36
160
  }
37
- const metadata = buildMetadata(parsed, { type: "url", url: skill.source.url }, skill.name);
38
- await writeSkillFiles(skill.name, markdown, metadata);
39
- const allowSystem = options.system || config.manageSystem;
40
- const installPaths = (skill.installs ?? [])
41
- .filter((install) => allowSystem || install.scope !== "system")
42
- .filter((install) => !projectRoot || install.projectRoot === projectRoot)
43
- .map((install) => install.path);
44
- if (installPaths.length > 0) {
45
- await copySkillToInstallPaths(skill.name, installPaths);
161
+ else {
162
+ results.push({ name: skill.name, source: skill.source.type, status: "skipped" });
46
163
  }
47
- const nextIndex = upsertSkill(index, {
48
- name: skill.name,
49
- source: { type: "url", url: skill.source.url },
50
- checksum: parsed.checksum,
51
- updatedAt: metadata.updatedAt,
52
- lastSync: new Date().toISOString(),
53
- });
54
- index.skills = nextIndex.skills;
55
- updated.push(skill.name);
56
164
  }
57
165
  await saveIndex(sortIndex(index));
166
+ const sourceGroups = groupBySource(results);
167
+ const totalUpdated = results.filter((r) => r.status === "updated").length;
168
+ const totalFailed = results.filter((r) => r.status === "failed").length;
169
+ const totalSkipped = results.filter((r) => r.status === "skipped").length;
170
+ const totalTrackable = results.filter((r) => r.source !== "local").length;
58
171
  if (isJsonEnabled(options)) {
59
172
  printJson({
60
173
  ok: true,
61
174
  command: "update",
62
175
  data: {
63
176
  name: name ?? null,
64
- system: Boolean(options.system),
65
177
  project: projectRoot,
66
- updated,
178
+ total: results.length,
179
+ updated: totalUpdated,
180
+ failed: totalFailed,
181
+ skipped: totalSkipped,
182
+ results,
183
+ bySource: sourceGroups,
67
184
  },
68
185
  });
69
186
  return;
70
187
  }
71
- if (updated.length === 0) {
72
- printInfo("No skills updated.");
188
+ if (results.length === 0) {
189
+ printInfo("No skills to update.");
73
190
  return;
74
191
  }
75
- printInfo(`Updated ${updated.length} skill(s):`);
76
- for (const skillName of updated) {
77
- printInfo(`- ${skillName}`);
192
+ printInfo("Skill Update");
193
+ for (const group of sourceGroups) {
194
+ printInfo("");
195
+ printSourceGroup(group);
196
+ }
197
+ // Summary line
198
+ printInfo("");
199
+ if (totalFailed > 0) {
200
+ printInfo(`Updated ${totalUpdated} of ${totalTrackable} trackable skills (${totalFailed} failed).`);
201
+ }
202
+ else if (totalUpdated > 0) {
203
+ printInfo(`Updated ${totalUpdated} of ${totalTrackable} trackable skills.`);
204
+ }
205
+ else if (totalSkipped > 0 && totalTrackable === 0) {
206
+ printInfo("No trackable skills to update.");
78
207
  }
79
208
  }
80
209
  catch (error) {
81
210
  handleCommandError(options, "update", error);
82
211
  }
83
212
  });
84
- };
213
+ }
@@ -1,6 +1,6 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
1
  import os from "node:os";
2
+ import path from "node:path";
3
+ import { exists } from "./fs-utils.js";
4
4
  const home = os.homedir();
5
5
  const agentRoots = {
6
6
  opencode: [path.join(home, ".config", "opencode")],
@@ -10,16 +10,7 @@ const agentRoots = {
10
10
  amp: [path.join(home, ".config", "agents")],
11
11
  antigravity: [path.join(home, ".gemini", "antigravity")],
12
12
  };
13
- const exists = async (target) => {
14
- try {
15
- await fs.access(target);
16
- return true;
17
- }
18
- catch {
19
- return false;
20
- }
21
- };
22
- export const detectAgents = async () => {
13
+ export async function detectAgents() {
23
14
  const detected = [];
24
15
  for (const [agent, roots] of Object.entries(agentRoots)) {
25
16
  const matches = await Promise.all(roots.map((root) => exists(root)));
@@ -28,4 +19,4 @@ export const detectAgents = async () => {
28
19
  }
29
20
  }
30
21
  return detected;
31
- };
22
+ }