@its-thepoe/skills 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../lib/cli-main.mjs";
3
+
4
+ process.exitCode = run(process.argv.slice(2));
@@ -0,0 +1,249 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+ import {
6
+ AGENT_TARGETS,
7
+ agentBaseDir,
8
+ linkSkillToAgents,
9
+ readSkillVersion,
10
+ removeSkillFromAgents,
11
+ resolveSkillRoot,
12
+ } from "./link-engine.mjs";
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const PKG_DIR = path.resolve(__dirname, "..");
16
+ const MANIFEST_PATH = path.join(PKG_DIR, "skills.manifest.json");
17
+
18
+ function logLine(msg) {
19
+ process.stdout.write(msg + "\n");
20
+ }
21
+
22
+ function loadManifest() {
23
+ const raw = fs.readFileSync(MANIFEST_PATH, "utf8");
24
+ return JSON.parse(raw);
25
+ }
26
+
27
+ function parseGlobalFlags(argv) {
28
+ const flags = {
29
+ dryRun: false,
30
+ only: [],
31
+ strategy: "symlink",
32
+ online: false,
33
+ };
34
+ const rest = [];
35
+ for (let i = 0; i < argv.length; i += 1) {
36
+ const a = argv[i];
37
+ if (a === "--dry-run") flags.dryRun = true;
38
+ else if (a === "--online") flags.online = true;
39
+ else if (a === "--strategy" && argv[i + 1]) {
40
+ flags.strategy = String(argv[++i]);
41
+ if (flags.strategy !== "symlink" && flags.strategy !== "copy") {
42
+ throw new Error('--strategy must be "symlink" or "copy"');
43
+ }
44
+ } else if (a.startsWith("--only=")) {
45
+ flags.only.push(
46
+ ...a
47
+ .slice("--only=".length)
48
+ .split(",")
49
+ .map((s) => s.trim())
50
+ .filter(Boolean)
51
+ );
52
+ } else if (a === "--only" && argv[i + 1]) {
53
+ flags.only.push(
54
+ ...String(argv[++i])
55
+ .split(",")
56
+ .map((s) => s.trim())
57
+ .filter(Boolean)
58
+ );
59
+ } else rest.push(a);
60
+ }
61
+ return { flags, rest };
62
+ }
63
+
64
+ function printHelp() {
65
+ logLine(`@its-thepoe/skills — install Agent Skills into local agent directories
66
+
67
+ Usage:
68
+ npx @its-thepoe/skills@latest <command> [options]
69
+
70
+ Commands:
71
+ install --all Link or copy every skill from the manifest
72
+ install <skill-name> [...] Install specific skills (folder names)
73
+ sync --all Idempotent re-install (same as install --all)
74
+ sync <skill-name> [...] Re-install specific skills
75
+ check Verify installs under agent dirs
76
+ remove --all Remove managed installs for all manifest skills
77
+ remove <skill-name> [...] Remove specific skills
78
+
79
+ Options:
80
+ --dry-run Print actions without writing
81
+ --only=<a>[,<b>...] Limit targets: cursor, claude, opencode, windsurf, all
82
+ --strategy symlink|copy Default symlink; use copy if symlinks fail (e.g. Windows)
83
+ --online With check: compare to npm registry (requires network)
84
+
85
+ Agents (default: all):
86
+ ${Object.keys(AGENT_TARGETS)
87
+ .map((k) => ` - ${k}`)
88
+ .join("\n")}
89
+ `);
90
+ }
91
+
92
+ function getManifestEntries(manifest, names) {
93
+ const byName = new Map(manifest.skills.map((s) => [s.name, s]));
94
+ const out = [];
95
+ for (const n of names) {
96
+ const row = byName.get(n);
97
+ if (!row) throw new Error(`Unknown skill "${n}" (not in skills.manifest.json)`);
98
+ out.push(row);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ function npmViewVersion(pkg) {
104
+ const r = spawnSync("npm", ["view", pkg, "version"], {
105
+ encoding: "utf8",
106
+ shell: process.platform === "win32",
107
+ });
108
+ if (r.status !== 0) return null;
109
+ return String(r.stdout || "").trim() || null;
110
+ }
111
+
112
+ /**
113
+ * @param {{ online: boolean }} opts
114
+ */
115
+ function runCheck(opts) {
116
+ const manifest = loadManifest();
117
+ const { online } = opts;
118
+ let exit = 0;
119
+ for (const row of manifest.skills) {
120
+ let root = null;
121
+ try {
122
+ root = resolveSkillRoot(row.package, import.meta.url);
123
+ } catch {
124
+ logLine(`BUNDLE ${row.package}: not resolvable via node_modules (install orchestrator deps)`);
125
+ exit = 1;
126
+ continue;
127
+ }
128
+ const bundledVer = readSkillVersion(root);
129
+ let latest = null;
130
+ if (online) {
131
+ latest = npmViewVersion(row.package);
132
+ }
133
+ logLine(`\n${row.name} (${row.package}) bundled@${bundledVer}${latest ? ` registry_latest@${latest}` : ""}`);
134
+
135
+ for (const agent of Object.keys(AGENT_TARGETS)) {
136
+ const dest = path.join(agentBaseDir(agent), row.name);
137
+ if (!fs.existsSync(dest)) {
138
+ logLine(` [${agent}] MISSING ${dest}`);
139
+ exit = 1;
140
+ continue;
141
+ }
142
+ const st = fs.lstatSync(dest);
143
+ if (st.isSymbolicLink()) {
144
+ const t = fs.readlinkSync(dest);
145
+ logLine(` [${agent}] OK symlink -> ${t}`);
146
+ } else if (st.isDirectory()) {
147
+ const skillMd = path.join(dest, "SKILL.md");
148
+ if (!fs.existsSync(skillMd)) {
149
+ logLine(` [${agent}] BAD copy dir (no SKILL.md) ${dest}`);
150
+ exit = 1;
151
+ } else {
152
+ logLine(` [${agent}] OK directory copy ${dest}`);
153
+ }
154
+ } else {
155
+ logLine(` [${agent}] UNEXPECTED ${dest}`);
156
+ exit = 1;
157
+ }
158
+ }
159
+ }
160
+ return exit;
161
+ }
162
+
163
+ /**
164
+ * @param {string[]} argv
165
+ */
166
+ export function run(argv) {
167
+ if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
168
+ printHelp();
169
+ return 0;
170
+ }
171
+
172
+ const { flags, rest } = parseGlobalFlags(argv);
173
+ const cmd = rest[0];
174
+ const args = rest.slice(1);
175
+
176
+ if (!cmd || cmd === "help") {
177
+ printHelp();
178
+ return 0;
179
+ }
180
+
181
+ const log = logLine;
182
+ const only = flags.only.length ? flags.only.join(",").split(",").map((s) => s.trim()).filter(Boolean) : ["all"];
183
+
184
+ try {
185
+ if (cmd === "check") {
186
+ return runCheck({ online: flags.online });
187
+ }
188
+
189
+ if (cmd === "install" || cmd === "sync") {
190
+ const all = args.includes("--all");
191
+ const names = args.filter((a) => a !== "--all");
192
+ const manifest = loadManifest();
193
+ let entries = manifest.skills;
194
+ if (!all) {
195
+ if (names.length === 0) {
196
+ logLine("Error: specify skill names or --all");
197
+ printHelp();
198
+ return 1;
199
+ }
200
+ entries = getManifestEntries(manifest, names);
201
+ }
202
+
203
+ for (const row of entries) {
204
+ const root = resolveSkillRoot(row.package, import.meta.url);
205
+ const version = readSkillVersion(root);
206
+ logLine(`\n${cmd} ${row.name} @ ${root}`);
207
+ linkSkillToAgents({
208
+ skillRoot: root,
209
+ skillName: row.name,
210
+ skillPackage: row.package,
211
+ version,
212
+ only,
213
+ strategy: flags.strategy,
214
+ dryRun: flags.dryRun,
215
+ log,
216
+ });
217
+ }
218
+ logLine(`\nDone. Reload your agents (Cursor, Claude Code, OpenCode, Windsurf).`);
219
+ return 0;
220
+ }
221
+
222
+ if (cmd === "remove") {
223
+ const all = args.includes("--all");
224
+ const names = args.filter((a) => a !== "--all");
225
+ const manifest = loadManifest();
226
+ let targets = manifest.skills.map((s) => s.name);
227
+ if (!all) {
228
+ if (names.length === 0) {
229
+ logLine("Error: specify skill names or --all");
230
+ return 1;
231
+ }
232
+ targets = names;
233
+ for (const n of targets) getManifestEntries(manifest, [n]);
234
+ }
235
+ for (const name of targets) {
236
+ logLine(`\nremove ${name}`);
237
+ removeSkillFromAgents({ skillName: name, only, dryRun: flags.dryRun, log });
238
+ }
239
+ return 0;
240
+ }
241
+
242
+ logLine(`Unknown command: ${cmd}`);
243
+ printHelp();
244
+ return 1;
245
+ } catch (e) {
246
+ logLine(`Error: ${e instanceof Error ? e.message : e}`);
247
+ return 1;
248
+ }
249
+ }
@@ -0,0 +1,218 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+
6
+ const INSTALL_MARKER = ".its-thepoe-skills-install.json";
7
+
8
+ /** @type {Record<string, { env?: string, segments: string[] }>} */
9
+ export const AGENT_TARGETS = {
10
+ cursor: { segments: [".cursor", "skills"] },
11
+ claude: { segments: [".claude", "skills"] },
12
+ opencode: { segments: [".config", "opencode", "skills"] },
13
+ windsurf: { segments: [".codeium", "windsurf", "skills"] },
14
+ };
15
+
16
+ /**
17
+ * @param {string} key
18
+ * @returns {string}
19
+ */
20
+ export function agentBaseDir(key) {
21
+ const home = os.homedir();
22
+ const spec = AGENT_TARGETS[key];
23
+ if (!spec) throw new Error(`Unknown agent target: ${key}`);
24
+ return path.join(home, ...spec.segments);
25
+ }
26
+
27
+ /**
28
+ * @param {string[]} onlyKeys
29
+ */
30
+ export function normalizeOnly(onlyKeys) {
31
+ const allowed = new Set(["cursor", "claude", "opencode", "windsurf"]);
32
+ if (onlyKeys.length === 0 || onlyKeys.includes("all")) {
33
+ return [...allowed];
34
+ }
35
+ for (const k of onlyKeys) {
36
+ if (!allowed.has(k)) throw new Error(`Invalid --only value: ${k}`);
37
+ }
38
+ return onlyKeys;
39
+ }
40
+
41
+ function exists(p) {
42
+ try {
43
+ fs.accessSync(p);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param {string} skillPackage
52
+ * @param {string} fromFile - use import.meta.url of caller for resolution context
53
+ * @returns {string} absolute path to package root
54
+ */
55
+ export function resolveSkillRoot(skillPackage, fromFile) {
56
+ const require = createRequire(fromFile);
57
+ // Do not use `${pkg}/package.json` — many packages omit it from "exports".
58
+ // Resolve the package main (e.g. SKILL.md); its directory is the package root.
59
+ const entry = require.resolve(skillPackage);
60
+ return path.dirname(entry);
61
+ }
62
+
63
+ function readJson(p) {
64
+ return JSON.parse(fs.readFileSync(p, "utf8"));
65
+ }
66
+
67
+ function writeMarker(destDir, meta) {
68
+ const p = path.join(destDir, INSTALL_MARKER);
69
+ fs.writeFileSync(p, JSON.stringify(meta, null, 2) + "\n", "utf8");
70
+ }
71
+
72
+ /**
73
+ * @param {{ dest: string, source: string, dryRun: boolean, strategy: 'symlink'|'copy', skillName: string, skillPackage: string, version: string, log: (m:string)=>void }} opts
74
+ */
75
+ function linkOneDest(opts) {
76
+ const { dest, source, dryRun, strategy, skillName, skillPackage, version, log } = opts;
77
+ const absSource = path.resolve(source);
78
+
79
+ if (strategy === "symlink") {
80
+ if (exists(dest)) {
81
+ const st = fs.lstatSync(dest);
82
+ if (st.isSymbolicLink()) {
83
+ let target = fs.readlinkSync(dest);
84
+ if (!path.isAbsolute(target)) {
85
+ target = path.resolve(path.dirname(dest), target);
86
+ }
87
+ if (path.resolve(target) === absSource) {
88
+ log(`SKIP ${dest} -> ${absSource}`);
89
+ return;
90
+ }
91
+ log(`${dryRun ? "DRY-RUN " : ""}REPLACE ${dest} (symlink -> new target)`);
92
+ if (!dryRun) fs.unlinkSync(dest);
93
+ } else if (st.isDirectory()) {
94
+ // copy-mode directory or manual folder — don't destroy
95
+ const marker = path.join(dest, INSTALL_MARKER);
96
+ if (exists(marker)) {
97
+ log(`${dryRun ? "DRY-RUN " : ""}REPLACE ${dest} (prior copy install)`);
98
+ if (!dryRun) fs.rmSync(dest, { recursive: true, force: true });
99
+ } else {
100
+ throw new Error(
101
+ `Refusing to replace ${dest}: exists and is not a symlink managed by this tool`
102
+ );
103
+ }
104
+ } else {
105
+ throw new Error(`Refusing to replace ${dest}: not a directory or symlink`);
106
+ }
107
+ } else {
108
+ log(`${dryRun ? "DRY-RUN " : ""}CREATE ${dest} -> ${absSource}`);
109
+ }
110
+
111
+ if (dryRun) return;
112
+
113
+ try {
114
+ fs.symlinkSync(absSource, dest, "dir");
115
+ } catch (err) {
116
+ if (err && err.code === "EPERM" && process.platform === "win32") {
117
+ throw new Error(
118
+ `Symlink failed (EPERM). Re-run with --strategy copy, or enable Developer Mode / run as admin for symlinks.`
119
+ );
120
+ }
121
+ throw err;
122
+ }
123
+ return;
124
+ }
125
+
126
+ // copy
127
+ if (exists(dest)) {
128
+ log(`${dryRun ? "DRY-RUN " : ""}REPLACE ${dest} (copy)`);
129
+ if (!dryRun) fs.rmSync(dest, { recursive: true, force: true });
130
+ } else {
131
+ log(`${dryRun ? "DRY-RUN " : ""}CREATE ${dest} (copy)`);
132
+ }
133
+ if (dryRun) return;
134
+ fs.cpSync(absSource, dest, { recursive: true });
135
+ writeMarker(dest, {
136
+ managedBy: "@its-thepoe/skills",
137
+ skillName,
138
+ package: skillPackage,
139
+ version,
140
+ strategy: "copy",
141
+ installedAt: new Date().toISOString(),
142
+ });
143
+ }
144
+
145
+ /**
146
+ * @param {{ skillRoot: string, skillName: string, skillPackage: string, version: string, only: string[], strategy: 'symlink'|'copy', dryRun: boolean, log: (m:string)=>void }} p
147
+ */
148
+ export function linkSkillToAgents(p) {
149
+ const { skillRoot, skillName, skillPackage, version, only, strategy, dryRun, log } = p;
150
+ if (!exists(path.join(skillRoot, "SKILL.md"))) {
151
+ throw new Error(`Missing SKILL.md under ${skillRoot}`);
152
+ }
153
+
154
+ for (const key of normalizeOnly(only)) {
155
+ const base = agentBaseDir(key);
156
+ const dest = path.join(base, skillName);
157
+ if (!dryRun) fs.mkdirSync(base, { recursive: true });
158
+ linkOneDest({
159
+ dest,
160
+ source: skillRoot,
161
+ dryRun,
162
+ strategy,
163
+ skillName,
164
+ skillPackage,
165
+ version,
166
+ log,
167
+ });
168
+ }
169
+ }
170
+
171
+ /**
172
+ * @param {{ skillName: string, only: string[], dryRun: boolean, log: (m:string)=>void }} p
173
+ */
174
+ export function removeSkillFromAgents(p) {
175
+ const { skillName, only, dryRun, log } = p;
176
+ for (const key of normalizeOnly(only)) {
177
+ const dest = path.join(agentBaseDir(key), skillName);
178
+ if (!exists(dest)) {
179
+ log(`SKIP (missing) ${dest}`);
180
+ continue;
181
+ }
182
+ const st = fs.lstatSync(dest);
183
+ if (st.isSymbolicLink()) {
184
+ log(`REMOVE ${dest}`);
185
+ if (!dryRun) fs.unlinkSync(dest);
186
+ continue;
187
+ }
188
+ if (st.isDirectory()) {
189
+ const marker = path.join(dest, INSTALL_MARKER);
190
+ if (exists(marker)) {
191
+ try {
192
+ const meta = readJson(marker);
193
+ if (meta.managedBy === "@its-thepoe/skills") {
194
+ log(`REMOVE ${dest} (copy install)`);
195
+ if (!dryRun) fs.rmSync(dest, { recursive: true, force: true });
196
+ continue;
197
+ }
198
+ } catch {
199
+ /* fall through */
200
+ }
201
+ }
202
+ log(`SKIP ${dest} (directory not managed; no marker)`);
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * @param {string} skillRoot
209
+ */
210
+ export function readSkillVersion(skillRoot) {
211
+ const pj = path.join(skillRoot, "package.json");
212
+ if (!exists(pj)) return "0.0.0";
213
+ try {
214
+ return readJson(String(pj)).version ?? "0.0.0";
215
+ } catch {
216
+ return "0.0.0";
217
+ }
218
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@its-thepoe/skills",
3
+ "version": "1.0.0",
4
+ "description": "Orchestrator CLI to install, check, sync, and remove @its-thepoe Agent Skills (Cursor, Claude Code, OpenCode, Windsurf).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "agent-skills",
9
+ "cursor",
10
+ "claude-code",
11
+ "skills",
12
+ "its-thepoe"
13
+ ],
14
+ "bin": {
15
+ "skills": "./bin/cli.mjs"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "lib",
20
+ "skills.manifest.json",
21
+ "package.json"
22
+ ],
23
+ "dependencies": {
24
+ "@its-thepoe/alt-text": "1.0.0",
25
+ "@its-thepoe/design-and-refine": "1.0.0",
26
+ "@its-thepoe/design-engineering": "1.0.0",
27
+ "@its-thepoe/design-motion-principles": "1.0.0",
28
+ "@its-thepoe/family-taste": "1.0.0",
29
+ "@its-thepoe/write-a-skill": "1.0.0"
30
+ }
31
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "skills": [
3
+ { "name": "alt-text", "package": "@its-thepoe/alt-text" },
4
+ { "name": "design-and-refine", "package": "@its-thepoe/design-and-refine" },
5
+ { "name": "design-engineering", "package": "@its-thepoe/design-engineering" },
6
+ { "name": "design-motion-principles", "package": "@its-thepoe/design-motion-principles" },
7
+ { "name": "family-taste", "package": "@its-thepoe/family-taste" },
8
+ { "name": "write-a-skill", "package": "@its-thepoe/write-a-skill" }
9
+ ]
10
+ }