@ecology91/skills 0.1.6 → 0.1.7
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/bin/install-summary.mjs +74 -0
- package/bin/install-summary.test.mjs +31 -0
- package/bin/install.mjs +79 -22
- package/bin/prompt-skills.mjs +1 -2
- package/bin/resolve-selection.mjs +12 -1
- package/bin/resolve-selection.test.mjs +6 -0
- package/package.json +4 -4
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const home = os.homedir();
|
|
5
|
+
|
|
6
|
+
/** @param {string} targetPath */
|
|
7
|
+
export function shortenPath(targetPath) {
|
|
8
|
+
const normalized = path.resolve(targetPath);
|
|
9
|
+
if (normalized.startsWith(`${home}${path.sep}`)) {
|
|
10
|
+
return `~${path.sep}${path.relative(home, normalized)}`;
|
|
11
|
+
}
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {string} bucketId */
|
|
16
|
+
export function bucketTitle(bucketId) {
|
|
17
|
+
return bucketId
|
|
18
|
+
.split("-")
|
|
19
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
20
|
+
.join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @param {string[]} paths */
|
|
24
|
+
export function dedupePaths(paths) {
|
|
25
|
+
return [...new Set(paths)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Map<string, { name: string, dest: string, src?: string }[]>} grouped
|
|
30
|
+
* @param {{ mode: "copy" | "link", dryRun: boolean }} options
|
|
31
|
+
*/
|
|
32
|
+
export function formatInstallSummary(grouped, { mode, dryRun }) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
const actionLabel = dryRun ? (mode === "link" ? "would link" : "would install") : mode;
|
|
35
|
+
|
|
36
|
+
for (const [bucket, bucketSkills] of grouped) {
|
|
37
|
+
lines.push("");
|
|
38
|
+
lines.push(bucketTitle(bucket));
|
|
39
|
+
|
|
40
|
+
for (const skill of bucketSkills) {
|
|
41
|
+
const shortDest = shortenPath(skill.dest);
|
|
42
|
+
|
|
43
|
+
if (mode === "copy") {
|
|
44
|
+
const marker = dryRun ? actionLabel : "copied";
|
|
45
|
+
lines.push(`✓ ${skill.name} (${marker})`);
|
|
46
|
+
lines.push(` → ${shortDest}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
lines.push(`✓ ${skill.name} (${actionLabel})`);
|
|
52
|
+
lines.push(` → ${shortDest}`);
|
|
53
|
+
if (skill.src) {
|
|
54
|
+
lines.push(` → ${shortenPath(skill.src)}`);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
lines.push(`✓ ${skill.name}`);
|
|
60
|
+
lines.push(` → ${shortDest}`);
|
|
61
|
+
if (skill.src) {
|
|
62
|
+
lines.push(` → ${shortenPath(skill.src)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const skillCount = [...grouped.values()].reduce((total, skills) => total + skills.length, 0);
|
|
68
|
+
const verb = dryRun ? "Checked" : mode === "link" ? "Linked" : "Installed";
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
title: `${verb} ${skillCount} skill${skillCount === 1 ? "" : "s"}`,
|
|
72
|
+
lines: lines.join("\n").trimStart(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { dedupePaths, formatInstallSummary, shortenPath } from "./install-summary.mjs";
|
|
6
|
+
|
|
7
|
+
test("dedupePaths removes duplicate install destinations", () => {
|
|
8
|
+
assert.deepEqual(
|
|
9
|
+
dedupePaths(["~/.agents/skills/tdd", "~/.agents/skills/tdd", "~/.cursor/skills/tdd"]),
|
|
10
|
+
["~/.agents/skills/tdd", "~/.cursor/skills/tdd"],
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("formatInstallSummary shows one destination line per skill", () => {
|
|
15
|
+
const dest = path.join(os.homedir(), ".agents", "skills", "diagnose");
|
|
16
|
+
const grouped = new Map([
|
|
17
|
+
[
|
|
18
|
+
"engineering",
|
|
19
|
+
[{ name: "diagnose", dest }],
|
|
20
|
+
],
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const summary = formatInstallSummary(grouped, { mode: "copy", dryRun: false });
|
|
24
|
+
assert.equal(summary.title, "Installed 1 skill");
|
|
25
|
+
assert.equal(summary.lines.split("\n").filter((line) => line.startsWith(" →")).length, 1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("shortenPath replaces home prefix with tilde", () => {
|
|
29
|
+
const dest = path.join(os.homedir(), ".agents", "skills", "tdd");
|
|
30
|
+
assert.equal(shortenPath(dest), "~/.agents/skills/tdd");
|
|
31
|
+
});
|
package/bin/install.mjs
CHANGED
|
@@ -3,12 +3,14 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { note, outro, spinner } from "@clack/prompts";
|
|
6
7
|
import {
|
|
7
8
|
collectSkillsByBucket,
|
|
8
9
|
countByBucket,
|
|
9
10
|
flattenCatalog,
|
|
10
11
|
formatBucketCounts,
|
|
11
12
|
} from "./collect-skills.mjs";
|
|
13
|
+
import { formatInstallSummary } from "./install-summary.mjs";
|
|
12
14
|
import { promptSkillSelection } from "./prompt-skills.mjs";
|
|
13
15
|
import { parseArgs, resolveSelection, skillsFromNames } from "./resolve-selection.mjs";
|
|
14
16
|
|
|
@@ -54,6 +56,7 @@ const isGitCheckout = fs.existsSync(path.join(root, ".git"));
|
|
|
54
56
|
const link = flags.has("--link") || (isGitCheckout && !flags.has("--copy"));
|
|
55
57
|
const isInteractive =
|
|
56
58
|
Boolean(process.stdin.isTTY && process.stdout.isTTY) && !flags.has("--all");
|
|
59
|
+
const useClackSummary = isInteractive;
|
|
57
60
|
|
|
58
61
|
const catalog = collectSkillsByBucket(root, destRoot);
|
|
59
62
|
const allSkills = flattenCatalog(catalog);
|
|
@@ -97,39 +100,54 @@ function installCopy(skills) {
|
|
|
97
100
|
|
|
98
101
|
const grouped = groupSkillsByBucket(skills);
|
|
99
102
|
for (const [bucket, bucketSkills] of grouped) {
|
|
100
|
-
|
|
103
|
+
if (!useClackSummary) {
|
|
104
|
+
console.log(`${bucket} (${bucketSkills.length})`);
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
for (const skill of bucketSkills) {
|
|
102
108
|
if (dryRun) {
|
|
103
|
-
|
|
109
|
+
if (!useClackSummary) {
|
|
110
|
+
console.log(` would install ${skill.name} -> ${skill.dest}`);
|
|
111
|
+
}
|
|
104
112
|
continue;
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
fs.rmSync(skill.dest, { recursive: true, force: true });
|
|
108
116
|
fs.cpSync(skill.src, skill.dest, { recursive: true, force: true });
|
|
109
|
-
|
|
117
|
+
if (!useClackSummary) {
|
|
118
|
+
console.log(` installed ${skill.name} -> ${skill.dest}`);
|
|
119
|
+
}
|
|
110
120
|
}
|
|
111
121
|
}
|
|
122
|
+
|
|
123
|
+
return grouped;
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
/** @param {{ bucket: string, name: string, src: string, dest: string }[]} skills */
|
|
115
127
|
function installLink(skills) {
|
|
128
|
+
const grouped = groupSkillsByBucket(skills);
|
|
129
|
+
|
|
116
130
|
if (dryRun) {
|
|
117
|
-
const grouped = groupSkillsByBucket(skills);
|
|
118
131
|
for (const [bucket, bucketSkills] of grouped) {
|
|
119
|
-
|
|
132
|
+
if (!useClackSummary) {
|
|
133
|
+
console.log(`${bucket} (${bucketSkills.length})`);
|
|
134
|
+
}
|
|
120
135
|
for (const skill of bucketSkills) {
|
|
121
|
-
|
|
136
|
+
if (!useClackSummary) {
|
|
137
|
+
console.log(` would link ${skill.name} -> ${skill.dest} (${skill.src})`);
|
|
138
|
+
}
|
|
122
139
|
}
|
|
123
140
|
}
|
|
124
|
-
return;
|
|
141
|
+
return grouped;
|
|
125
142
|
}
|
|
126
143
|
|
|
127
144
|
assertSafeDest();
|
|
128
145
|
fs.mkdirSync(destRoot, { recursive: true });
|
|
129
146
|
|
|
130
|
-
const grouped = groupSkillsByBucket(skills);
|
|
131
147
|
for (const [bucket, bucketSkills] of grouped) {
|
|
132
|
-
|
|
148
|
+
if (!useClackSummary) {
|
|
149
|
+
console.log(`${bucket} (${bucketSkills.length})`);
|
|
150
|
+
}
|
|
133
151
|
for (const skill of bucketSkills) {
|
|
134
152
|
const stat = fs.lstatSync(skill.dest, { throwIfNoEntry: false });
|
|
135
153
|
if (stat) {
|
|
@@ -137,9 +155,43 @@ function installLink(skills) {
|
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
fs.symlinkSync(skill.src, skill.dest);
|
|
140
|
-
|
|
158
|
+
if (!useClackSummary) {
|
|
159
|
+
console.log(` linked ${skill.name} -> ${skill.src}`);
|
|
160
|
+
}
|
|
141
161
|
}
|
|
142
162
|
}
|
|
163
|
+
|
|
164
|
+
return grouped;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function printPlainSummary(selectedSkills, mode) {
|
|
168
|
+
const verb = dryRun ? "Checked" : mode === "link" ? "Linked" : "Installed";
|
|
169
|
+
console.log(
|
|
170
|
+
`${verb} ${selectedSkills.length} skills to ~/.agents/skills (${formatBucketCounts(countByBucket(selectedSkills))}).`,
|
|
171
|
+
);
|
|
172
|
+
console.log("Restart your agent (opencode, Cursor, etc.) to reload the skill list.");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function printClackSummary(grouped, mode) {
|
|
176
|
+
const summarySkills = new Map();
|
|
177
|
+
for (const [bucket, bucketSkills] of grouped) {
|
|
178
|
+
summarySkills.set(
|
|
179
|
+
bucket,
|
|
180
|
+
bucketSkills.map((skill) => ({
|
|
181
|
+
name: skill.name,
|
|
182
|
+
dest: skill.dest,
|
|
183
|
+
src: mode === "link" ? skill.src : undefined,
|
|
184
|
+
})),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { title, lines } = formatInstallSummary(summarySkills, {
|
|
189
|
+
mode: mode === "link" ? "link" : "copy",
|
|
190
|
+
dryRun,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
note(lines, title);
|
|
194
|
+
outro("Restart your agent (opencode, Cursor, etc.) to reload the skill list.");
|
|
143
195
|
}
|
|
144
196
|
|
|
145
197
|
let selectedSkills;
|
|
@@ -163,18 +215,23 @@ if (selectedSkills.length === 0) {
|
|
|
163
215
|
process.exit(0);
|
|
164
216
|
}
|
|
165
217
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
218
|
+
const installMode = link ? "link" : "copy";
|
|
219
|
+
const s = spinner();
|
|
220
|
+
|
|
221
|
+
if (useClackSummary) {
|
|
222
|
+
s.start("Installing skills...");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let grouped;
|
|
226
|
+
if (installMode === "link") {
|
|
227
|
+
grouped = installLink(selectedSkills);
|
|
172
228
|
} else {
|
|
173
|
-
installCopy(selectedSkills);
|
|
174
|
-
const verb = dryRun ? "Checked" : "Installed";
|
|
175
|
-
console.log(
|
|
176
|
-
`${verb} ${selectedSkills.length} skills to ~/.agents/skills (${formatBucketCounts(countByBucket(selectedSkills))}).`,
|
|
177
|
-
);
|
|
229
|
+
grouped = installCopy(selectedSkills);
|
|
178
230
|
}
|
|
179
231
|
|
|
180
|
-
|
|
232
|
+
if (useClackSummary) {
|
|
233
|
+
s.stop("Installation complete");
|
|
234
|
+
printClackSummary(grouped, installMode);
|
|
235
|
+
} else {
|
|
236
|
+
printPlainSummary(selectedSkills, installMode);
|
|
237
|
+
}
|
package/bin/prompt-skills.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cancel, groupMultiselect, intro, isCancel
|
|
1
|
+
import { cancel, groupMultiselect, intro, isCancel } from "@clack/prompts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @param {{ id: string, label: string, skills: { name: string, description: string }[] }[]} catalog
|
|
@@ -34,6 +34,5 @@ export async function promptSkillSelection(catalog) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
outro(`Selected ${selected.length} skill${selected.length === 1 ? "" : "s"}.`);
|
|
38
37
|
return selected;
|
|
39
38
|
}
|
|
@@ -103,5 +103,16 @@ function filterByFlags(allSkills, values, allNames) {
|
|
|
103
103
|
export function skillsFromNames(selectedNames, allSkills) {
|
|
104
104
|
if (!selectedNames || selectedNames.length === 0) return [];
|
|
105
105
|
const byName = new Map(allSkills.map((skill) => [skill.name, skill]));
|
|
106
|
-
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
/** @type {typeof allSkills} */
|
|
108
|
+
const selected = [];
|
|
109
|
+
|
|
110
|
+
for (const name of selectedNames) {
|
|
111
|
+
if (seen.has(name)) continue;
|
|
112
|
+
seen.add(name);
|
|
113
|
+
const skill = byName.get(name);
|
|
114
|
+
if (skill) selected.push(skill);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return selected;
|
|
107
118
|
}
|
|
@@ -58,3 +58,9 @@ test("skillsFromNames preserves prompt order", () => {
|
|
|
58
58
|
const selected = skillsFromNames(["caveman", "tdd"], allSkills);
|
|
59
59
|
assert.deepEqual(selected.map((skill) => skill.name), ["caveman", "tdd"]);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
test("skillsFromNames deduplicates repeated selections", () => {
|
|
63
|
+
const allSkills = catalog.flatMap((bucket) => bucket.skills);
|
|
64
|
+
const selected = skillsFromNames(["tdd", "tdd", "tdd"], allSkills);
|
|
65
|
+
assert.deepEqual(selected.map((skill) => skill.name), ["tdd"]);
|
|
66
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecology91/skills",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "opencode agent skills for real engineering workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"typecheck": "node --check bin/install.mjs && node --check bin/collect-skills.mjs && node --check bin/prompt-skills.mjs && node --check bin/resolve-selection.mjs && node --check bin/resolve-selection.test.mjs && node --check scripts/generate-marketplace.mjs && node --check scripts/verify-marketplace.mjs && node --check scripts/verify-package-files.mjs",
|
|
45
|
-
"test": "node --test bin/resolve-selection.test.mjs && node bin/install.mjs --dry-run --copy --all",
|
|
46
|
-
"test:unit": "node --test bin/resolve-selection.test.mjs",
|
|
44
|
+
"typecheck": "node --check bin/install.mjs && node --check bin/collect-skills.mjs && node --check bin/install-summary.mjs && node --check bin/prompt-skills.mjs && node --check bin/resolve-selection.mjs && node --check bin/resolve-selection.test.mjs && node --check bin/install-summary.test.mjs && node --check scripts/generate-marketplace.mjs && node --check scripts/verify-marketplace.mjs && node --check scripts/verify-package-files.mjs",
|
|
45
|
+
"test": "node --test bin/resolve-selection.test.mjs && node --test bin/install-summary.test.mjs && node bin/install.mjs --dry-run --copy --all",
|
|
46
|
+
"test:unit": "node --test bin/resolve-selection.test.mjs && node --test bin/install-summary.test.mjs",
|
|
47
47
|
"link-skills": "./scripts/link-skills.sh",
|
|
48
48
|
"build": "node scripts/verify-package-files.mjs && node scripts/verify-marketplace.mjs",
|
|
49
49
|
"generate:marketplace": "node scripts/generate-marketplace.mjs",
|