@skillsmanager/cli 0.0.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 +84 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +68 -0
- package/dist/backends/gdrive.d.ts +24 -0
- package/dist/backends/gdrive.js +371 -0
- package/dist/backends/interface.d.ts +14 -0
- package/dist/backends/interface.js +1 -0
- package/dist/backends/local.d.ts +20 -0
- package/dist/backends/local.js +159 -0
- package/dist/bm25.d.ts +20 -0
- package/dist/bm25.js +65 -0
- package/dist/cache.d.ts +21 -0
- package/dist/cache.js +59 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +62 -0
- package/dist/commands/collection.d.ts +1 -0
- package/dist/commands/collection.js +42 -0
- package/dist/commands/fetch.d.ts +5 -0
- package/dist/commands/fetch.js +46 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +45 -0
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.js +89 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +38 -0
- package/dist/commands/refresh.d.ts +1 -0
- package/dist/commands/refresh.js +46 -0
- package/dist/commands/registry.d.ts +14 -0
- package/dist/commands/registry.js +258 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +37 -0
- package/dist/commands/setup/google.d.ts +1 -0
- package/dist/commands/setup/google.js +281 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +83 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +136 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +128 -0
- package/dist/ready.d.ts +15 -0
- package/dist/ready.js +39 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.js +58 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +13 -0
- package/package.json +56 -0
- package/skills/skillsmanager/SKILL.md +139 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { parseCollection, serializeCollection, parseRegistryFile, serializeRegistryFile, COLLECTION_FILENAME, LEGACY_COLLECTION_FILENAME, } from "../registry.js";
|
|
5
|
+
import { CONFIG_DIR } from "../config.js";
|
|
6
|
+
const COLLECTIONS_DIR = path.join(CONFIG_DIR, "collections");
|
|
7
|
+
const LOCAL_REGISTRY_PATH = path.join(CONFIG_DIR, "registry.yaml");
|
|
8
|
+
/**
|
|
9
|
+
* Local filesystem backend — stores collections and the registry under ~/.skillsmanager/.
|
|
10
|
+
* Works with zero setup, no auth, no internet. This is the default backend.
|
|
11
|
+
*/
|
|
12
|
+
export class LocalBackend {
|
|
13
|
+
// ── Identity ─────────────────────────────────────────────────────────────
|
|
14
|
+
async getOwner() {
|
|
15
|
+
// Try to read from existing registry first
|
|
16
|
+
if (fs.existsSync(LOCAL_REGISTRY_PATH)) {
|
|
17
|
+
const data = parseRegistryFile(fs.readFileSync(LOCAL_REGISTRY_PATH, "utf-8"));
|
|
18
|
+
if (data.owner)
|
|
19
|
+
return data.owner;
|
|
20
|
+
}
|
|
21
|
+
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
22
|
+
}
|
|
23
|
+
// ── Collection operations ────────────────────────────────────────────────
|
|
24
|
+
async discoverCollections() {
|
|
25
|
+
if (!fs.existsSync(COLLECTIONS_DIR))
|
|
26
|
+
return [];
|
|
27
|
+
const collections = [];
|
|
28
|
+
for (const entry of fs.readdirSync(COLLECTIONS_DIR, { withFileTypes: true })) {
|
|
29
|
+
if (!entry.isDirectory())
|
|
30
|
+
continue;
|
|
31
|
+
const dir = path.join(COLLECTIONS_DIR, entry.name);
|
|
32
|
+
const hasNew = fs.existsSync(path.join(dir, COLLECTION_FILENAME));
|
|
33
|
+
const hasLegacy = fs.existsSync(path.join(dir, LEGACY_COLLECTION_FILENAME));
|
|
34
|
+
if (hasNew || hasLegacy) {
|
|
35
|
+
collections.push({
|
|
36
|
+
name: entry.name,
|
|
37
|
+
backend: "local",
|
|
38
|
+
folderId: dir,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return collections;
|
|
43
|
+
}
|
|
44
|
+
async readCollection(collection) {
|
|
45
|
+
const dir = collection.folderId;
|
|
46
|
+
for (const filename of [COLLECTION_FILENAME, LEGACY_COLLECTION_FILENAME]) {
|
|
47
|
+
const filePath = path.join(dir, filename);
|
|
48
|
+
if (fs.existsSync(filePath)) {
|
|
49
|
+
return parseCollection(fs.readFileSync(filePath, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Collection file not found in "${collection.name}"`);
|
|
53
|
+
}
|
|
54
|
+
async writeCollection(collection, data) {
|
|
55
|
+
const dir = collection.folderId;
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
fs.writeFileSync(path.join(dir, COLLECTION_FILENAME), serializeCollection(data));
|
|
58
|
+
}
|
|
59
|
+
async downloadSkill(collection, skillName, destDir) {
|
|
60
|
+
const src = path.join(collection.folderId, skillName);
|
|
61
|
+
if (!fs.existsSync(src)) {
|
|
62
|
+
throw new Error(`Skill "${skillName}" not found in local collection "${collection.name}"`);
|
|
63
|
+
}
|
|
64
|
+
// If source and dest are the same, no-op
|
|
65
|
+
if (path.resolve(src) === path.resolve(destDir))
|
|
66
|
+
return;
|
|
67
|
+
copyDirSync(src, destDir);
|
|
68
|
+
}
|
|
69
|
+
async uploadSkill(collection, localPath, skillName) {
|
|
70
|
+
const dest = path.join(collection.folderId, skillName);
|
|
71
|
+
// If source and dest are the same, no-op
|
|
72
|
+
if (path.resolve(localPath) === path.resolve(dest))
|
|
73
|
+
return;
|
|
74
|
+
copyDirSync(localPath, dest);
|
|
75
|
+
}
|
|
76
|
+
// ── Registry operations ──────────────────────────────────────────────────
|
|
77
|
+
async discoverRegistries() {
|
|
78
|
+
if (!fs.existsSync(LOCAL_REGISTRY_PATH))
|
|
79
|
+
return [];
|
|
80
|
+
return [{
|
|
81
|
+
name: "local",
|
|
82
|
+
backend: "local",
|
|
83
|
+
folderId: CONFIG_DIR,
|
|
84
|
+
fileId: LOCAL_REGISTRY_PATH,
|
|
85
|
+
}];
|
|
86
|
+
}
|
|
87
|
+
async readRegistry(registry) {
|
|
88
|
+
const filePath = registry.fileId ?? LOCAL_REGISTRY_PATH;
|
|
89
|
+
if (!fs.existsSync(filePath)) {
|
|
90
|
+
throw new Error("Local registry not found");
|
|
91
|
+
}
|
|
92
|
+
return parseRegistryFile(fs.readFileSync(filePath, "utf-8"));
|
|
93
|
+
}
|
|
94
|
+
async writeRegistry(registry, data) {
|
|
95
|
+
const filePath = registry.fileId ?? LOCAL_REGISTRY_PATH;
|
|
96
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(filePath, serializeRegistryFile(data));
|
|
98
|
+
}
|
|
99
|
+
async resolveCollectionRef(ref) {
|
|
100
|
+
if (ref.backend !== "local")
|
|
101
|
+
return null;
|
|
102
|
+
const dir = path.join(COLLECTIONS_DIR, ref.ref);
|
|
103
|
+
const hasNew = fs.existsSync(path.join(dir, COLLECTION_FILENAME));
|
|
104
|
+
const hasLegacy = fs.existsSync(path.join(dir, LEGACY_COLLECTION_FILENAME));
|
|
105
|
+
if (!hasNew && !hasLegacy)
|
|
106
|
+
return null;
|
|
107
|
+
return {
|
|
108
|
+
name: ref.name,
|
|
109
|
+
backend: "local",
|
|
110
|
+
folderId: dir,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async createRegistry(name) {
|
|
114
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
115
|
+
const owner = await this.getOwner();
|
|
116
|
+
const data = {
|
|
117
|
+
name: name ?? "local",
|
|
118
|
+
owner,
|
|
119
|
+
source: "local",
|
|
120
|
+
collections: [],
|
|
121
|
+
};
|
|
122
|
+
fs.writeFileSync(LOCAL_REGISTRY_PATH, serializeRegistryFile(data));
|
|
123
|
+
return {
|
|
124
|
+
id: randomUUID(),
|
|
125
|
+
name: name ?? "local",
|
|
126
|
+
backend: "local",
|
|
127
|
+
folderId: CONFIG_DIR,
|
|
128
|
+
fileId: LOCAL_REGISTRY_PATH,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// ── Convenience: create a local collection ───────────────────────────────
|
|
132
|
+
async createCollection(name) {
|
|
133
|
+
const dir = path.join(COLLECTIONS_DIR, name);
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
const owner = await this.getOwner();
|
|
136
|
+
const data = { name, owner, skills: [] };
|
|
137
|
+
fs.writeFileSync(path.join(dir, COLLECTION_FILENAME), serializeCollection(data));
|
|
138
|
+
return {
|
|
139
|
+
id: randomUUID(),
|
|
140
|
+
name,
|
|
141
|
+
backend: "local",
|
|
142
|
+
folderId: dir,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
147
|
+
function copyDirSync(src, dest) {
|
|
148
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
149
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
150
|
+
const srcPath = path.join(src, entry.name);
|
|
151
|
+
const destPath = path.join(dest, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
copyDirSync(srcPath, destPath);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
fs.copyFileSync(srcPath, destPath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/bm25.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 ranking implementation.
|
|
3
|
+
*
|
|
4
|
+
* Scores a set of documents against a query. Higher score = better match.
|
|
5
|
+
* Documents are ranked by how well query terms appear in them, weighted by
|
|
6
|
+
* how rare those terms are across the whole corpus.
|
|
7
|
+
*
|
|
8
|
+
* Parameters (industry defaults):
|
|
9
|
+
* k1 = 1.5 — TF saturation: diminishing returns on repeated terms
|
|
10
|
+
* b = 0.75 — length normalization: penalizes longer documents slightly
|
|
11
|
+
*/
|
|
12
|
+
export interface BM25Document {
|
|
13
|
+
id: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
export interface BM25Result {
|
|
17
|
+
id: string;
|
|
18
|
+
score: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function bm25Search(docs: BM25Document[], query: string, topK?: number): BM25Result[];
|
package/dist/bm25.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 ranking implementation.
|
|
3
|
+
*
|
|
4
|
+
* Scores a set of documents against a query. Higher score = better match.
|
|
5
|
+
* Documents are ranked by how well query terms appear in them, weighted by
|
|
6
|
+
* how rare those terms are across the whole corpus.
|
|
7
|
+
*
|
|
8
|
+
* Parameters (industry defaults):
|
|
9
|
+
* k1 = 1.5 — TF saturation: diminishing returns on repeated terms
|
|
10
|
+
* b = 0.75 — length normalization: penalizes longer documents slightly
|
|
11
|
+
*/
|
|
12
|
+
const K1 = 1.5;
|
|
13
|
+
const B = 0.75;
|
|
14
|
+
function tokenize(text) {
|
|
15
|
+
return text
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[_\-]/g, " ") // treat underscores and hyphens as word separators
|
|
18
|
+
.replace(/[^a-z0-9 ]/g, "")
|
|
19
|
+
.split(/\s+/)
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
export function bm25Search(docs, query, topK = 10) {
|
|
23
|
+
if (docs.length === 0 || !query.trim())
|
|
24
|
+
return [];
|
|
25
|
+
const queryTokens = tokenize(query);
|
|
26
|
+
if (queryTokens.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
// Build per-document token frequencies
|
|
29
|
+
const docTokens = docs.map((d) => tokenize(d.text));
|
|
30
|
+
const docLengths = docTokens.map((t) => t.length);
|
|
31
|
+
const avgLength = docLengths.reduce((a, b) => a + b, 0) / docs.length;
|
|
32
|
+
// Build IDF: count how many documents contain each query term
|
|
33
|
+
const docFreq = {};
|
|
34
|
+
for (const token of queryTokens) {
|
|
35
|
+
docFreq[token] = 0;
|
|
36
|
+
for (const tokens of docTokens) {
|
|
37
|
+
if (tokens.includes(token))
|
|
38
|
+
docFreq[token]++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const scores = docs.map((doc, i) => {
|
|
42
|
+
const tf = {};
|
|
43
|
+
for (const t of docTokens[i]) {
|
|
44
|
+
tf[t] = (tf[t] ?? 0) + 1;
|
|
45
|
+
}
|
|
46
|
+
let score = 0;
|
|
47
|
+
for (const token of queryTokens) {
|
|
48
|
+
const f = tf[token] ?? 0;
|
|
49
|
+
if (f === 0)
|
|
50
|
+
continue;
|
|
51
|
+
const df = docFreq[token];
|
|
52
|
+
// IDF formula: log((N - df + 0.5) / (df + 0.5) + 1)
|
|
53
|
+
const idf = Math.log((docs.length - df + 0.5) / (df + 0.5) + 1);
|
|
54
|
+
// TF normalization with length penalty
|
|
55
|
+
const tfNorm = (f * (K1 + 1)) /
|
|
56
|
+
(f + K1 * (1 - B + B * (docLengths[i] / avgLength)));
|
|
57
|
+
score += idf * tfNorm;
|
|
58
|
+
}
|
|
59
|
+
return { id: doc.id, score };
|
|
60
|
+
});
|
|
61
|
+
return scores
|
|
62
|
+
.filter((r) => r.score > 0)
|
|
63
|
+
.sort((a, b) => b.score - a.score)
|
|
64
|
+
.slice(0, topK);
|
|
65
|
+
}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CollectionInfo } from "./types.js";
|
|
2
|
+
export declare function getCachePath(collection: CollectionInfo, skillName?: string): string;
|
|
3
|
+
export declare function ensureCachePath(collection: CollectionInfo): string;
|
|
4
|
+
export type Scope = "global" | "project";
|
|
5
|
+
/**
|
|
6
|
+
* Returns the symlink target directory for the given agent and scope.
|
|
7
|
+
* - global: ~/.agent/skills/ (from AGENT_PATHS)
|
|
8
|
+
* - project: <cwd>/.agent/skills/
|
|
9
|
+
*
|
|
10
|
+
* Also returns whether the directory had to be created, so the caller
|
|
11
|
+
* can print a transparent message to the user.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveSkillsDir(agentName: string, scope: Scope, cwd: string): {
|
|
14
|
+
skillsDir: string;
|
|
15
|
+
created: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function createSymlink(skillName: string, cachePath: string, agentName: string, scope?: Scope, cwd?: string): {
|
|
18
|
+
skillsDir: string;
|
|
19
|
+
created: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function skillExistsInCache(collection: CollectionInfo, skillName: string): boolean;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { CACHE_DIR } from "./config.js";
|
|
4
|
+
import { AGENT_PATHS } from "./types.js";
|
|
5
|
+
export function getCachePath(collection, skillName) {
|
|
6
|
+
const base = path.join(CACHE_DIR, collection.id);
|
|
7
|
+
return skillName ? path.join(base, skillName) : base;
|
|
8
|
+
}
|
|
9
|
+
export function ensureCachePath(collection) {
|
|
10
|
+
const p = getCachePath(collection);
|
|
11
|
+
fs.mkdirSync(p, { recursive: true });
|
|
12
|
+
return p;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the symlink target directory for the given agent and scope.
|
|
16
|
+
* - global: ~/.agent/skills/ (from AGENT_PATHS)
|
|
17
|
+
* - project: <cwd>/.agent/skills/
|
|
18
|
+
*
|
|
19
|
+
* Also returns whether the directory had to be created, so the caller
|
|
20
|
+
* can print a transparent message to the user.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveSkillsDir(agentName, scope, cwd) {
|
|
23
|
+
if (!AGENT_PATHS[agentName]) {
|
|
24
|
+
const supported = Object.keys(AGENT_PATHS).join(", ");
|
|
25
|
+
throw new Error(`Unknown agent "${agentName}". Supported agents: ${supported}`);
|
|
26
|
+
}
|
|
27
|
+
let skillsDir;
|
|
28
|
+
if (scope === "project") {
|
|
29
|
+
// Derive the agent subfolder name from the global path (e.g. ".claude/skills")
|
|
30
|
+
const globalDir = AGENT_PATHS[agentName];
|
|
31
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
32
|
+
const relative = path.relative(home, globalDir); // e.g. ".claude/skills"
|
|
33
|
+
skillsDir = path.join(cwd, relative);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
skillsDir = AGENT_PATHS[agentName];
|
|
37
|
+
}
|
|
38
|
+
const existed = fs.existsSync(skillsDir);
|
|
39
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
40
|
+
return { skillsDir, created: !existed };
|
|
41
|
+
}
|
|
42
|
+
export function createSymlink(skillName, cachePath, agentName, scope = "global", cwd = process.cwd()) {
|
|
43
|
+
const { skillsDir, created } = resolveSkillsDir(agentName, scope, cwd);
|
|
44
|
+
const linkPath = path.join(skillsDir, skillName);
|
|
45
|
+
if (fs.existsSync(linkPath)) {
|
|
46
|
+
const stat = fs.lstatSync(linkPath);
|
|
47
|
+
if (stat.isSymbolicLink()) {
|
|
48
|
+
fs.unlinkSync(linkPath);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
throw new Error(`${linkPath} already exists and is not a symlink. Remove it manually to proceed.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
fs.symlinkSync(cachePath, linkPath);
|
|
55
|
+
return { skillsDir, created };
|
|
56
|
+
}
|
|
57
|
+
export function skillExistsInCache(collection, skillName) {
|
|
58
|
+
return fs.existsSync(getCachePath(collection, skillName));
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
import { ensureReady } from "../ready.js";
|
|
7
|
+
import { trackSkill } from "../config.js";
|
|
8
|
+
export async function addCommand(skillPath, options) {
|
|
9
|
+
const absPath = path.resolve(skillPath);
|
|
10
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
|
|
11
|
+
console.log(chalk.red(`"${skillPath}" is not a valid directory.`));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const skillMdPath = path.join(absPath, "SKILL.md");
|
|
15
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
16
|
+
console.log(chalk.red(`No SKILL.md found in "${skillPath}".`));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
20
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
if (!frontmatterMatch) {
|
|
22
|
+
console.log(chalk.red("SKILL.md is missing YAML frontmatter."));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const frontmatter = YAML.parse(frontmatterMatch[1]);
|
|
26
|
+
const skillName = frontmatter.name;
|
|
27
|
+
const description = frontmatter.description ?? "";
|
|
28
|
+
if (!skillName) {
|
|
29
|
+
console.log(chalk.red("SKILL.md frontmatter is missing 'name' field."));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const { config, backend } = await ensureReady();
|
|
33
|
+
// Pick collection — first one by default, or by name
|
|
34
|
+
let collection = config.collections[0];
|
|
35
|
+
if (options.collection) {
|
|
36
|
+
const found = config.collections.find((c) => c.name === options.collection);
|
|
37
|
+
if (!found) {
|
|
38
|
+
console.log(chalk.red(`Collection "${options.collection}" not found.`));
|
|
39
|
+
console.log(chalk.dim(` Available: ${config.collections.map((c) => c.name).join(", ")}`));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
collection = found;
|
|
43
|
+
}
|
|
44
|
+
const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
|
|
45
|
+
try {
|
|
46
|
+
await backend.uploadSkill(collection, absPath, skillName);
|
|
47
|
+
const col = await backend.readCollection(collection);
|
|
48
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
49
|
+
if (existing >= 0) {
|
|
50
|
+
col.skills[existing] = { name: skillName, path: `${skillName}/`, description };
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
col.skills.push({ name: skillName, path: `${skillName}/`, description });
|
|
54
|
+
}
|
|
55
|
+
await backend.writeCollection(collection, col);
|
|
56
|
+
trackSkill(skillName, collection.id, absPath);
|
|
57
|
+
spinner.succeed(`${chalk.bold(skillName)} added to gdrive:${collection.name}`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function collectionCreateCommand(name?: string): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { writeConfig, CONFIG_PATH } from "../config.js";
|
|
4
|
+
import { ensureAuth } from "../auth.js";
|
|
5
|
+
import { GDriveBackend } from "../backends/gdrive.js";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
export async function collectionCreateCommand(name) {
|
|
8
|
+
const auth = await ensureAuth();
|
|
9
|
+
const backend = new GDriveBackend(auth);
|
|
10
|
+
const PREFIX = "SKILLS_";
|
|
11
|
+
let folderName;
|
|
12
|
+
if (!name) {
|
|
13
|
+
folderName = `${PREFIX}MY_SKILLS`;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
folderName = name.startsWith(PREFIX) ? name : `${PREFIX}${name}`;
|
|
17
|
+
}
|
|
18
|
+
const spinner = ora(`Creating collection "${folderName}" in Google Drive...`).start();
|
|
19
|
+
try {
|
|
20
|
+
const collection = await backend.createCollection(folderName);
|
|
21
|
+
spinner.succeed(`Collection "${folderName}" created in Google Drive`);
|
|
22
|
+
let config = { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
|
|
23
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
24
|
+
try {
|
|
25
|
+
config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
26
|
+
}
|
|
27
|
+
catch { /* use default */ }
|
|
28
|
+
}
|
|
29
|
+
const already = config.collections.findIndex((c) => c.name === collection.name);
|
|
30
|
+
if (already >= 0) {
|
|
31
|
+
config.collections[already] = collection;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
config.collections.push(collection);
|
|
35
|
+
}
|
|
36
|
+
writeConfig(config);
|
|
37
|
+
console.log(`\nRun ${chalk.bold(`skillsmanager add <path>`)} to add skills to it.\n`);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getCachePath, ensureCachePath, createSymlink } from "../cache.js";
|
|
5
|
+
import { ensureReady } from "../ready.js";
|
|
6
|
+
import { trackSkill } from "../config.js";
|
|
7
|
+
export async function fetchCommand(names, options) {
|
|
8
|
+
if (names.length === 0) {
|
|
9
|
+
console.log(chalk.red("Please specify at least one skill name."));
|
|
10
|
+
console.log(chalk.dim(" Example: skillsmanager fetch pdf-skill --agent claude"));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const { config, backend } = await ensureReady();
|
|
14
|
+
// Gather all skills across collections
|
|
15
|
+
const allSkills = [];
|
|
16
|
+
for (const collection of config.collections) {
|
|
17
|
+
const col = await backend.readCollection(collection);
|
|
18
|
+
for (const entry of col.skills) {
|
|
19
|
+
allSkills.push({ entry, collection });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const scope = options.scope ?? "global";
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
for (const name of names) {
|
|
25
|
+
const match = allSkills.find((s) => s.entry.name === name);
|
|
26
|
+
if (!match) {
|
|
27
|
+
console.log(chalk.red(`Skill "${name}" not found in any collection.`));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const spinner = ora(`Fetching ${chalk.bold(name)}...`).start();
|
|
31
|
+
try {
|
|
32
|
+
ensureCachePath(match.collection);
|
|
33
|
+
const cachePath = getCachePath(match.collection, name);
|
|
34
|
+
await backend.downloadSkill(match.collection, name, cachePath);
|
|
35
|
+
const { skillsDir, created } = createSymlink(name, cachePath, options.agent, scope, cwd);
|
|
36
|
+
trackSkill(name, match.collection.id, path.join(skillsDir, name));
|
|
37
|
+
spinner.succeed(`${chalk.bold(name)} → ${scope === "project" ? "project" : "global"} ${options.agent} skills`);
|
|
38
|
+
if (created) {
|
|
39
|
+
console.log(chalk.dim(` Created ${skillsDir}`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
spinner.fail(`${chalk.bold(name)}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCommand(): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { writeConfig, mergeCollections, readConfig } from "../config.js";
|
|
4
|
+
import { ensureAuth } from "../auth.js";
|
|
5
|
+
import { GDriveBackend } from "../backends/gdrive.js";
|
|
6
|
+
export async function initCommand() {
|
|
7
|
+
console.log(chalk.bold("\nSkills Manager Init\n"));
|
|
8
|
+
const auth = await ensureAuth();
|
|
9
|
+
console.log(chalk.green(" ✓ Authenticated"));
|
|
10
|
+
const spinner = ora(" Discovering collections...").start();
|
|
11
|
+
const backend = new GDriveBackend(auth);
|
|
12
|
+
const fresh = await backend.discoverCollections();
|
|
13
|
+
let existing = [];
|
|
14
|
+
try {
|
|
15
|
+
existing = readConfig().collections;
|
|
16
|
+
}
|
|
17
|
+
catch { /* no existing config */ }
|
|
18
|
+
const collections = mergeCollections(fresh, existing);
|
|
19
|
+
spinner.stop();
|
|
20
|
+
if (collections.length === 0) {
|
|
21
|
+
console.log(chalk.yellow(" No collections found."));
|
|
22
|
+
console.log(chalk.dim(' Run: skillsmanager collection create <name>'));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.log(chalk.green(` ✓ Found ${collections.length} collection(s):`));
|
|
26
|
+
for (const c of collections) {
|
|
27
|
+
const col = await backend.readCollection(c);
|
|
28
|
+
console.log(` gdrive:${c.name} (${col.skills.length} skills)`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
let existingSkills = {};
|
|
32
|
+
try {
|
|
33
|
+
existingSkills = readConfig().skills ?? {};
|
|
34
|
+
}
|
|
35
|
+
catch { /* ok */ }
|
|
36
|
+
let existingRegistries = [];
|
|
37
|
+
try {
|
|
38
|
+
existingRegistries = readConfig().registries ?? [];
|
|
39
|
+
}
|
|
40
|
+
catch { /* ok */ }
|
|
41
|
+
writeConfig({ registries: existingRegistries, collections, skills: existingSkills, discoveredAt: new Date().toISOString() });
|
|
42
|
+
const totalSkills = (await Promise.all(collections.map((c) => backend.readCollection(c)))).reduce((sum, col) => sum + col.skills.length, 0);
|
|
43
|
+
console.log(`\n${totalSkills} skills across ${collections.length} collection(s).`);
|
|
44
|
+
console.log(`\nRun ${chalk.bold("skillsmanager list")} to browse all available skills.\n`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { AGENT_PATHS } from "../types.js";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const SKILL_SOURCE = path.resolve(__dirname, "..", "..", "skills", "skillsmanager");
|
|
8
|
+
function installToDir(skillsDir, label) {
|
|
9
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
10
|
+
const linkPath = path.join(skillsDir, "skillsmanager");
|
|
11
|
+
if (fs.existsSync(linkPath)) {
|
|
12
|
+
const stat = fs.lstatSync(linkPath);
|
|
13
|
+
if (stat.isSymbolicLink()) {
|
|
14
|
+
fs.unlinkSync(linkPath);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(chalk.yellow(` Skipped ${label} — ${linkPath} exists and is not a symlink`));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
fs.symlinkSync(SKILL_SOURCE, linkPath);
|
|
22
|
+
console.log(chalk.green(` Installed for ${label} → ${linkPath}`));
|
|
23
|
+
}
|
|
24
|
+
export function installCommand(options) {
|
|
25
|
+
if (!fs.existsSync(SKILL_SOURCE)) {
|
|
26
|
+
console.log(chalk.red("Bundled skillsmanager skill not found. Reinstall the package."));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (options.path) {
|
|
30
|
+
const absPath = path.resolve(options.path);
|
|
31
|
+
installToDir(absPath, absPath);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (options.agent) {
|
|
35
|
+
const agents = options.agent.split(",").map((a) => a.trim());
|
|
36
|
+
for (const agent of agents) {
|
|
37
|
+
const skillsDir = AGENT_PATHS[agent];
|
|
38
|
+
if (!skillsDir) {
|
|
39
|
+
console.log(chalk.red(` Unknown agent "${agent}". Supported: ${Object.keys(AGENT_PATHS).join(", ")}`));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
installToDir(skillsDir, agent);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Default: install to all agents
|
|
47
|
+
console.log(chalk.dim("Installing skillsmanager skill to all agent directories...\n"));
|
|
48
|
+
for (const [agent, skillsDir] of Object.entries(AGENT_PATHS)) {
|
|
49
|
+
installToDir(skillsDir, agent);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function uninstallFromDir(skillsDir, label) {
|
|
53
|
+
const linkPath = path.join(skillsDir, "skillsmanager");
|
|
54
|
+
if (!fs.existsSync(linkPath)) {
|
|
55
|
+
console.log(chalk.dim(` ${label} — not installed, skipping`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const stat = fs.lstatSync(linkPath);
|
|
59
|
+
if (!stat.isSymbolicLink()) {
|
|
60
|
+
console.log(chalk.yellow(` ${label} — ${linkPath} is not a symlink, skipping`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
fs.unlinkSync(linkPath);
|
|
64
|
+
console.log(chalk.green(` Removed from ${label} → ${linkPath}`));
|
|
65
|
+
}
|
|
66
|
+
export function uninstallCommand(options) {
|
|
67
|
+
if (options.path) {
|
|
68
|
+
const absPath = path.resolve(options.path);
|
|
69
|
+
uninstallFromDir(absPath, absPath);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (options.agent) {
|
|
73
|
+
const agents = options.agent.split(",").map((a) => a.trim());
|
|
74
|
+
for (const agent of agents) {
|
|
75
|
+
const skillsDir = AGENT_PATHS[agent];
|
|
76
|
+
if (!skillsDir) {
|
|
77
|
+
console.log(chalk.red(` Unknown agent "${agent}". Supported: ${Object.keys(AGENT_PATHS).join(", ")}`));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
uninstallFromDir(skillsDir, agent);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Default: uninstall from all agents
|
|
85
|
+
console.log(chalk.dim("Removing skillsmanager skill from all agent directories...\n"));
|
|
86
|
+
for (const [agent, skillsDir] of Object.entries(AGENT_PATHS)) {
|
|
87
|
+
uninstallFromDir(skillsDir, agent);
|
|
88
|
+
}
|
|
89
|
+
}
|