@tickpick/skills 0.1.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.
Files changed (3) hide show
  1. package/README.md +42 -0
  2. package/cli.mjs +226 -0
  3. package/package.json +19 -0
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @tickpick/skills
2
+
3
+ Install Claude Code skills shared by the TickPick PM team.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # List available skills
9
+ npx @tickpick/skills list
10
+
11
+ # Install a skill (project-local)
12
+ npx @tickpick/skills add write-prd
13
+
14
+ # Install globally (available in all projects)
15
+ npx @tickpick/skills add write-prd --global
16
+
17
+ # Update all installed skills
18
+ npx @tickpick/skills update
19
+
20
+ # Remove a skill
21
+ npx @tickpick/skills remove write-prd
22
+ ```
23
+
24
+ ## Using installed skills
25
+
26
+ Once installed, skills are available in Claude Code via the `/` command:
27
+
28
+ ```
29
+ /write-prd Payment confirmation redesign
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ By default, skills are fetched from the TickPick PM Companion registry. Override with:
35
+
36
+ ```bash
37
+ # Environment variable
38
+ SKILLS_REGISTRY_URL=https://your-app.vercel.app/api/skills/registry npx @tickpick/skills list
39
+
40
+ # CLI flag
41
+ npx @tickpick/skills list --registry=https://your-app.vercel.app/api/skills/registry
42
+ ```
package/cli.mjs ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+
7
+ const DEFAULT_REGISTRY =
8
+ process.env.SKILLS_REGISTRY_URL || "https://pm-companion.vercel.app/api/skills/registry";
9
+
10
+ const MANIFEST_FILE = ".skills-manifest.json";
11
+
12
+ // ─── Helpers ─────────────────────────────────────────────────
13
+
14
+ async function fetchRegistry(registryUrl) {
15
+ const res = await fetch(registryUrl);
16
+ if (!res.ok) throw new Error(`Failed to fetch registry: ${res.status}`);
17
+ const data = await res.json();
18
+ return data.skills;
19
+ }
20
+
21
+ async function fetchSkillMd(registryUrl, slug) {
22
+ const res = await fetch(`${registryUrl}/${slug}`);
23
+ if (!res.ok) {
24
+ if (res.status === 404) return null;
25
+ throw new Error(`Failed to fetch skill "${slug}": ${res.status}`);
26
+ }
27
+ return res.text();
28
+ }
29
+
30
+ function getSkillsDir(global) {
31
+ return global
32
+ ? join(homedir(), ".claude", "skills")
33
+ : join(process.cwd(), ".claude", "skills");
34
+ }
35
+
36
+ function getManifestPath(global) {
37
+ return global
38
+ ? join(homedir(), ".claude", MANIFEST_FILE)
39
+ : join(process.cwd(), ".claude", MANIFEST_FILE);
40
+ }
41
+
42
+ function readManifest(global) {
43
+ const path = getManifestPath(global);
44
+ if (!existsSync(path)) return { registry: DEFAULT_REGISTRY, installed: {} };
45
+ try {
46
+ return JSON.parse(readFileSync(path, "utf-8"));
47
+ } catch {
48
+ return { registry: DEFAULT_REGISTRY, installed: {} };
49
+ }
50
+ }
51
+
52
+ function writeManifest(global, manifest) {
53
+ const path = getManifestPath(global);
54
+ mkdirSync(join(path, ".."), { recursive: true });
55
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
56
+ }
57
+
58
+ function installSkill(slug, content, global) {
59
+ const dir = join(getSkillsDir(global), slug);
60
+ mkdirSync(dir, { recursive: true });
61
+ writeFileSync(join(dir, "SKILL.md"), content);
62
+ }
63
+
64
+ // ─── Commands ────────────────────────────────────────────────
65
+
66
+ async function cmdList(registryUrl) {
67
+ const skills = await fetchRegistry(registryUrl);
68
+ if (skills.length === 0) {
69
+ console.log("No skills available.");
70
+ return;
71
+ }
72
+
73
+ console.log("\nAvailable skills:\n");
74
+ const maxSlug = Math.max(...skills.map((s) => s.slug.length));
75
+ for (const s of skills) {
76
+ console.log(` ${s.slug.padEnd(maxSlug + 2)}${s.description}`);
77
+ }
78
+ console.log(`\n${skills.length} skill${skills.length !== 1 ? "s" : ""} available.`);
79
+ console.log("Install with: npx @tickpick/skills add <name>\n");
80
+ }
81
+
82
+ async function cmdAdd(slug, registryUrl, global) {
83
+ const content = await fetchSkillMd(registryUrl, slug);
84
+ if (!content) {
85
+ console.error(`Error: skill "${slug}" not found.`);
86
+ process.exit(1);
87
+ }
88
+
89
+ installSkill(slug, content, global);
90
+
91
+ // Update manifest
92
+ const manifest = readManifest(global);
93
+ manifest.registry = registryUrl;
94
+ if (!manifest.installed) manifest.installed = {};
95
+ manifest.installed[slug] = { updatedAt: new Date().toISOString() };
96
+ writeManifest(global, manifest);
97
+
98
+ const target = resolve(getSkillsDir(global), slug, "SKILL.md");
99
+ console.log(`✓ Installed ${slug} → ${target}`);
100
+ }
101
+
102
+ async function cmdUpdate(registryUrl, global) {
103
+ const manifest = readManifest(global);
104
+ const installed = Object.keys(manifest.installed || {});
105
+
106
+ if (installed.length === 0) {
107
+ console.log("No skills installed. Use `npx @tickpick/skills add <name>` first.");
108
+ return;
109
+ }
110
+
111
+ const url = registryUrl || manifest.registry || DEFAULT_REGISTRY;
112
+ const skills = await fetchRegistry(url);
113
+ const skillMap = Object.fromEntries(skills.map((s) => [s.slug, s]));
114
+
115
+ let updated = 0;
116
+ for (const slug of installed) {
117
+ const remote = skillMap[slug];
118
+ if (!remote) {
119
+ console.log(` ${slug} ⚠ no longer in registry`);
120
+ continue;
121
+ }
122
+
123
+ const localDate = manifest.installed[slug]?.updatedAt;
124
+ if (localDate && new Date(remote.updatedAt) <= new Date(localDate)) {
125
+ console.log(` ${slug} · unchanged`);
126
+ continue;
127
+ }
128
+
129
+ const content = await fetchSkillMd(url, slug);
130
+ if (content) {
131
+ installSkill(slug, content, global);
132
+ manifest.installed[slug] = { updatedAt: remote.updatedAt };
133
+ console.log(` ${slug} ✓ updated`);
134
+ updated++;
135
+ }
136
+ }
137
+
138
+ writeManifest(global, manifest);
139
+ console.log(`\n${updated} skill${updated !== 1 ? "s" : ""} updated.`);
140
+ }
141
+
142
+ async function cmdRemove(slug, global) {
143
+ const dir = join(getSkillsDir(global), slug);
144
+ if (!existsSync(dir)) {
145
+ console.error(`Error: skill "${slug}" is not installed.`);
146
+ process.exit(1);
147
+ }
148
+
149
+ rmSync(dir, { recursive: true });
150
+
151
+ const manifest = readManifest(global);
152
+ if (manifest.installed) {
153
+ delete manifest.installed[slug];
154
+ writeManifest(global, manifest);
155
+ }
156
+
157
+ console.log(`✓ Removed ${slug}`);
158
+ }
159
+
160
+ // ─── CLI entry point ─────────────────────────────────────────
161
+
162
+ const args = process.argv.slice(2);
163
+ const command = args[0];
164
+ const isGlobal = args.includes("--global") || args.includes("-g");
165
+ const registryFlag = args.find((a) => a.startsWith("--registry="));
166
+ const registryUrl = registryFlag
167
+ ? registryFlag.split("=").slice(1).join("=")
168
+ : DEFAULT_REGISTRY;
169
+
170
+ // Filter out flags to get positional args
171
+ const positional = args.filter(
172
+ (a) => !a.startsWith("--") && !a.startsWith("-g"),
173
+ );
174
+
175
+ switch (command) {
176
+ case "list":
177
+ case "ls":
178
+ await cmdList(registryUrl);
179
+ break;
180
+
181
+ case "add":
182
+ case "install":
183
+ if (!positional[1]) {
184
+ console.error("Usage: npx @tickpick/skills add <skill-name>");
185
+ process.exit(1);
186
+ }
187
+ await cmdAdd(positional[1], registryUrl, isGlobal);
188
+ break;
189
+
190
+ case "update":
191
+ case "upgrade":
192
+ await cmdUpdate(registryUrl, isGlobal);
193
+ break;
194
+
195
+ case "remove":
196
+ case "rm":
197
+ if (!positional[1]) {
198
+ console.error("Usage: npx @tickpick/skills remove <skill-name>");
199
+ process.exit(1);
200
+ }
201
+ await cmdRemove(positional[1], isGlobal);
202
+ break;
203
+
204
+ default:
205
+ console.log(`
206
+ @tickpick/skills — Claude Code skills for the TickPick PM team
207
+
208
+ Commands:
209
+ list Show available skills
210
+ add <name> Install a skill to .claude/skills/
211
+ add <name> --global Install to ~/.claude/skills/ (all projects)
212
+ update Re-fetch all installed skills
213
+ remove <name> Remove an installed skill
214
+
215
+ Options:
216
+ --global, -g Use global (~/.claude/) instead of project-local
217
+ --registry=<url> Override the registry URL
218
+
219
+ Examples:
220
+ npx @tickpick/skills list
221
+ npx @tickpick/skills add write-prd
222
+ npx @tickpick/skills add sprint-retro --global
223
+ npx @tickpick/skills update
224
+ `);
225
+ break;
226
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@tickpick/skills",
3
+ "version": "0.1.0",
4
+ "description": "Install Claude Code skills shared by the TickPick PM team",
5
+ "type": "module",
6
+ "bin": {
7
+ "skills": "./cli.mjs"
8
+ },
9
+ "files": [
10
+ "cli.mjs"
11
+ ],
12
+ "keywords": [
13
+ "claude-code",
14
+ "skills",
15
+ "tickpick"
16
+ ],
17
+ "license": "UNLICENSED",
18
+ "private": false
19
+ }