skillbox 0.1.1
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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/cli.js +25 -0
- package/dist/commands/add.js +105 -0
- package/dist/commands/agent.js +33 -0
- package/dist/commands/config.js +55 -0
- package/dist/commands/convert.js +64 -0
- package/dist/commands/import.js +50 -0
- package/dist/commands/list.js +95 -0
- package/dist/commands/meta.js +60 -0
- package/dist/commands/project.js +174 -0
- package/dist/commands/status.js +129 -0
- package/dist/commands/update.js +84 -0
- package/dist/lib/agents.js +44 -0
- package/dist/lib/command.js +9 -0
- package/dist/lib/config.js +27 -0
- package/dist/lib/fetcher.js +8 -0
- package/dist/lib/grouping.js +34 -0
- package/dist/lib/index.js +42 -0
- package/dist/lib/installs.js +36 -0
- package/dist/lib/options.js +21 -0
- package/dist/lib/output.js +26 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/project-paths.js +18 -0
- package/dist/lib/project-root.js +25 -0
- package/dist/lib/projects.js +31 -0
- package/dist/lib/runtime.js +24 -0
- package/dist/lib/skill-parser.js +61 -0
- package/dist/lib/skill-store.js +28 -0
- package/dist/lib/sync.js +43 -0
- package/dist/lib/types.js +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
3
|
+
import { loadProjects, saveProjects, upsertProject } from "../lib/projects.js";
|
|
4
|
+
import { loadIndex } from "../lib/index.js";
|
|
5
|
+
import { copySkillToInstallPaths } from "../lib/sync.js";
|
|
6
|
+
import { collectProjectSkills, getProjectInstallPaths, getProjectSkills } from "../lib/installs.js";
|
|
7
|
+
import { handleCommandError } from "../lib/command.js";
|
|
8
|
+
const collect = (value, previous = []) => {
|
|
9
|
+
return [...previous, value];
|
|
10
|
+
};
|
|
11
|
+
const parseAgentPaths = (entries) => {
|
|
12
|
+
const overrides = {};
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
const [agent, pathValue] = entry.split("=");
|
|
15
|
+
if (!agent || !pathValue) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const existing = overrides[agent] ?? [];
|
|
19
|
+
overrides[agent] = [...existing, pathValue];
|
|
20
|
+
}
|
|
21
|
+
return overrides;
|
|
22
|
+
};
|
|
23
|
+
export const registerProject = (program) => {
|
|
24
|
+
const project = program.command("project").description("Manage projects");
|
|
25
|
+
project
|
|
26
|
+
.command("add")
|
|
27
|
+
.argument("<path>", "Project path")
|
|
28
|
+
.option("--agent-path <agentPath>", "Agent path override (agent=path)", collect)
|
|
29
|
+
.option("--json", "JSON output")
|
|
30
|
+
.action(async (inputPath, options) => {
|
|
31
|
+
try {
|
|
32
|
+
const resolved = path.resolve(inputPath);
|
|
33
|
+
const index = await loadProjects();
|
|
34
|
+
const updated = upsertProject(index, resolved);
|
|
35
|
+
const overrides = parseAgentPaths(options.agentPath ?? []);
|
|
36
|
+
const merged = {
|
|
37
|
+
...updated,
|
|
38
|
+
projects: updated.projects.map((project) => {
|
|
39
|
+
if (project.root !== resolved) {
|
|
40
|
+
return project;
|
|
41
|
+
}
|
|
42
|
+
const nextPaths = project.agentPaths ? { ...project.agentPaths } : {};
|
|
43
|
+
for (const [agent, paths] of Object.entries(overrides)) {
|
|
44
|
+
nextPaths[agent] = paths;
|
|
45
|
+
}
|
|
46
|
+
return { ...project, agentPaths: nextPaths };
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
await saveProjects(merged);
|
|
50
|
+
if (isJsonEnabled(options)) {
|
|
51
|
+
printJson({
|
|
52
|
+
ok: true,
|
|
53
|
+
command: "project add",
|
|
54
|
+
data: {
|
|
55
|
+
path: resolved,
|
|
56
|
+
agentPaths: merged.projects.find((project) => project.root === resolved)?.agentPaths ?? {},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
printInfo(`Project registered: ${resolved}`);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
handleCommandError(options, "project add", error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
project
|
|
68
|
+
.command("list")
|
|
69
|
+
.option("--json", "JSON output")
|
|
70
|
+
.action(async (options) => {
|
|
71
|
+
const projectsIndex = await loadProjects();
|
|
72
|
+
const skillIndex = await loadIndex();
|
|
73
|
+
const projectSkills = collectProjectSkills(skillIndex.skills);
|
|
74
|
+
const projects = projectsIndex.projects.map((project) => ({
|
|
75
|
+
...project,
|
|
76
|
+
skills: projectSkills.get(project.root) ?? [],
|
|
77
|
+
}));
|
|
78
|
+
if (isJsonEnabled(options)) {
|
|
79
|
+
printJson({
|
|
80
|
+
ok: true,
|
|
81
|
+
command: "project list",
|
|
82
|
+
data: {
|
|
83
|
+
projects,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
printInfo(`Projects: ${projects.length}`);
|
|
89
|
+
for (const entry of projects) {
|
|
90
|
+
const skills = entry.skills ?? [];
|
|
91
|
+
const label = skills.length > 0 ? ` (${skills.length} skills)` : "";
|
|
92
|
+
printInfo(`- ${entry.root}${label}`);
|
|
93
|
+
for (const skillName of skills) {
|
|
94
|
+
printInfo(` - ${skillName}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
project
|
|
99
|
+
.command("inspect")
|
|
100
|
+
.argument("<path>", "Project path")
|
|
101
|
+
.option("--json", "JSON output")
|
|
102
|
+
.action(async (inputPath, options) => {
|
|
103
|
+
const projectsIndex = await loadProjects();
|
|
104
|
+
const skillIndex = await loadIndex();
|
|
105
|
+
const resolved = path.resolve(inputPath);
|
|
106
|
+
const project = projectsIndex.projects.find((entry) => entry.root === resolved);
|
|
107
|
+
if (!project) {
|
|
108
|
+
handleCommandError(options, "project inspect", new Error(`Project not registered: ${resolved}`));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const skills = getProjectSkills(skillIndex.skills, resolved);
|
|
112
|
+
const data = {
|
|
113
|
+
root: project.root,
|
|
114
|
+
agentPaths: project.agentPaths ?? {},
|
|
115
|
+
skills,
|
|
116
|
+
};
|
|
117
|
+
if (isJsonEnabled(options)) {
|
|
118
|
+
printJson({
|
|
119
|
+
ok: true,
|
|
120
|
+
command: "project inspect",
|
|
121
|
+
data,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
printInfo(`Project: ${data.root}`);
|
|
126
|
+
const agentEntries = Object.entries(data.agentPaths);
|
|
127
|
+
if (agentEntries.length === 0) {
|
|
128
|
+
printInfo("Agent paths: default");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
printInfo("Agent paths:");
|
|
132
|
+
for (const [agent, paths] of agentEntries) {
|
|
133
|
+
printInfo(`- ${agent}: ${paths.join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (data.skills.length === 0) {
|
|
137
|
+
printInfo("Skills: none");
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
printInfo("Skills:");
|
|
141
|
+
for (const skillName of data.skills) {
|
|
142
|
+
printInfo(`- ${skillName}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
project
|
|
147
|
+
.command("sync")
|
|
148
|
+
.argument("<path>", "Project path")
|
|
149
|
+
.option("--json", "JSON output")
|
|
150
|
+
.action(async (inputPath, options) => {
|
|
151
|
+
const skillIndex = await loadIndex();
|
|
152
|
+
const resolved = path.resolve(inputPath);
|
|
153
|
+
const installPaths = getProjectInstallPaths(skillIndex.skills, resolved);
|
|
154
|
+
if (installPaths.size === 0) {
|
|
155
|
+
handleCommandError(options, "project sync", new Error(`No skills recorded for project: ${resolved}`));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const [skillName, paths] of installPaths.entries()) {
|
|
159
|
+
await copySkillToInstallPaths(skillName, paths);
|
|
160
|
+
}
|
|
161
|
+
if (isJsonEnabled(options)) {
|
|
162
|
+
printJson({
|
|
163
|
+
ok: true,
|
|
164
|
+
command: "project sync",
|
|
165
|
+
data: {
|
|
166
|
+
root: resolved,
|
|
167
|
+
skills: Array.from(installPaths.keys()).sort(),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
printInfo(`Synced ${installPaths.size} skill(s) for ${resolved}`);
|
|
173
|
+
});
|
|
174
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isJsonEnabled, printInfo, printJson, printList } from "../lib/output.js";
|
|
2
|
+
import { loadIndex, saveIndex } from "../lib/index.js";
|
|
3
|
+
import { fetchText } from "../lib/fetcher.js";
|
|
4
|
+
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) => {
|
|
9
|
+
program
|
|
10
|
+
.command("status")
|
|
11
|
+
.option("--group <group>", "Group by project or source")
|
|
12
|
+
.option("--json", "JSON output")
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
try {
|
|
15
|
+
const index = await loadIndex();
|
|
16
|
+
const config = await loadConfig();
|
|
17
|
+
const results = [];
|
|
18
|
+
for (const skill of index.skills) {
|
|
19
|
+
const projects = (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;
|
|
45
|
+
}
|
|
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
|
+
}
|
|
60
|
+
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);
|
|
65
|
+
if (isJsonEnabled(options)) {
|
|
66
|
+
printJson({
|
|
67
|
+
ok: true,
|
|
68
|
+
command: "status",
|
|
69
|
+
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,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
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;
|
|
92
|
+
}
|
|
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;
|
|
105
|
+
}
|
|
106
|
+
printList("Outdated", outdated);
|
|
107
|
+
printList("Up to date", upToDate);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
handleCommandError(options, "status", error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
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
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
import path from "node:path";
|
|
8
|
+
import { loadConfig } from "../lib/config.js";
|
|
9
|
+
import { handleCommandError } from "../lib/command.js";
|
|
10
|
+
export const registerUpdate = (program) => {
|
|
11
|
+
program
|
|
12
|
+
.command("update")
|
|
13
|
+
.argument("[name]", "Skill name")
|
|
14
|
+
.option("--system", "Allow system-scope updates")
|
|
15
|
+
.option("--project <path>", "Only update installs for a project")
|
|
16
|
+
.option("--json", "JSON output")
|
|
17
|
+
.action(async (name, options) => {
|
|
18
|
+
try {
|
|
19
|
+
const index = await loadIndex();
|
|
20
|
+
const config = await loadConfig();
|
|
21
|
+
const targets = name ? index.skills.filter((skill) => skill.name === name) : index.skills;
|
|
22
|
+
if (name && targets.length === 0) {
|
|
23
|
+
throw new Error(`Skill not found: ${name}`);
|
|
24
|
+
}
|
|
25
|
+
const updated = [];
|
|
26
|
+
await ensureSkillsDir();
|
|
27
|
+
const projectRoot = options.project ? path.resolve(options.project) : null;
|
|
28
|
+
for (const skill of targets) {
|
|
29
|
+
if (skill.source.type !== "url" || !skill.source.url) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
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.`);
|
|
36
|
+
}
|
|
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);
|
|
46
|
+
}
|
|
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
|
+
}
|
|
57
|
+
await saveIndex(sortIndex(index));
|
|
58
|
+
if (isJsonEnabled(options)) {
|
|
59
|
+
printJson({
|
|
60
|
+
ok: true,
|
|
61
|
+
command: "update",
|
|
62
|
+
data: {
|
|
63
|
+
name: name ?? null,
|
|
64
|
+
system: Boolean(options.system),
|
|
65
|
+
project: projectRoot,
|
|
66
|
+
updated,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (updated.length === 0) {
|
|
72
|
+
printInfo("No skills updated.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
printInfo(`Updated ${updated.length} skill(s):`);
|
|
76
|
+
for (const skillName of updated) {
|
|
77
|
+
printInfo(`- ${skillName}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
handleCommandError(options, "update", error);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
const home = os.homedir();
|
|
4
|
+
export const agentPaths = (projectRoot) => ({
|
|
5
|
+
opencode: {
|
|
6
|
+
project: [
|
|
7
|
+
path.join(projectRoot, ".opencode", "skills"),
|
|
8
|
+
path.join(projectRoot, ".claude", "skills"),
|
|
9
|
+
],
|
|
10
|
+
user: [path.join(home, ".config", "opencode", "skills"), path.join(home, ".claude", "skills")],
|
|
11
|
+
},
|
|
12
|
+
claude: {
|
|
13
|
+
project: [path.join(projectRoot, ".claude", "skills")],
|
|
14
|
+
user: [path.join(home, ".claude", "skills")],
|
|
15
|
+
},
|
|
16
|
+
cursor: {
|
|
17
|
+
project: [
|
|
18
|
+
path.join(projectRoot, ".cursor", "skills"),
|
|
19
|
+
path.join(projectRoot, ".claude", "skills"),
|
|
20
|
+
],
|
|
21
|
+
user: [path.join(home, ".cursor", "skills"), path.join(home, ".claude", "skills")],
|
|
22
|
+
},
|
|
23
|
+
codex: {
|
|
24
|
+
project: [path.join(projectRoot, ".codex", "skills")],
|
|
25
|
+
user: [path.join(home, ".codex", "skills")],
|
|
26
|
+
system: [path.join(path.sep, "etc", "codex", "skills")],
|
|
27
|
+
},
|
|
28
|
+
amp: {
|
|
29
|
+
project: [
|
|
30
|
+
path.join(projectRoot, ".agents", "skills"),
|
|
31
|
+
path.join(projectRoot, ".claude", "skills"),
|
|
32
|
+
],
|
|
33
|
+
user: [path.join(home, ".config", "agents", "skills"), path.join(home, ".claude", "skills")],
|
|
34
|
+
},
|
|
35
|
+
antigravity: {
|
|
36
|
+
project: [path.join(projectRoot, ".agent", "skills")],
|
|
37
|
+
user: [path.join(home, ".gemini", "antigravity", "skills")],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
export const allAgents = ["opencode", "claude", "cursor", "codex", "amp", "antigravity"];
|
|
41
|
+
const agentSet = new Set(allAgents);
|
|
42
|
+
export const isAgentId = (value) => {
|
|
43
|
+
return agentSet.has(value);
|
|
44
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isJsonEnabled, printError, printJson } from "./output.js";
|
|
2
|
+
export const handleCommandError = (options, command, error) => {
|
|
3
|
+
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
4
|
+
if (isJsonEnabled(options)) {
|
|
5
|
+
printJson({ ok: false, command, error: { message } });
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
printError(message);
|
|
9
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { skillboxRoot } from "./paths.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const defaultConfig = () => ({
|
|
5
|
+
version: 1,
|
|
6
|
+
defaultAgents: [],
|
|
7
|
+
defaultScope: "project",
|
|
8
|
+
manageSystem: false,
|
|
9
|
+
});
|
|
10
|
+
export const configPath = () => path.join(skillboxRoot(), "config.json");
|
|
11
|
+
export const loadConfig = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const content = await fs.readFile(configPath(), "utf8");
|
|
14
|
+
return JSON.parse(content);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code === "ENOENT") {
|
|
18
|
+
return defaultConfig();
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const saveConfig = async (config) => {
|
|
24
|
+
await fs.mkdir(skillboxRoot(), { recursive: true });
|
|
25
|
+
const json = JSON.stringify(config, null, 2);
|
|
26
|
+
await fs.writeFile(configPath(), `${json}\n`, "utf8");
|
|
27
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const groupNamesByKey = (items, nameOf, keysOf) => {
|
|
2
|
+
const map = new Map();
|
|
3
|
+
for (const item of items) {
|
|
4
|
+
const name = nameOf(item);
|
|
5
|
+
for (const key of keysOf(item)) {
|
|
6
|
+
const existing = map.get(key) ?? [];
|
|
7
|
+
if (!existing.includes(name)) {
|
|
8
|
+
existing.push(name);
|
|
9
|
+
map.set(key, existing);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return Array.from(map.entries())
|
|
14
|
+
.map(([key, skills]) => ({ key, skills: skills.sort() }))
|
|
15
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
16
|
+
};
|
|
17
|
+
export const groupStatusByKey = (items, nameOf, outdatedOf, keysOf) => {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
const name = nameOf(item);
|
|
21
|
+
const outdated = outdatedOf(item);
|
|
22
|
+
for (const key of keysOf(item)) {
|
|
23
|
+
const entry = map.get(key) ?? { key, outdated: [], upToDate: [] };
|
|
24
|
+
if (outdated) {
|
|
25
|
+
entry.outdated.push(name);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
entry.upToDate.push(name);
|
|
29
|
+
}
|
|
30
|
+
map.set(key, entry);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Array.from(map.values()).sort((a, b) => a.key.localeCompare(b.key));
|
|
34
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { skillboxIndexPath, skillboxRoot } from "./paths.js";
|
|
3
|
+
const emptyIndex = () => ({ version: 1, skills: [] });
|
|
4
|
+
export const loadIndex = async () => {
|
|
5
|
+
const filePath = skillboxIndexPath();
|
|
6
|
+
try {
|
|
7
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
8
|
+
return JSON.parse(content);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.code === "ENOENT") {
|
|
12
|
+
return emptyIndex();
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export const saveIndex = async (index) => {
|
|
18
|
+
await fs.mkdir(skillboxRoot(), { recursive: true });
|
|
19
|
+
const filePath = skillboxIndexPath();
|
|
20
|
+
const json = JSON.stringify(index, null, 2);
|
|
21
|
+
await fs.writeFile(filePath, `${json}\n`, "utf8");
|
|
22
|
+
};
|
|
23
|
+
export const upsertSkill = (index, skill) => {
|
|
24
|
+
const next = { ...index, skills: [...index.skills] };
|
|
25
|
+
const existingIndex = next.skills.findIndex((item) => item.name === skill.name);
|
|
26
|
+
if (existingIndex === -1) {
|
|
27
|
+
next.skills.push(skill);
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
next.skills[existingIndex] = { ...next.skills[existingIndex], ...skill };
|
|
31
|
+
return next;
|
|
32
|
+
};
|
|
33
|
+
export const sortIndex = (index) => {
|
|
34
|
+
const skills = [...index.skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
35
|
+
return { ...index, skills };
|
|
36
|
+
};
|
|
37
|
+
export const ensureIndexSchema = (index) => {
|
|
38
|
+
if (!index.version) {
|
|
39
|
+
return { version: 1, skills: index.skills ?? [] };
|
|
40
|
+
}
|
|
41
|
+
return index;
|
|
42
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const projectInstalls = (install) => {
|
|
2
|
+
return install.scope === "project" && Boolean(install.projectRoot);
|
|
3
|
+
};
|
|
4
|
+
export const collectProjectSkills = (skills) => {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
for (const skill of skills) {
|
|
7
|
+
for (const install of skill.installs ?? []) {
|
|
8
|
+
if (!projectInstalls(install)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const existing = map.get(install.projectRoot) ?? [];
|
|
12
|
+
if (!existing.includes(skill.name)) {
|
|
13
|
+
existing.push(skill.name);
|
|
14
|
+
map.set(install.projectRoot, existing);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
};
|
|
20
|
+
export const getProjectSkills = (skills, projectRoot) => {
|
|
21
|
+
const map = collectProjectSkills(skills);
|
|
22
|
+
return (map.get(projectRoot) ?? []).sort();
|
|
23
|
+
};
|
|
24
|
+
export const getProjectInstallPaths = (skills, projectRoot) => {
|
|
25
|
+
const map = new Map();
|
|
26
|
+
for (const skill of skills) {
|
|
27
|
+
const paths = (skill.installs ?? [])
|
|
28
|
+
.filter(projectInstalls)
|
|
29
|
+
.filter((install) => install.projectRoot === projectRoot)
|
|
30
|
+
.map((install) => install.path);
|
|
31
|
+
if (paths.length > 0) {
|
|
32
|
+
map.set(skill.name, paths);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return map;
|
|
36
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { allAgents, isAgentId } from "./agents.js";
|
|
2
|
+
export const parseAgentList = (value) => {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return value
|
|
7
|
+
.split(",")
|
|
8
|
+
.map((agent) => agent.trim())
|
|
9
|
+
.filter((agent) => agent.length > 0)
|
|
10
|
+
.filter(isAgentId);
|
|
11
|
+
};
|
|
12
|
+
export const resolveAgentList = (override, config) => {
|
|
13
|
+
const parsed = parseAgentList(override);
|
|
14
|
+
if (parsed.length > 0) {
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
if (config.defaultAgents.length > 0) {
|
|
18
|
+
return config.defaultAgents.filter(isAgentId);
|
|
19
|
+
}
|
|
20
|
+
return allAgents;
|
|
21
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export const isJsonEnabled = (options) => Boolean(options.json);
|
|
3
|
+
export const printJson = (result) => {
|
|
4
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
5
|
+
};
|
|
6
|
+
export const printInfo = (message) => {
|
|
7
|
+
process.stdout.write(`${message}\n`);
|
|
8
|
+
};
|
|
9
|
+
export const printError = (message) => {
|
|
10
|
+
process.stderr.write(`${chalk.red(message)}\n`);
|
|
11
|
+
};
|
|
12
|
+
export const printList = (label, items, indent = "- ") => {
|
|
13
|
+
printInfo(`${label}: ${items.length}`);
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
printInfo(`${indent}${item}`);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const printGroupList = (label, groups, itemPrefix = " - ") => {
|
|
19
|
+
printInfo(`${label}: ${groups.length}`);
|
|
20
|
+
for (const group of groups) {
|
|
21
|
+
printInfo(`- ${group.key}`);
|
|
22
|
+
for (const item of group.items) {
|
|
23
|
+
printInfo(`${itemPrefix}${item}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const skillboxRoot = () => path.join(os.homedir(), ".config", "skillbox");
|
|
4
|
+
export const skillboxSkillsDir = () => path.join(skillboxRoot(), "skills");
|
|
5
|
+
export const skillboxIndexPath = () => path.join(skillboxRoot(), "index.json");
|
|
6
|
+
export const skillboxProjectsPath = () => path.join(skillboxRoot(), "projects.json");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { agentPaths } from "./agents.js";
|
|
2
|
+
export const buildProjectAgentPaths = (projectRoot, project) => {
|
|
3
|
+
const defaults = agentPaths(projectRoot);
|
|
4
|
+
if (!project?.agentPaths) {
|
|
5
|
+
return defaults;
|
|
6
|
+
}
|
|
7
|
+
const merged = { ...defaults };
|
|
8
|
+
for (const [agent, paths] of Object.entries(project.agentPaths)) {
|
|
9
|
+
if (!paths) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
merged[agent] = {
|
|
13
|
+
...defaults[agent],
|
|
14
|
+
project: paths,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return merged;
|
|
18
|
+
};
|