skillbox 0.2.1 → 0.2.2
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 +62 -12
- package/dist/cli.js +2 -0
- package/dist/commands/add-repo.js +157 -0
- package/dist/commands/add.js +39 -12
- package/dist/commands/config.js +6 -2
- package/dist/commands/import.js +8 -15
- package/dist/commands/list.js +1 -2
- package/dist/commands/remove.js +76 -0
- package/dist/commands/status.js +0 -17
- package/dist/commands/update.js +54 -17
- package/dist/lib/agents.js +0 -5
- package/dist/lib/command.js +4 -1
- package/dist/lib/config.js +5 -2
- package/dist/lib/github.js +43 -0
- package/dist/lib/global-skills.js +12 -32
- package/dist/lib/installs.js +10 -4
- package/dist/lib/repo-skills.js +136 -0
- package/dist/lib/sync.js +31 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,68 +13,108 @@
|
|
|
13
13
|
npm install -g skillbox
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
16
|
## Quick Start
|
|
19
17
|
|
|
20
|
-
Skillbox will detect installed agents on your machine.
|
|
18
|
+
Skillbox will detect installed agents on your machine. Repo and URL installs track their origin so updates stay one command away.
|
|
21
19
|
|
|
22
|
-
Tip: run `skillbox list` right after install to see existing skills.
|
|
20
|
+
> Tip: run `skillbox list` right after install to see existing skills.
|
|
21
|
+
|
|
22
|
+
Skillbox links agent folders to the canonical store using symlinks on macOS/Linux and file copies on Windows.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
+
# install all repo skills
|
|
26
|
+
skillbox add owner/repo
|
|
27
|
+
# list skills in repo
|
|
28
|
+
skillbox add owner/repo --list
|
|
29
|
+
# install single repo skill
|
|
30
|
+
skillbox add owner/repo --skill linting
|
|
31
|
+
# add skill from URL
|
|
25
32
|
skillbox add https://example.com/skills/linting/SKILL.md
|
|
33
|
+
# list installed skills
|
|
26
34
|
skillbox list
|
|
35
|
+
# check for updates
|
|
27
36
|
skillbox status
|
|
37
|
+
# update one skill
|
|
28
38
|
skillbox update linting
|
|
29
39
|
```
|
|
30
40
|
|
|
41
|
+
Notes:
|
|
42
|
+
- GitHub unauthenticated API limit: 60 req/hr per IP
|
|
43
|
+
- use `skillbox convert` for non-skill URLs
|
|
44
|
+
|
|
31
45
|
## Commands
|
|
32
46
|
|
|
33
47
|
### Core Commands
|
|
34
48
|
|
|
35
49
|
```bash
|
|
50
|
+
# add skill from URL
|
|
36
51
|
skillbox add <url> [--name <name>] [--global] [--agents ...]
|
|
52
|
+
# add skill(s) from repo
|
|
53
|
+
skillbox add <repo> [--list] [--skill <name>] [--global] [--agents ...]
|
|
54
|
+
# convert content to skill
|
|
37
55
|
skillbox convert <url> [--name <name>] [--output <dir>] [--agent]
|
|
56
|
+
# list skills
|
|
38
57
|
skillbox list [--group=category|namespace|source|project] [--json]
|
|
58
|
+
# check for updates
|
|
39
59
|
skillbox status [--group=project|source] [--json]
|
|
40
|
-
|
|
60
|
+
# update skills
|
|
61
|
+
skillbox update [name] [--project <path>]
|
|
62
|
+
# remove skills
|
|
63
|
+
skillbox remove <name> [--project <path>]
|
|
64
|
+
# import existing skills
|
|
41
65
|
skillbox import <path>
|
|
66
|
+
# update metadata
|
|
42
67
|
skillbox meta set <name> --category foo --tag bar --namespace baz
|
|
68
|
+
# open agent REPL
|
|
43
69
|
skillbox agent
|
|
44
70
|
```
|
|
45
71
|
|
|
46
72
|
### Project Commands
|
|
47
73
|
|
|
48
74
|
```bash
|
|
75
|
+
# register project
|
|
49
76
|
skillbox project add <path> [--agent-path agent=path]
|
|
77
|
+
# list projects
|
|
50
78
|
skillbox project list
|
|
79
|
+
# show project details
|
|
51
80
|
skillbox project inspect <path>
|
|
81
|
+
# resync project skills
|
|
52
82
|
skillbox project sync <path>
|
|
53
83
|
```
|
|
54
84
|
|
|
55
85
|
### Config
|
|
56
86
|
|
|
57
87
|
```bash
|
|
88
|
+
# show config
|
|
58
89
|
skillbox config get
|
|
90
|
+
# set default scope
|
|
59
91
|
skillbox config set --default-scope user
|
|
92
|
+
# replace default agents
|
|
60
93
|
skillbox config set --default-agent claude --default-agent cursor
|
|
94
|
+
# add default agent
|
|
61
95
|
skillbox config set --add-agent codex
|
|
62
|
-
|
|
96
|
+
# use symlink installs
|
|
97
|
+
skillbox config set --install-mode symlink
|
|
98
|
+
# use file copies
|
|
99
|
+
skillbox config set --install-mode copy
|
|
63
100
|
```
|
|
64
101
|
|
|
65
102
|
Config defaults live in `~/.config/skillbox/config.json`:
|
|
66
103
|
|
|
67
|
-
- `defaultScope`: `
|
|
104
|
+
- `defaultScope`: `user` (default) or `project`
|
|
68
105
|
- `defaultAgents`: empty array means all agents
|
|
69
|
-
- `
|
|
106
|
+
- `installMode`: `symlink` (macOS/Linux) or `copy` (Windows)
|
|
70
107
|
|
|
71
108
|
## Agent Mode
|
|
72
109
|
|
|
73
110
|
Use `--json` for machine-readable output.
|
|
74
111
|
|
|
75
112
|
```bash
|
|
113
|
+
# list skills (json)
|
|
76
114
|
skillbox list --json
|
|
115
|
+
# status check (json)
|
|
77
116
|
skillbox status --json
|
|
117
|
+
# update one skill (json)
|
|
78
118
|
skillbox update linting --json
|
|
79
119
|
```
|
|
80
120
|
|
|
@@ -89,9 +129,21 @@ Common workflow:
|
|
|
89
129
|
3) skillbox update <name> --json
|
|
90
130
|
|
|
91
131
|
If you need to install a new skill from a URL, run:
|
|
132
|
+
# add skill from URL
|
|
92
133
|
skillbox add <url> [--name <name>]
|
|
93
134
|
|
|
135
|
+
If you need a skill from a repo, run:
|
|
136
|
+
# list skills in repo
|
|
137
|
+
skillbox add owner/repo --list
|
|
138
|
+
# install single repo skill
|
|
139
|
+
skillbox add owner/repo --skill <name>
|
|
140
|
+
# install all repo skills
|
|
141
|
+
skillbox add owner/repo
|
|
142
|
+
|
|
143
|
+
Note: GitHub unauthenticated API limits are 60 requests per hour per IP, so heavy repo usage may hit rate limits.
|
|
144
|
+
|
|
94
145
|
If a URL is not a valid skill, run:
|
|
146
|
+
# convert content to skill
|
|
95
147
|
skillbox convert <url> --agent
|
|
96
148
|
```
|
|
97
149
|
|
|
@@ -114,12 +166,10 @@ Agent paths (default):
|
|
|
114
166
|
- OpenCode: `.opencode/skills/`, `~/.config/opencode/skills/` (Claude-compatible `.claude/skills/` also supported)
|
|
115
167
|
- Claude: `.claude/skills/`, `~/.claude/skills/`
|
|
116
168
|
- Cursor: `.cursor/skills/`, `.claude/skills/`, `~/.cursor/skills/`, `~/.claude/skills/`
|
|
117
|
-
- Codex: `$REPO_ROOT/.codex/skills/`, `~/.codex/skills
|
|
169
|
+
- Codex: `$REPO_ROOT/.codex/skills/`, `~/.codex/skills/`
|
|
118
170
|
- Amp: `.agents/skills/`, `~/.config/agents/skills/` (Claude-compatible `.claude/skills/` also supported)
|
|
119
171
|
- Antigravity: `.agent/skills/`, `~/.gemini/antigravity/skills/`
|
|
120
172
|
|
|
121
|
-
Note: only Codex defines a system scope path.
|
|
122
|
-
|
|
123
173
|
## Usage with AI Agents
|
|
124
174
|
|
|
125
175
|
### Just ask the agent
|
|
@@ -153,7 +203,7 @@ git clone https://github.com/christiananagnostou/skillbox
|
|
|
153
203
|
cd skillbox
|
|
154
204
|
npm install
|
|
155
205
|
npm run build
|
|
156
|
-
npm link
|
|
206
|
+
npm link
|
|
157
207
|
```
|
|
158
208
|
|
|
159
209
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerMeta } from "./commands/meta.js";
|
|
|
10
10
|
import { registerProject } from "./commands/project.js";
|
|
11
11
|
import { registerAgent } from "./commands/agent.js";
|
|
12
12
|
import { registerConfig } from "./commands/config.js";
|
|
13
|
+
import { registerRemove } from "./commands/remove.js";
|
|
13
14
|
const program = new Command();
|
|
14
15
|
program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.1.0");
|
|
15
16
|
registerAdd(program);
|
|
@@ -22,6 +23,7 @@ registerMeta(program);
|
|
|
22
23
|
registerProject(program);
|
|
23
24
|
registerAgent(program);
|
|
24
25
|
registerConfig(program);
|
|
26
|
+
registerRemove(program);
|
|
25
27
|
const run = async () => {
|
|
26
28
|
const { runOnboarding } = await import("./lib/onboarding.js");
|
|
27
29
|
await runOnboarding();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { listRepoSkills, normalizeRepoRef, fetchRepoFile, writeRepoSkillDirectory, } from "../lib/repo-skills.js";
|
|
2
|
+
import { parseSkillMarkdown, buildMetadata } from "../lib/skill-parser.js";
|
|
3
|
+
import { writeSkillMetadata } from "../lib/skill-store.js";
|
|
4
|
+
import { loadConfig } from "../lib/config.js";
|
|
5
|
+
import { buildProjectAgentPaths } from "../lib/project-paths.js";
|
|
6
|
+
import { resolveRuntime, ensureProjectRegistered } from "../lib/runtime.js";
|
|
7
|
+
import { buildSymlinkWarning, buildTargets, installSkillToTargets } from "../lib/sync.js";
|
|
8
|
+
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
9
|
+
import { printInfo, printJson } from "../lib/output.js";
|
|
10
|
+
import { parseRepoRef } from "../lib/github.js";
|
|
11
|
+
import { getErrorMessage } from "../lib/command.js";
|
|
12
|
+
const normalizeSkillSelection = (skills, selections) => {
|
|
13
|
+
if (selections.length === 0) {
|
|
14
|
+
return skills;
|
|
15
|
+
}
|
|
16
|
+
const selectionSet = new Set(selections);
|
|
17
|
+
return skills.filter((name) => selectionSet.has(name));
|
|
18
|
+
};
|
|
19
|
+
const ensureRepoRef = async (input) => {
|
|
20
|
+
const ref = parseRepoRef(input);
|
|
21
|
+
if (!ref) {
|
|
22
|
+
throw new Error("Unsupported repo URL or shorthand.");
|
|
23
|
+
}
|
|
24
|
+
return await normalizeRepoRef(ref);
|
|
25
|
+
};
|
|
26
|
+
const installSkillTargets = async (skillName, options, installs) => {
|
|
27
|
+
const { projectRoot, scope, agentList } = await resolveRuntime({
|
|
28
|
+
global: options.global,
|
|
29
|
+
agents: options.agents,
|
|
30
|
+
});
|
|
31
|
+
const projectEntry = await ensureProjectRegistered(projectRoot, scope);
|
|
32
|
+
const paths = buildProjectAgentPaths(projectRoot, projectEntry);
|
|
33
|
+
const config = await loadConfig();
|
|
34
|
+
for (const agent of agentList) {
|
|
35
|
+
const map = paths[agent];
|
|
36
|
+
if (!map) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const targets = buildTargets(agent, map, scope).map((target) => target.path);
|
|
40
|
+
const results = await installSkillToTargets(skillName, targets, config);
|
|
41
|
+
const written = results
|
|
42
|
+
.filter((result) => result.mode !== "skipped")
|
|
43
|
+
.map((result) => result.path);
|
|
44
|
+
const warning = buildSymlinkWarning(agent, results);
|
|
45
|
+
if (warning) {
|
|
46
|
+
printInfo(warning);
|
|
47
|
+
}
|
|
48
|
+
if (written.length > 0) {
|
|
49
|
+
for (const target of written) {
|
|
50
|
+
installs.push({
|
|
51
|
+
scope,
|
|
52
|
+
agent,
|
|
53
|
+
path: target,
|
|
54
|
+
projectRoot: scope === "project" ? projectRoot : undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
export const isRepoUrl = (input) => {
|
|
61
|
+
return Boolean(parseRepoRef(input));
|
|
62
|
+
};
|
|
63
|
+
export const handleRepoInstall = async (input, options) => {
|
|
64
|
+
const ref = await ensureRepoRef(input);
|
|
65
|
+
const { skills } = await listRepoSkills(ref);
|
|
66
|
+
const skillNames = skills.map((skill) => skill.name).sort();
|
|
67
|
+
if (options.list) {
|
|
68
|
+
if (options.json) {
|
|
69
|
+
printJson({ ok: true, command: "add", data: { repo: input, skills: skillNames } });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
printInfo(`Skills found: ${skillNames.length}`);
|
|
73
|
+
for (const name of skillNames) {
|
|
74
|
+
printInfo(`- ${name}`);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const selected = normalizeSkillSelection(skillNames, options.skill ?? []);
|
|
79
|
+
if (selected.length === 0) {
|
|
80
|
+
throw new Error("No matching skills found. Use --list to see available skills.");
|
|
81
|
+
}
|
|
82
|
+
const summary = { installed: [], updated: [], skipped: [], failed: [] };
|
|
83
|
+
const index = await loadIndex();
|
|
84
|
+
for (const skill of skills) {
|
|
85
|
+
if (!selected.includes(skill.name)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const alreadyInstalled = index.skills.some((entry) => entry.name === skill.name);
|
|
89
|
+
try {
|
|
90
|
+
const skillMarkdown = await fetchRepoFile(ref, ref.path ? `${ref.path}/${skill.skillFile}` : skill.skillFile);
|
|
91
|
+
const parsed = parseSkillMarkdown(skillMarkdown);
|
|
92
|
+
if (!parsed.description) {
|
|
93
|
+
summary.skipped.push(skill.name);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
await writeRepoSkillDirectory(ref, skill.path, skill.name);
|
|
97
|
+
const sourcePath = [ref.path, skill.path].filter(Boolean).join("/");
|
|
98
|
+
const source = {
|
|
99
|
+
type: "git",
|
|
100
|
+
repo: `${ref.owner}/${ref.repo}`,
|
|
101
|
+
path: sourcePath || undefined,
|
|
102
|
+
ref: ref.ref,
|
|
103
|
+
};
|
|
104
|
+
const metadata = buildMetadata(parsed, source, skill.name);
|
|
105
|
+
await writeSkillMetadata(skill.name, metadata);
|
|
106
|
+
const updated = upsertSkill(index, {
|
|
107
|
+
name: skill.name,
|
|
108
|
+
source,
|
|
109
|
+
checksum: parsed.checksum,
|
|
110
|
+
updatedAt: metadata.updatedAt,
|
|
111
|
+
});
|
|
112
|
+
const installs = [];
|
|
113
|
+
await installSkillTargets(skill.name, options, installs);
|
|
114
|
+
const nextIndex = upsertSkill(updated, {
|
|
115
|
+
name: skill.name,
|
|
116
|
+
source,
|
|
117
|
+
checksum: parsed.checksum,
|
|
118
|
+
updatedAt: metadata.updatedAt,
|
|
119
|
+
installs,
|
|
120
|
+
});
|
|
121
|
+
index.skills = nextIndex.skills;
|
|
122
|
+
if (alreadyInstalled) {
|
|
123
|
+
summary.updated.push(skill.name);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
summary.installed.push(skill.name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = getErrorMessage(error, "unknown");
|
|
131
|
+
summary.failed.push({ name: skill.name, reason: message });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await saveIndex(sortIndex(index));
|
|
135
|
+
if (options.json) {
|
|
136
|
+
printJson({ ok: true, command: "add", data: summary });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (summary.failed.length > 0) {
|
|
140
|
+
printInfo("Some skills failed to install:");
|
|
141
|
+
for (const failure of summary.failed) {
|
|
142
|
+
printInfo(`- ${failure.name}: ${failure.reason}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (summary.installed.length > 0) {
|
|
146
|
+
printInfo(`Installed ${summary.installed.length} skill(s): ${summary.installed.join(", ")}`);
|
|
147
|
+
}
|
|
148
|
+
if (summary.updated.length > 0) {
|
|
149
|
+
printInfo(`Updated ${summary.updated.length} skill(s): ${summary.updated.join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
if (summary.skipped.length > 0) {
|
|
152
|
+
printInfo(`Skipped ${summary.skipped.length} skill(s): ${summary.skipped.join(", ")}`);
|
|
153
|
+
}
|
|
154
|
+
if (summary.installed.length === 0 && summary.updated.length === 0) {
|
|
155
|
+
printInfo("No agent targets were updated (canonical store only).");
|
|
156
|
+
}
|
|
157
|
+
};
|
package/dist/commands/add.js
CHANGED
|
@@ -4,19 +4,33 @@ import { parseSkillMarkdown, inferNameFromUrl, buildMetadata } from "../lib/skil
|
|
|
4
4
|
import { handleCommandError } from "../lib/command.js";
|
|
5
5
|
import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
|
|
6
6
|
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
7
|
-
import { buildTargets,
|
|
7
|
+
import { buildSymlinkWarning, buildTargets, installSkillToTargets } from "../lib/sync.js";
|
|
8
8
|
import { buildProjectAgentPaths } from "../lib/project-paths.js";
|
|
9
9
|
import { resolveRuntime, ensureProjectRegistered } from "../lib/runtime.js";
|
|
10
|
+
import { loadConfig } from "../lib/config.js";
|
|
11
|
+
import { handleRepoInstall, isRepoUrl } from "./add-repo.js";
|
|
10
12
|
export const registerAdd = (program) => {
|
|
11
13
|
program
|
|
12
14
|
.command("add")
|
|
13
|
-
.argument("<url>", "Skill URL")
|
|
15
|
+
.argument("<url>", "Skill URL or repo")
|
|
14
16
|
.option("--name <name>", "Override skill name")
|
|
15
17
|
.option("--global", "Install to user scope")
|
|
16
18
|
.option("--agents <list>", "Comma-separated agent list")
|
|
19
|
+
.option("--skill <skill>", "Skill name to install", collect)
|
|
20
|
+
.option("--list", "List skills in repo without installing")
|
|
17
21
|
.option("--json", "JSON output")
|
|
18
22
|
.action(async (url, options) => {
|
|
19
23
|
try {
|
|
24
|
+
if (options.list || options.skill || isRepoUrl(url)) {
|
|
25
|
+
await handleRepoInstall(url, {
|
|
26
|
+
global: options.global,
|
|
27
|
+
agents: options.agents,
|
|
28
|
+
json: options.json,
|
|
29
|
+
list: options.list,
|
|
30
|
+
skill: options.skill,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
20
34
|
const skillMarkdown = await fetchText(url);
|
|
21
35
|
const parsed = parseSkillMarkdown(skillMarkdown);
|
|
22
36
|
const inferred = inferNameFromUrl(url);
|
|
@@ -46,6 +60,7 @@ export const registerAdd = (program) => {
|
|
|
46
60
|
});
|
|
47
61
|
const projectEntry = await ensureProjectRegistered(projectRoot, scope);
|
|
48
62
|
const paths = buildProjectAgentPaths(projectRoot, projectEntry);
|
|
63
|
+
const config = await loadConfig();
|
|
49
64
|
const installed = [];
|
|
50
65
|
const installs = [];
|
|
51
66
|
for (const agent of agentList) {
|
|
@@ -54,15 +69,24 @@ export const registerAdd = (program) => {
|
|
|
54
69
|
continue;
|
|
55
70
|
}
|
|
56
71
|
const targets = buildTargets(agent, map, scope).map((target) => target.path);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
const results = await installSkillToTargets(skillName, targets, config);
|
|
73
|
+
const written = results
|
|
74
|
+
.filter((result) => result.mode !== "skipped")
|
|
75
|
+
.map((result) => result.path);
|
|
76
|
+
const warning = buildSymlinkWarning(agent, results);
|
|
77
|
+
if (warning) {
|
|
78
|
+
printInfo(warning);
|
|
79
|
+
}
|
|
80
|
+
if (written.length > 0) {
|
|
81
|
+
installed.push({ agent, scope, targets: written });
|
|
82
|
+
for (const target of written) {
|
|
83
|
+
installs.push({
|
|
84
|
+
scope,
|
|
85
|
+
agent,
|
|
86
|
+
path: target,
|
|
87
|
+
projectRoot: scope === "project" ? projectRoot : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
66
90
|
}
|
|
67
91
|
}
|
|
68
92
|
const nextIndex = upsertSkill(updated, {
|
|
@@ -90,7 +114,7 @@ export const registerAdd = (program) => {
|
|
|
90
114
|
printInfo(`Source: ${url}`);
|
|
91
115
|
printInfo(`Scope: ${scope}`);
|
|
92
116
|
if (installed.length === 0) {
|
|
93
|
-
printInfo("No agent targets were updated.");
|
|
117
|
+
printInfo("No agent targets were updated (canonical store only).");
|
|
94
118
|
}
|
|
95
119
|
else {
|
|
96
120
|
for (const entry of installed) {
|
|
@@ -103,3 +127,6 @@ export const registerAdd = (program) => {
|
|
|
103
127
|
}
|
|
104
128
|
});
|
|
105
129
|
};
|
|
130
|
+
const collect = (value, previous = []) => {
|
|
131
|
+
return [...previous, value];
|
|
132
|
+
};
|
package/dist/commands/config.js
CHANGED
|
@@ -27,7 +27,7 @@ export const registerConfig = (program) => {
|
|
|
27
27
|
.option("--default-agent <agent>", "Default agent", collect)
|
|
28
28
|
.option("--add-agent <agent>", "Add to default agents", collect)
|
|
29
29
|
.option("--default-scope <scope>", "Default scope: project or user")
|
|
30
|
-
.option("--
|
|
30
|
+
.option("--install-mode <mode>", "Install mode: symlink or copy")
|
|
31
31
|
.option("--json", "JSON output")
|
|
32
32
|
.action(async (options) => {
|
|
33
33
|
try {
|
|
@@ -39,11 +39,15 @@ export const registerConfig = (program) => {
|
|
|
39
39
|
const addedAgents = options.addAgent ?? [];
|
|
40
40
|
const nextAgents = options.defaultAgent ?? current.defaultAgents;
|
|
41
41
|
const mergedAgents = Array.from(new Set([...(nextAgents ?? []), ...addedAgents].filter((agent) => agent.length > 0)));
|
|
42
|
+
const nextMode = options.installMode ?? current.installMode;
|
|
43
|
+
if (nextMode !== "symlink" && nextMode !== "copy") {
|
|
44
|
+
throw new Error("installMode must be 'symlink' or 'copy'.");
|
|
45
|
+
}
|
|
42
46
|
const next = {
|
|
43
47
|
...current,
|
|
44
48
|
defaultAgents: mergedAgents,
|
|
45
49
|
defaultScope: nextScope,
|
|
46
|
-
|
|
50
|
+
installMode: nextMode,
|
|
47
51
|
};
|
|
48
52
|
await saveConfig(next);
|
|
49
53
|
if (isJsonEnabled(options)) {
|
package/dist/commands/import.js
CHANGED
|
@@ -6,24 +6,20 @@ import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
|
|
|
6
6
|
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
7
7
|
import { handleCommandError } from "../lib/command.js";
|
|
8
8
|
import { discoverSkills } from "../lib/discovery.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getUserAgentPaths } from "../lib/agents.js";
|
|
10
10
|
export const registerImport = (program) => {
|
|
11
11
|
program
|
|
12
12
|
.command("import")
|
|
13
13
|
.argument("[path]", "Path to skill directory")
|
|
14
14
|
.option("--global", "Import skills from user agent folders")
|
|
15
|
-
.option("--system", "Import skills from system agent folders")
|
|
16
15
|
.option("--json", "JSON output")
|
|
17
16
|
.action(async (inputPath, options) => {
|
|
18
17
|
try {
|
|
19
|
-
if (!inputPath && !options.global
|
|
20
|
-
throw new Error("Provide a path or use --global
|
|
18
|
+
if (!inputPath && !options.global) {
|
|
19
|
+
throw new Error("Provide a path or use --global.");
|
|
21
20
|
}
|
|
22
|
-
if (options.global
|
|
23
|
-
const summary = await importGlobalSkills(
|
|
24
|
-
includeUser: Boolean(options.global),
|
|
25
|
-
includeSystem: Boolean(options.system),
|
|
26
|
-
});
|
|
21
|
+
if (options.global) {
|
|
22
|
+
const summary = await importGlobalSkills();
|
|
27
23
|
if (isJsonEnabled(options)) {
|
|
28
24
|
printJson({
|
|
29
25
|
ok: true,
|
|
@@ -71,12 +67,9 @@ export const registerImport = (program) => {
|
|
|
71
67
|
}
|
|
72
68
|
});
|
|
73
69
|
};
|
|
74
|
-
const importGlobalSkills = async (
|
|
70
|
+
const importGlobalSkills = async () => {
|
|
75
71
|
const projectRoot = process.cwd();
|
|
76
|
-
const paths =
|
|
77
|
-
...(options.includeUser ? getUserAgentPaths(projectRoot) : []),
|
|
78
|
-
...(options.includeSystem ? getSystemAgentPaths(projectRoot) : []),
|
|
79
|
-
];
|
|
72
|
+
const paths = getUserAgentPaths(projectRoot);
|
|
80
73
|
const discovered = await discoverSkills(paths);
|
|
81
74
|
const index = await loadIndex();
|
|
82
75
|
const seen = new Set(index.skills.map((skill) => skill.name));
|
|
@@ -103,7 +96,7 @@ const importGlobalSkills = async (options) => {
|
|
|
103
96
|
updatedAt: metadata.updatedAt,
|
|
104
97
|
installs: [
|
|
105
98
|
{
|
|
106
|
-
scope:
|
|
99
|
+
scope: "user",
|
|
107
100
|
agent: "unknown",
|
|
108
101
|
path: skill.skillDir,
|
|
109
102
|
},
|
package/dist/commands/list.js
CHANGED
|
@@ -6,11 +6,10 @@ export const registerList = (program) => {
|
|
|
6
6
|
program
|
|
7
7
|
.command("list")
|
|
8
8
|
.option("--group <group>", "Group by category, namespace, source, project")
|
|
9
|
-
.option("--project-only", "Only show project-indexed skills")
|
|
10
9
|
.option("--json", "JSON output")
|
|
11
10
|
.action(async (options) => {
|
|
12
11
|
const index = await loadIndex();
|
|
13
|
-
const globalSkills =
|
|
12
|
+
const globalSkills = await listGlobalSkills(index.skills);
|
|
14
13
|
const skills = [...index.skills, ...globalSkills];
|
|
15
14
|
const groupedProjects = groupByProject(skills);
|
|
16
15
|
const groupedSources = groupBySource(skills);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
4
|
+
import { loadIndex, saveIndex, sortIndex } from "../lib/index.js";
|
|
5
|
+
import { skillDir } from "../lib/skill-store.js";
|
|
6
|
+
import { handleCommandError } from "../lib/command.js";
|
|
7
|
+
const removePaths = async (paths) => {
|
|
8
|
+
for (const target of paths) {
|
|
9
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export const registerRemove = (program) => {
|
|
13
|
+
program
|
|
14
|
+
.command("remove")
|
|
15
|
+
.argument("<name>", "Skill name")
|
|
16
|
+
.option("--project <path>", "Only remove installs for a project")
|
|
17
|
+
.option("--json", "JSON output")
|
|
18
|
+
.action(async (name, options) => {
|
|
19
|
+
try {
|
|
20
|
+
const index = await loadIndex();
|
|
21
|
+
const skill = index.skills.find((entry) => entry.name === name);
|
|
22
|
+
if (!skill) {
|
|
23
|
+
throw new Error(`Skill not found: ${name}`);
|
|
24
|
+
}
|
|
25
|
+
const projectRoot = options.project ? path.resolve(options.project) : null;
|
|
26
|
+
const installs = skill.installs ?? [];
|
|
27
|
+
const toRemove = projectRoot
|
|
28
|
+
? installs.filter((install) => install.scope === "project" &&
|
|
29
|
+
install.projectRoot &&
|
|
30
|
+
install.projectRoot === projectRoot)
|
|
31
|
+
: installs;
|
|
32
|
+
if (projectRoot && toRemove.length === 0) {
|
|
33
|
+
throw new Error(`No installs found for ${name} in ${projectRoot}.`);
|
|
34
|
+
}
|
|
35
|
+
const removedPaths = toRemove.map((install) => install.path);
|
|
36
|
+
await removePaths(removedPaths);
|
|
37
|
+
let removedCanonical = false;
|
|
38
|
+
if (projectRoot) {
|
|
39
|
+
const remaining = installs.filter((install) => !(install.scope === "project" &&
|
|
40
|
+
install.projectRoot &&
|
|
41
|
+
install.projectRoot === projectRoot));
|
|
42
|
+
index.skills = index.skills.map((entry) => entry.name === name
|
|
43
|
+
? { ...entry, installs: remaining.length > 0 ? remaining : undefined }
|
|
44
|
+
: entry);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
index.skills = index.skills.filter((entry) => entry.name !== name);
|
|
48
|
+
await fs.rm(skillDir(name), { recursive: true, force: true });
|
|
49
|
+
removedCanonical = true;
|
|
50
|
+
}
|
|
51
|
+
await saveIndex(sortIndex(index));
|
|
52
|
+
if (isJsonEnabled(options)) {
|
|
53
|
+
printJson({
|
|
54
|
+
ok: true,
|
|
55
|
+
command: "remove",
|
|
56
|
+
data: {
|
|
57
|
+
name,
|
|
58
|
+
project: projectRoot,
|
|
59
|
+
removed: removedPaths,
|
|
60
|
+
removedCanonical,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (projectRoot) {
|
|
66
|
+
printInfo(`Removed ${removedPaths.length} install(s) for ${name} in ${projectRoot}.`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
printInfo(`Removed ${name} and ${removedPaths.length} install(s).`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
handleCommandError(options, "remove", error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
package/dist/commands/status.js
CHANGED
|
@@ -3,7 +3,6 @@ import { loadIndex, saveIndex } from "../lib/index.js";
|
|
|
3
3
|
import { fetchText } from "../lib/fetcher.js";
|
|
4
4
|
import { hashContent } from "../lib/skill-store.js";
|
|
5
5
|
import { groupStatusByKey } from "../lib/grouping.js";
|
|
6
|
-
import { loadConfig } from "../lib/config.js";
|
|
7
6
|
import { handleCommandError } from "../lib/command.js";
|
|
8
7
|
export const registerStatus = (program) => {
|
|
9
8
|
program
|
|
@@ -13,25 +12,11 @@ export const registerStatus = (program) => {
|
|
|
13
12
|
.action(async (options) => {
|
|
14
13
|
try {
|
|
15
14
|
const index = await loadIndex();
|
|
16
|
-
const config = await loadConfig();
|
|
17
15
|
const results = [];
|
|
18
16
|
for (const skill of index.skills) {
|
|
19
17
|
const projects = Array.from(new Set((skill.installs ?? [])
|
|
20
18
|
.filter((install) => install.scope === "project" && install.projectRoot)
|
|
21
19
|
.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
20
|
if (skill.source.type !== "url" || !skill.source.url) {
|
|
36
21
|
results.push({
|
|
37
22
|
name: skill.name,
|
|
@@ -39,7 +24,6 @@ export const registerStatus = (program) => {
|
|
|
39
24
|
outdated: false,
|
|
40
25
|
localChecksum: skill.checksum,
|
|
41
26
|
projects,
|
|
42
|
-
system: isSystem,
|
|
43
27
|
});
|
|
44
28
|
continue;
|
|
45
29
|
}
|
|
@@ -54,7 +38,6 @@ export const registerStatus = (program) => {
|
|
|
54
38
|
localChecksum: skill.checksum,
|
|
55
39
|
remoteChecksum,
|
|
56
40
|
projects,
|
|
57
|
-
system: false,
|
|
58
41
|
});
|
|
59
42
|
}
|
|
60
43
|
await saveIndex(index);
|
package/dist/commands/update.js
CHANGED
|
@@ -2,51 +2,89 @@ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
|
2
2
|
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
3
3
|
import { fetchText } from "../lib/fetcher.js";
|
|
4
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";
|
|
5
|
+
import { ensureSkillsDir, writeSkillFiles, writeSkillMetadata } from "../lib/skill-store.js";
|
|
7
6
|
import path from "node:path";
|
|
8
|
-
import {
|
|
7
|
+
import { installSkillToTargets } from "../lib/sync.js";
|
|
9
8
|
import { handleCommandError } from "../lib/command.js";
|
|
9
|
+
import { loadConfig } from "../lib/config.js";
|
|
10
|
+
import { fetchRepoFile, normalizeRepoRef, writeRepoSkillDirectory } from "../lib/repo-skills.js";
|
|
11
|
+
import { getInstallPaths } from "../lib/installs.js";
|
|
10
12
|
export const registerUpdate = (program) => {
|
|
11
13
|
program
|
|
12
14
|
.command("update")
|
|
13
15
|
.argument("[name]", "Skill name")
|
|
14
|
-
.option("--system", "Allow system-scope updates")
|
|
15
16
|
.option("--project <path>", "Only update installs for a project")
|
|
16
17
|
.option("--json", "JSON output")
|
|
17
18
|
.action(async (name, options) => {
|
|
18
19
|
try {
|
|
19
20
|
const index = await loadIndex();
|
|
20
|
-
const config = await loadConfig();
|
|
21
21
|
const targets = name ? index.skills.filter((skill) => skill.name === name) : index.skills;
|
|
22
22
|
if (name && targets.length === 0) {
|
|
23
23
|
throw new Error(`Skill not found: ${name}`);
|
|
24
24
|
}
|
|
25
25
|
const updated = [];
|
|
26
26
|
await ensureSkillsDir();
|
|
27
|
+
const config = await loadConfig();
|
|
27
28
|
const projectRoot = options.project ? path.resolve(options.project) : null;
|
|
28
29
|
for (const skill of targets) {
|
|
29
|
-
if (skill.source.type
|
|
30
|
+
if (skill.source.type === "url" && skill.source.url) {
|
|
31
|
+
const markdown = await fetchText(skill.source.url);
|
|
32
|
+
const parsed = parseSkillMarkdown(markdown);
|
|
33
|
+
if (!parsed.description) {
|
|
34
|
+
throw new Error(`Skill ${skill.name} is missing a description after update.`);
|
|
35
|
+
}
|
|
36
|
+
const metadata = buildMetadata(parsed, { type: "url", url: skill.source.url }, skill.name);
|
|
37
|
+
await writeSkillFiles(skill.name, markdown, metadata);
|
|
38
|
+
const installPaths = getInstallPaths(skill, projectRoot);
|
|
39
|
+
if (installPaths.length > 0) {
|
|
40
|
+
await installSkillToTargets(skill.name, installPaths, config);
|
|
41
|
+
}
|
|
42
|
+
const nextIndex = upsertSkill(index, {
|
|
43
|
+
name: skill.name,
|
|
44
|
+
source: { type: "url", url: skill.source.url },
|
|
45
|
+
checksum: parsed.checksum,
|
|
46
|
+
updatedAt: metadata.updatedAt,
|
|
47
|
+
lastSync: new Date().toISOString(),
|
|
48
|
+
});
|
|
49
|
+
index.skills = nextIndex.skills;
|
|
50
|
+
updated.push(skill.name);
|
|
30
51
|
continue;
|
|
31
52
|
}
|
|
32
|
-
|
|
53
|
+
if (skill.source.type !== "git" || !skill.source.repo) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const [owner, repo] = skill.source.repo.split("/");
|
|
57
|
+
if (!owner || !repo) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const skillPath = skill.source.path?.replace(/\/$/, "") ?? "";
|
|
61
|
+
const ref = await normalizeRepoRef({
|
|
62
|
+
owner,
|
|
63
|
+
repo,
|
|
64
|
+
ref: skill.source.ref ?? "main",
|
|
65
|
+
});
|
|
66
|
+
const skillFilePath = skillPath ? `${skillPath}/SKILL.md` : "SKILL.md";
|
|
67
|
+
const markdown = await fetchRepoFile(ref, skillFilePath);
|
|
33
68
|
const parsed = parseSkillMarkdown(markdown);
|
|
34
69
|
if (!parsed.description) {
|
|
35
70
|
throw new Error(`Skill ${skill.name} is missing a description after update.`);
|
|
36
71
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
72
|
+
await writeRepoSkillDirectory(ref, skillPath, skill.name);
|
|
73
|
+
const source = {
|
|
74
|
+
type: "git",
|
|
75
|
+
repo: skill.source.repo,
|
|
76
|
+
path: skillPath || undefined,
|
|
77
|
+
ref: ref.ref,
|
|
78
|
+
};
|
|
79
|
+
const metadata = buildMetadata(parsed, source, skill.name);
|
|
80
|
+
await writeSkillMetadata(skill.name, metadata);
|
|
81
|
+
const installPaths = getInstallPaths(skill, projectRoot);
|
|
44
82
|
if (installPaths.length > 0) {
|
|
45
|
-
await
|
|
83
|
+
await installSkillToTargets(skill.name, installPaths, config);
|
|
46
84
|
}
|
|
47
85
|
const nextIndex = upsertSkill(index, {
|
|
48
86
|
name: skill.name,
|
|
49
|
-
source
|
|
87
|
+
source,
|
|
50
88
|
checksum: parsed.checksum,
|
|
51
89
|
updatedAt: metadata.updatedAt,
|
|
52
90
|
lastSync: new Date().toISOString(),
|
|
@@ -61,7 +99,6 @@ export const registerUpdate = (program) => {
|
|
|
61
99
|
command: "update",
|
|
62
100
|
data: {
|
|
63
101
|
name: name ?? null,
|
|
64
|
-
system: Boolean(options.system),
|
|
65
102
|
project: projectRoot,
|
|
66
103
|
updated,
|
|
67
104
|
},
|
package/dist/lib/agents.js
CHANGED
|
@@ -23,7 +23,6 @@ export const agentPaths = (projectRoot) => ({
|
|
|
23
23
|
codex: {
|
|
24
24
|
project: [path.join(projectRoot, ".codex", "skills")],
|
|
25
25
|
user: [path.join(home, ".codex", "skills")],
|
|
26
|
-
system: [path.join(path.sep, "etc", "codex", "skills")],
|
|
27
26
|
},
|
|
28
27
|
amp: {
|
|
29
28
|
project: [
|
|
@@ -42,10 +41,6 @@ export const getUserAgentPaths = (projectRoot) => {
|
|
|
42
41
|
const paths = agentPaths(projectRoot);
|
|
43
42
|
return Object.values(paths).flatMap((entry) => entry.user);
|
|
44
43
|
};
|
|
45
|
-
export const getSystemAgentPaths = (projectRoot) => {
|
|
46
|
-
const paths = agentPaths(projectRoot);
|
|
47
|
-
return Object.values(paths).flatMap((entry) => entry.system ?? []);
|
|
48
|
-
};
|
|
49
44
|
const agentSet = new Set(allAgents);
|
|
50
45
|
export const isAgentId = (value) => {
|
|
51
46
|
return agentSet.has(value);
|
package/dist/lib/command.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isJsonEnabled, printError, printJson } from "./output.js";
|
|
2
|
+
export const getErrorMessage = (error, fallback = "Unexpected error") => {
|
|
3
|
+
return error instanceof Error ? error.message : fallback;
|
|
4
|
+
};
|
|
2
5
|
export const handleCommandError = (options, command, error) => {
|
|
3
|
-
const message = error
|
|
6
|
+
const message = getErrorMessage(error);
|
|
4
7
|
if (isJsonEnabled(options)) {
|
|
5
8
|
printJson({ ok: false, command, error: { message } });
|
|
6
9
|
return;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { skillboxRoot } from "./paths.js";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
const defaultInstallMode = () => {
|
|
5
|
+
return process.platform === "win32" ? "copy" : "symlink";
|
|
6
|
+
};
|
|
4
7
|
const defaultConfig = () => ({
|
|
5
8
|
version: 1,
|
|
6
9
|
defaultAgents: [],
|
|
7
|
-
defaultScope: "
|
|
8
|
-
|
|
10
|
+
defaultScope: "user",
|
|
11
|
+
installMode: defaultInstallMode(),
|
|
9
12
|
});
|
|
10
13
|
export const configPath = () => path.join(skillboxRoot(), "config.json");
|
|
11
14
|
export const loadConfig = async () => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fetchText } from "./fetcher.js";
|
|
2
|
+
const repoUrlRegex = /^https?:\/\/github\.com\/(.+?)\/(.+?)(?:\.git)?(?:\/)?$/i;
|
|
3
|
+
const treeUrlRegex = /^https?:\/\/github\.com\/(.+?)\/(.+?)\/tree\/([^/]+)\/(.+)$/i;
|
|
4
|
+
export const parseRepoRef = (input) => {
|
|
5
|
+
if (input.includes("github.com") && input.includes("/tree/")) {
|
|
6
|
+
const match = input.match(treeUrlRegex);
|
|
7
|
+
if (!match) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
owner: match[1],
|
|
12
|
+
repo: match[2],
|
|
13
|
+
ref: match[3],
|
|
14
|
+
path: match[4],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (!input.includes("github.com") && input.includes("/")) {
|
|
18
|
+
const [owner, repo] = input.split("/");
|
|
19
|
+
if (!owner || !repo) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return { owner, repo, ref: "main" };
|
|
23
|
+
}
|
|
24
|
+
if (input.includes("github.com")) {
|
|
25
|
+
const match = input.match(repoUrlRegex);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
owner: match[1],
|
|
31
|
+
repo: match[2].replace(/\.git$/, ""),
|
|
32
|
+
ref: "main",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
export const buildRawUrl = (ref, filePath) => {
|
|
38
|
+
return `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${ref.ref}/${filePath}`;
|
|
39
|
+
};
|
|
40
|
+
export const fetchJson = async (url) => {
|
|
41
|
+
const response = await fetchText(url);
|
|
42
|
+
return JSON.parse(response);
|
|
43
|
+
};
|
|
@@ -1,36 +1,16 @@
|
|
|
1
|
-
import { getUserAgentPaths
|
|
1
|
+
import { getUserAgentPaths } from "./agents.js";
|
|
2
2
|
import { discoverSkills } from "./discovery.js";
|
|
3
3
|
export const discoverGlobalSkills = async (projectRoot) => {
|
|
4
4
|
const userPaths = getUserAgentPaths(projectRoot);
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
scope: "user",
|
|
17
|
-
agent: "unknown",
|
|
18
|
-
path: skill.skillDir,
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
for (const skill of systemSkills) {
|
|
24
|
-
entries.push({
|
|
25
|
-
name: skill.name,
|
|
26
|
-
installs: [
|
|
27
|
-
{
|
|
28
|
-
scope: "system",
|
|
29
|
-
agent: "unknown",
|
|
30
|
-
path: skill.skillDir,
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
return entries;
|
|
5
|
+
const userSkills = await discoverSkills(userPaths);
|
|
6
|
+
return userSkills.map((skill) => ({
|
|
7
|
+
name: skill.name,
|
|
8
|
+
installs: [
|
|
9
|
+
{
|
|
10
|
+
scope: "user",
|
|
11
|
+
agent: "unknown",
|
|
12
|
+
path: skill.skillDir,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
}));
|
|
36
16
|
};
|
package/dist/lib/installs.js
CHANGED
|
@@ -21,13 +21,19 @@ export const getProjectSkills = (skills, projectRoot) => {
|
|
|
21
21
|
const map = collectProjectSkills(skills);
|
|
22
22
|
return (map.get(projectRoot) ?? []).sort();
|
|
23
23
|
};
|
|
24
|
+
export const getInstallPaths = (skill, projectRoot) => {
|
|
25
|
+
if (!projectRoot) {
|
|
26
|
+
return (skill.installs ?? []).map((install) => install.path);
|
|
27
|
+
}
|
|
28
|
+
return (skill.installs ?? [])
|
|
29
|
+
.filter(projectInstalls)
|
|
30
|
+
.filter((install) => install.projectRoot === projectRoot)
|
|
31
|
+
.map((install) => install.path);
|
|
32
|
+
};
|
|
24
33
|
export const getProjectInstallPaths = (skills, projectRoot) => {
|
|
25
34
|
const map = new Map();
|
|
26
35
|
for (const skill of skills) {
|
|
27
|
-
const paths = (skill
|
|
28
|
-
.filter(projectInstalls)
|
|
29
|
-
.filter((install) => install.projectRoot === projectRoot)
|
|
30
|
-
.map((install) => install.path);
|
|
36
|
+
const paths = getInstallPaths(skill, projectRoot);
|
|
31
37
|
if (paths.length > 0) {
|
|
32
38
|
map.set(skill.name, paths);
|
|
33
39
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fetchJson, buildRawUrl, parseRepoRef } from "./github.js";
|
|
4
|
+
import { fetchText } from "./fetcher.js";
|
|
5
|
+
import { ensureSkillsDir, skillDir } from "./skill-store.js";
|
|
6
|
+
const skillRoots = [
|
|
7
|
+
"skills",
|
|
8
|
+
"skill",
|
|
9
|
+
".skills",
|
|
10
|
+
".skill",
|
|
11
|
+
"agents/skills",
|
|
12
|
+
".claude/skills",
|
|
13
|
+
".codex/skills",
|
|
14
|
+
".cursor/skills",
|
|
15
|
+
".opencode/skills",
|
|
16
|
+
];
|
|
17
|
+
const buildTreeUrl = (ref) => {
|
|
18
|
+
return `https://api.github.com/repos/${ref.owner}/${ref.repo}/git/trees/${ref.ref}?recursive=1`;
|
|
19
|
+
};
|
|
20
|
+
const normalizeSkillPath = (filePath, basePath) => {
|
|
21
|
+
if (!filePath.endsWith("/SKILL.md") && filePath !== "SKILL.md") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const normalized = basePath ? filePath.replace(`${basePath}/`, "") : filePath;
|
|
25
|
+
const segments = normalized.split("/");
|
|
26
|
+
if (segments.length === 1) {
|
|
27
|
+
const name = basePath ? (basePath.split("/").filter(Boolean).pop() ?? "root") : "root";
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
path: "",
|
|
31
|
+
skillFile: normalized,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
name: segments[segments.length - 2],
|
|
36
|
+
path: segments.slice(0, -1).join("/"),
|
|
37
|
+
skillFile: normalized,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const normalizeSkillFile = (skillPath) => {
|
|
41
|
+
return skillPath ? `${skillPath}/SKILL.md` : "SKILL.md";
|
|
42
|
+
};
|
|
43
|
+
const filterSkills = (entries, basePath, includeAll = false) => {
|
|
44
|
+
const skills = [];
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.type !== "blob") {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (basePath &&
|
|
50
|
+
!entry.path.startsWith(`${basePath}/`) &&
|
|
51
|
+
entry.path !== `${basePath}/SKILL.md`) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const skill = normalizeSkillPath(entry.path, basePath);
|
|
55
|
+
if (!skill) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
skills.push(skill);
|
|
59
|
+
}
|
|
60
|
+
if (basePath) {
|
|
61
|
+
return skills;
|
|
62
|
+
}
|
|
63
|
+
if (includeAll) {
|
|
64
|
+
return skills;
|
|
65
|
+
}
|
|
66
|
+
return skills.filter((skill) => {
|
|
67
|
+
const isRoot = skill.skillFile === "SKILL.md";
|
|
68
|
+
if (isRoot) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return skillRoots.some((root) => skill.path.startsWith(root));
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
export const listRepoSkills = async (input) => {
|
|
75
|
+
const ref = typeof input === "string" ? parseRepoRef(input) : input;
|
|
76
|
+
if (!ref) {
|
|
77
|
+
throw new Error("Unsupported repo URL or shorthand.");
|
|
78
|
+
}
|
|
79
|
+
const normalized = await normalizeRepoRef(ref);
|
|
80
|
+
const tree = await fetchJson(buildTreeUrl(normalized));
|
|
81
|
+
const includeAll = normalized.repo.toLowerCase() === "skills" && !normalized.path;
|
|
82
|
+
const skills = filterSkills(tree.tree, normalized.path, includeAll);
|
|
83
|
+
if (skills.length === 0) {
|
|
84
|
+
throw new Error("No skills found in repository.");
|
|
85
|
+
}
|
|
86
|
+
return { ref: normalized, skills };
|
|
87
|
+
};
|
|
88
|
+
export const listRepoFiles = async (ref, skill, basePath) => {
|
|
89
|
+
const tree = await fetchJson(buildTreeUrl(ref));
|
|
90
|
+
const prefix = basePath ? [basePath, skill.path].filter(Boolean).join("/") : skill.path;
|
|
91
|
+
const files = tree.tree
|
|
92
|
+
.filter((entry) => entry.type === "blob")
|
|
93
|
+
.map((entry) => entry.path)
|
|
94
|
+
.filter((filePath) => (prefix ? filePath.startsWith(`${prefix}/`) : true))
|
|
95
|
+
.map((filePath) => (basePath ? filePath.replace(`${basePath}/`, "") : filePath));
|
|
96
|
+
if (files.length === 0) {
|
|
97
|
+
return [skill.skillFile];
|
|
98
|
+
}
|
|
99
|
+
return files;
|
|
100
|
+
};
|
|
101
|
+
export const fetchRepoFile = async (ref, filePath) => {
|
|
102
|
+
return await fetchText(buildRawUrl(ref, filePath));
|
|
103
|
+
};
|
|
104
|
+
export const writeRepoSkillDirectory = async (ref, skillPath, skillName) => {
|
|
105
|
+
const normalizedSkillPath = skillPath.replace(/\/$/, "");
|
|
106
|
+
const files = await listRepoFiles(ref, {
|
|
107
|
+
name: skillName,
|
|
108
|
+
path: normalizedSkillPath,
|
|
109
|
+
skillFile: normalizeSkillFile(normalizedSkillPath),
|
|
110
|
+
}, ref.path);
|
|
111
|
+
const targetDir = skillDir(skillName);
|
|
112
|
+
await ensureSkillsDir();
|
|
113
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const filePath = ref.path ? `${ref.path}/${file}` : file;
|
|
116
|
+
const content = await fetchRepoFile(ref, filePath);
|
|
117
|
+
const relative = normalizedSkillPath ? file.replace(`${normalizedSkillPath}/`, "") : file;
|
|
118
|
+
const targetPath = path.join(targetDir, relative);
|
|
119
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
120
|
+
await fs.writeFile(targetPath, content, "utf8");
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
export const normalizeRepoRef = async (ref) => {
|
|
124
|
+
try {
|
|
125
|
+
await fetchJson(buildTreeUrl(ref));
|
|
126
|
+
return ref;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
if (ref.ref === "main") {
|
|
130
|
+
const fallback = { ...ref, ref: "master" };
|
|
131
|
+
await fetchJson(buildTreeUrl(fallback));
|
|
132
|
+
return fallback;
|
|
133
|
+
}
|
|
134
|
+
throw new Error("Unable to resolve repository ref.");
|
|
135
|
+
}
|
|
136
|
+
};
|
package/dist/lib/sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { skillDir } from "./skill-store.js";
|
|
4
|
+
import { getErrorMessage } from "./command.js";
|
|
4
5
|
export const ensureDir = async (dir) => {
|
|
5
6
|
await fs.mkdir(dir, { recursive: true });
|
|
6
7
|
};
|
|
@@ -16,16 +17,42 @@ const copyFiles = async (sourceDir, targetDir) => {
|
|
|
16
17
|
await fs.copyFile(sourcePath, destPath);
|
|
17
18
|
}
|
|
18
19
|
};
|
|
19
|
-
|
|
20
|
+
const createSymlink = async (sourceDir, targetDir) => {
|
|
21
|
+
await fs.symlink(sourceDir, targetDir, "dir");
|
|
22
|
+
};
|
|
23
|
+
export const buildSymlinkWarning = (agent, results) => {
|
|
24
|
+
const skipped = results.filter((result) => result.mode === "skipped");
|
|
25
|
+
if (skipped.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const details = skipped
|
|
29
|
+
.map((result) => `${result.path}: ${result.error ?? "unknown error"}`)
|
|
30
|
+
.join("; ");
|
|
31
|
+
return `Warning: symlink failed for ${agent}. ${details}. Remove the existing target or run "skillbox config set --install-mode copy" to use file copies.`;
|
|
32
|
+
};
|
|
33
|
+
export const installSkillToTargets = async (skillName, targets, config) => {
|
|
20
34
|
const sourceDir = skillDir(skillName);
|
|
21
|
-
const
|
|
35
|
+
const results = [];
|
|
22
36
|
for (const targetRoot of targets) {
|
|
23
37
|
const targetDir = path.join(targetRoot, skillName);
|
|
38
|
+
await ensureDir(targetRoot);
|
|
39
|
+
if (config.installMode === "symlink") {
|
|
40
|
+
try {
|
|
41
|
+
await createSymlink(sourceDir, targetDir);
|
|
42
|
+
results.push({ path: targetDir, mode: "symlink" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = getErrorMessage(error, "unknown error");
|
|
47
|
+
results.push({ path: targetDir, mode: "skipped", error: message });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
24
51
|
await ensureDir(targetDir);
|
|
25
52
|
await copyFiles(sourceDir, targetDir);
|
|
26
|
-
|
|
53
|
+
results.push({ path: targetDir, mode: "copy" });
|
|
27
54
|
}
|
|
28
|
-
return
|
|
55
|
+
return results;
|
|
29
56
|
};
|
|
30
57
|
export const copySkillToInstallPaths = async (skillName, installPaths) => {
|
|
31
58
|
const sourceDir = skillDir(skillName);
|
|
@@ -35,9 +62,6 @@ export const copySkillToInstallPaths = async (skillName, installPaths) => {
|
|
|
35
62
|
}
|
|
36
63
|
};
|
|
37
64
|
export const buildTargets = (agent, paths, scope) => {
|
|
38
|
-
if (scope === "system") {
|
|
39
|
-
return (paths.system ?? []).map((pathValue) => ({ agent, scope, path: pathValue }));
|
|
40
|
-
}
|
|
41
65
|
const list = scope === "user" ? paths.user : paths.project;
|
|
42
66
|
return list.map((pathValue) => ({ agent, scope, path: pathValue }));
|
|
43
67
|
};
|