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.
- package/README.md +73 -105
- package/dist/cli.js +15 -13
- package/dist/commands/add-repo.js +176 -0
- package/dist/commands/add.js +59 -29
- package/dist/commands/agent.js +5 -11
- package/dist/commands/config.js +11 -9
- package/dist/commands/convert.js +21 -6
- package/dist/commands/import.js +67 -77
- package/dist/commands/list.js +129 -92
- package/dist/commands/meta.js +5 -7
- package/dist/commands/project.js +24 -39
- package/dist/commands/remove.js +101 -0
- package/dist/commands/status.js +110 -101
- package/dist/commands/update.js +172 -43
- package/dist/lib/agent-detect.js +4 -13
- package/dist/lib/agents.js +64 -45
- package/dist/lib/command.js +6 -3
- package/dist/lib/config.js +19 -12
- package/dist/lib/discovery.js +3 -11
- package/dist/lib/fetcher.js +3 -3
- package/dist/lib/fs-utils.js +17 -0
- package/dist/lib/github.js +43 -0
- package/dist/lib/global-skills.js +23 -34
- package/dist/lib/grouping.js +6 -6
- package/dist/lib/index.js +13 -11
- package/dist/lib/installs.js +20 -13
- package/dist/lib/onboarding.js +31 -32
- package/dist/lib/options.js +4 -4
- package/dist/lib/output.js +13 -11
- package/dist/lib/paths.js +12 -4
- package/dist/lib/project-paths.js +2 -2
- package/dist/lib/project-root.js +3 -12
- package/dist/lib/projects.js +13 -11
- package/dist/lib/repo-skills.js +124 -0
- package/dist/lib/runtime.js +12 -12
- package/dist/lib/skill-parser.js +12 -12
- package/dist/lib/skill-store.js +13 -13
- package/dist/lib/source-grouping.js +49 -0
- package/dist/lib/sync.js +39 -16
- 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
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,129 +1,138 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
17
|
-
const results = [];
|
|
94
|
+
const statuses = [];
|
|
18
95
|
for (const skill of index.skills) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
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
|
-
|
|
71
|
-
outdated,
|
|
72
|
-
upToDate,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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 (
|
|
72
|
-
printInfo("No skills
|
|
188
|
+
if (results.length === 0) {
|
|
189
|
+
printInfo("No skills to update.");
|
|
73
190
|
return;
|
|
74
191
|
}
|
|
75
|
-
printInfo(
|
|
76
|
-
for (const
|
|
77
|
-
printInfo(
|
|
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
|
+
}
|
package/dist/lib/agent-detect.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|