skillbox 0.3.0 → 0.3.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/dist/cli.js +1 -1
- package/dist/commands/add-repo.js +14 -9
- package/dist/commands/add.js +8 -5
- package/dist/commands/list.js +42 -0
- package/dist/lib/installs.js +11 -0
- package/dist/lib/sync.js +34 -10
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { registerRemove } from "./commands/remove.js";
|
|
|
12
12
|
import { registerStatus } from "./commands/status.js";
|
|
13
13
|
import { registerUpdate } from "./commands/update.js";
|
|
14
14
|
const program = new Command();
|
|
15
|
-
program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.3.
|
|
15
|
+
program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.3.2");
|
|
16
16
|
registerAdd(program);
|
|
17
17
|
registerAgent(program);
|
|
18
18
|
registerConfig(program);
|
|
@@ -2,6 +2,7 @@ import { getErrorMessage } from "../lib/command.js";
|
|
|
2
2
|
import { loadConfig } from "../lib/config.js";
|
|
3
3
|
import { parseRepoRef } from "../lib/github.js";
|
|
4
4
|
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
5
|
+
import { recordInstallPaths } from "../lib/installs.js";
|
|
5
6
|
import { printInfo, printJson } from "../lib/output.js";
|
|
6
7
|
import { buildProjectAgentPaths } from "../lib/project-paths.js";
|
|
7
8
|
import { fetchRepoFile, listRepoSkills, normalizeRepoRef, writeRepoSkillDirectory, } from "../lib/repo-skills.js";
|
|
@@ -31,6 +32,7 @@ async function installSkillTargets(skillName, options, installs) {
|
|
|
31
32
|
const projectEntry = await ensureProjectRegistered(projectRoot, scope);
|
|
32
33
|
const paths = buildProjectAgentPaths(projectRoot, projectEntry);
|
|
33
34
|
const config = await loadConfig();
|
|
35
|
+
const recordedPaths = new Set();
|
|
34
36
|
for (const agent of agentList) {
|
|
35
37
|
const map = paths[agent];
|
|
36
38
|
if (!map) {
|
|
@@ -41,17 +43,20 @@ async function installSkillTargets(skillName, options, installs) {
|
|
|
41
43
|
const written = results
|
|
42
44
|
.filter((result) => result.mode !== "skipped")
|
|
43
45
|
.map((result) => result.path);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
+
const warnings = buildSymlinkWarning(agent, results);
|
|
47
|
+
for (const warning of warnings) {
|
|
46
48
|
printInfo(warning);
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const deduped = recordInstallPaths(written, recordedPaths);
|
|
51
|
+
if (deduped.length > 0) {
|
|
52
|
+
for (const target of deduped) {
|
|
53
|
+
installs.push({
|
|
54
|
+
scope,
|
|
55
|
+
agent,
|
|
56
|
+
path: target,
|
|
57
|
+
projectRoot: scope === "project" ? projectRoot : undefined,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
}
|
package/dist/commands/add.js
CHANGED
|
@@ -3,6 +3,7 @@ import { loadConfig } from "../lib/config.js";
|
|
|
3
3
|
import { fetchText } from "../lib/fetcher.js";
|
|
4
4
|
import { collect } from "../lib/fs-utils.js";
|
|
5
5
|
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
6
|
+
import { recordInstallPaths } from "../lib/installs.js";
|
|
6
7
|
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
7
8
|
import { buildProjectAgentPaths } from "../lib/project-paths.js";
|
|
8
9
|
import { ensureProjectRegistered, resolveRuntime } from "../lib/runtime.js";
|
|
@@ -64,6 +65,7 @@ export function registerAdd(program) {
|
|
|
64
65
|
const config = await loadConfig();
|
|
65
66
|
const installed = [];
|
|
66
67
|
const installs = [];
|
|
68
|
+
const recordedPaths = new Set();
|
|
67
69
|
for (const agent of agentList) {
|
|
68
70
|
const map = paths[agent];
|
|
69
71
|
if (!map) {
|
|
@@ -74,13 +76,14 @@ export function registerAdd(program) {
|
|
|
74
76
|
const written = results
|
|
75
77
|
.filter((result) => result.mode !== "skipped")
|
|
76
78
|
.map((result) => result.path);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
+
const warnings = buildSymlinkWarning(agent, results);
|
|
80
|
+
for (const warning of warnings) {
|
|
79
81
|
printInfo(warning);
|
|
80
82
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const deduped = recordInstallPaths(written, recordedPaths);
|
|
84
|
+
if (deduped.length > 0) {
|
|
85
|
+
installed.push({ agent, scope, targets: deduped });
|
|
86
|
+
for (const target of deduped) {
|
|
84
87
|
installs.push({
|
|
85
88
|
scope,
|
|
86
89
|
agent,
|
package/dist/commands/list.js
CHANGED
|
@@ -58,9 +58,28 @@ function groupByScope(skills) {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
if (projectSkills.length > 0) {
|
|
61
|
+
const projectRoots = new Map();
|
|
62
|
+
for (const skill of projectSkills) {
|
|
63
|
+
const roots = getProjectRoots(skill);
|
|
64
|
+
if (roots.length === 0) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const root of roots) {
|
|
68
|
+
const existing = projectRoots.get(root) ?? [];
|
|
69
|
+
existing.push(skill);
|
|
70
|
+
projectRoots.set(root, existing);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const projectGroups = Array.from(projectRoots.entries())
|
|
74
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
75
|
+
.map(([projectRoot, skillsForProject]) => ({
|
|
76
|
+
projectRoot,
|
|
77
|
+
sourceGroups: groupBySourceType(skillsForProject),
|
|
78
|
+
}));
|
|
61
79
|
result.push({
|
|
62
80
|
scope: "project",
|
|
63
81
|
sourceGroups: groupBySourceType(projectSkills),
|
|
82
|
+
projectGroups,
|
|
64
83
|
});
|
|
65
84
|
}
|
|
66
85
|
return result;
|
|
@@ -71,10 +90,33 @@ function groupBySourceType(skills) {
|
|
|
71
90
|
const grouped = groupAndSort(skills, (skill) => skill.source.type, LIST_SOURCE_ORDER, sortByName);
|
|
72
91
|
return grouped.map(({ key, items }) => ({ source: key, skills: items }));
|
|
73
92
|
}
|
|
93
|
+
function getProjectRoots(skill) {
|
|
94
|
+
const roots = (skill.installs ?? [])
|
|
95
|
+
.filter((install) => install.scope === "project")
|
|
96
|
+
.map((install) => install.projectRoot)
|
|
97
|
+
.filter((root) => Boolean(root));
|
|
98
|
+
return Array.from(new Set(roots));
|
|
99
|
+
}
|
|
74
100
|
function printScopeGroup(group) {
|
|
75
101
|
const label = group.scope === "global" ? "Global Skills" : "Project Skills";
|
|
76
102
|
const totalCount = group.sourceGroups.reduce((sum, g) => sum + g.skills.length, 0);
|
|
77
103
|
printInfo(`${label} (${totalCount})`);
|
|
104
|
+
if (group.scope === "project" && group.projectGroups) {
|
|
105
|
+
for (const projectGroup of group.projectGroups) {
|
|
106
|
+
printInfo("");
|
|
107
|
+
printInfo(projectGroup.projectRoot);
|
|
108
|
+
for (const sourceGroup of projectGroup.sourceGroups) {
|
|
109
|
+
printInfo(` ${sourceGroup.source}`);
|
|
110
|
+
for (const skill of sourceGroup.skills) {
|
|
111
|
+
printInfo(` ${skill.name}`);
|
|
112
|
+
if (skill.subcommands.length > 0) {
|
|
113
|
+
printInfo(` → ${skill.subcommands.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
78
120
|
for (const sourceGroup of group.sourceGroups) {
|
|
79
121
|
printInfo("");
|
|
80
122
|
printInfo(`${sourceGroup.source}`);
|
package/dist/lib/installs.js
CHANGED
|
@@ -31,6 +31,17 @@ export function getInstallPaths(skill, projectRoot) {
|
|
|
31
31
|
.filter((install) => install.projectRoot === projectRoot)
|
|
32
32
|
.map((install) => install.path);
|
|
33
33
|
}
|
|
34
|
+
export function recordInstallPaths(paths, recorded) {
|
|
35
|
+
const deduped = [];
|
|
36
|
+
for (const path of paths) {
|
|
37
|
+
if (recorded.has(path)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
recorded.add(path);
|
|
41
|
+
deduped.push(path);
|
|
42
|
+
}
|
|
43
|
+
return deduped;
|
|
44
|
+
}
|
|
34
45
|
export function getProjectInstallPaths(skills, projectRoot) {
|
|
35
46
|
const map = new Map();
|
|
36
47
|
for (const skill of skills) {
|
package/dist/lib/sync.js
CHANGED
|
@@ -17,29 +17,53 @@ async function copyFiles(sourceDir, targetDir) {
|
|
|
17
17
|
await fs.copyFile(sourcePath, destPath);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
async function isSymlinkTo(targetDir, expectedSource) {
|
|
21
|
+
try {
|
|
22
|
+
const stat = await fs.lstat(targetDir);
|
|
23
|
+
if (!stat.isSymbolicLink()) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const linkTarget = await fs.readlink(targetDir);
|
|
27
|
+
return linkTarget === expectedSource;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
20
33
|
async function createSymlink(sourceDir, targetDir) {
|
|
34
|
+
if (await isSymlinkTo(targetDir, sourceDir)) {
|
|
35
|
+
return "exists";
|
|
36
|
+
}
|
|
21
37
|
await fs.symlink(sourceDir, targetDir, "dir");
|
|
38
|
+
return "created";
|
|
22
39
|
}
|
|
23
40
|
export function buildSymlinkWarning(agent, results) {
|
|
24
41
|
const skipped = results.filter((result) => result.mode === "skipped");
|
|
25
42
|
if (skipped.length === 0) {
|
|
26
|
-
return
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const warnings = [];
|
|
46
|
+
for (const result of skipped) {
|
|
47
|
+
const skillName = path.basename(result.path);
|
|
48
|
+
const isExists = result.error?.includes("EEXIST");
|
|
49
|
+
if (isExists) {
|
|
50
|
+
warnings.push(` ⚠ ${skillName} (${agent}): already exists at target, remove manually or use --install-mode copy`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
warnings.push(` ⚠ ${skillName} (${agent}): ${result.error ?? "unknown error"}`);
|
|
54
|
+
}
|
|
27
55
|
}
|
|
28
|
-
|
|
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.`;
|
|
56
|
+
return warnings;
|
|
32
57
|
}
|
|
33
58
|
export async function installSkillToTargets(skillName, targets, config) {
|
|
34
59
|
const sourceDir = skillDir(skillName);
|
|
35
60
|
const results = [];
|
|
36
|
-
for (const
|
|
37
|
-
|
|
38
|
-
await ensureDir(targetRoot);
|
|
61
|
+
for (const targetDir of targets) {
|
|
62
|
+
await ensureDir(path.dirname(targetDir));
|
|
39
63
|
if (config.installMode === "symlink") {
|
|
40
64
|
try {
|
|
41
|
-
await createSymlink(sourceDir, targetDir);
|
|
42
|
-
results.push({ path: targetDir, mode: "symlink" });
|
|
65
|
+
const status = await createSymlink(sourceDir, targetDir);
|
|
66
|
+
results.push({ path: targetDir, mode: status === "exists" ? "symlink" : "symlink" });
|
|
43
67
|
}
|
|
44
68
|
catch (error) {
|
|
45
69
|
const message = getErrorMessage(error, "unknown error");
|