@skillsmanager/cli 0.0.4 → 0.0.6
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 +10 -18
- package/README.md +93 -36
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +19 -2
- package/dist/backends/gdrive.d.ts +6 -4
- package/dist/backends/gdrive.js +27 -13
- package/dist/backends/github.d.ts +10 -9
- package/dist/backends/github.js +71 -50
- package/dist/backends/interface.d.ts +19 -2
- package/dist/backends/local.d.ts +6 -4
- package/dist/backends/local.js +18 -13
- package/dist/backends/resolve.d.ts +2 -0
- package/dist/backends/resolve.js +25 -4
- package/dist/backends/routing.d.ts +38 -0
- package/dist/backends/routing.js +124 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +130 -26
- package/dist/commands/collection.d.ts +1 -0
- package/dist/commands/collection.js +43 -37
- package/dist/commands/init.js +3 -3
- package/dist/commands/list.js +78 -8
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/refresh.js +1 -1
- package/dist/commands/registry.d.ts +1 -0
- package/dist/commands/registry.js +74 -36
- package/dist/commands/search.js +1 -1
- package/dist/commands/setup/github.d.ts +3 -0
- package/dist/commands/setup/github.js +8 -2
- package/dist/commands/setup/google.js +82 -42
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +76 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +35 -0
- package/dist/config.js +6 -1
- package/dist/index.js +37 -3
- package/dist/registry.js +20 -8
- package/dist/types.d.ts +2 -0
- package/dist/utils/git.d.ts +10 -0
- package/dist/utils/git.js +27 -0
- package/package.json +2 -2
- package/skills/skillsmanager/SKILL.md +109 -6
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
2
|
+
export interface CreateCollectionOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
repo?: string;
|
|
5
|
+
skillsRepo?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CreateRegistryOptions {
|
|
8
|
+
name?: string;
|
|
9
|
+
repo?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BackendStatus {
|
|
12
|
+
loggedIn: boolean;
|
|
13
|
+
identity: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}
|
|
2
16
|
export interface StorageBackend {
|
|
3
17
|
getOwner(): Promise<string>;
|
|
18
|
+
getStatus(): Promise<BackendStatus>;
|
|
4
19
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
5
20
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
6
21
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
7
22
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
8
|
-
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<
|
|
23
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
9
24
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
25
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
10
26
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
11
27
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
12
28
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
13
29
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
14
|
-
createRegistry(
|
|
30
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
31
|
+
createCollection(options: CreateCollectionOptions): Promise<CollectionInfo>;
|
|
15
32
|
}
|
package/dist/backends/local.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageBackend } from "./interface.js";
|
|
1
|
+
import type { BackendStatus, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
2
2
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Local filesystem backend — stores collections and the registry under ~/.skillsmanager/.
|
|
@@ -6,16 +6,18 @@ import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFil
|
|
|
6
6
|
*/
|
|
7
7
|
export declare class LocalBackend implements StorageBackend {
|
|
8
8
|
getOwner(): Promise<string>;
|
|
9
|
+
getStatus(): Promise<BackendStatus>;
|
|
9
10
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
10
11
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
11
12
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
12
13
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
|
-
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<
|
|
14
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
14
15
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
16
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
15
17
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
16
18
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
17
19
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
18
20
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
19
|
-
createRegistry(
|
|
20
|
-
createCollection(name:
|
|
21
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
22
|
+
createCollection({ name }: import("./interface.js").CreateCollectionOptions): Promise<CollectionInfo>;
|
|
21
23
|
}
|
package/dist/backends/local.js
CHANGED
|
@@ -20,6 +20,9 @@ export class LocalBackend {
|
|
|
20
20
|
}
|
|
21
21
|
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
22
22
|
}
|
|
23
|
+
async getStatus() {
|
|
24
|
+
return { loggedIn: true, identity: await this.getOwner() };
|
|
25
|
+
}
|
|
23
26
|
// ── Collection operations ────────────────────────────────────────────────
|
|
24
27
|
async discoverCollections() {
|
|
25
28
|
if (!fs.existsSync(COLLECTIONS_DIR))
|
|
@@ -68,16 +71,22 @@ export class LocalBackend {
|
|
|
68
71
|
}
|
|
69
72
|
async uploadSkill(collection, localPath, skillName) {
|
|
70
73
|
const dest = path.join(collection.folderId, skillName);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
if (path.resolve(localPath) !== path.resolve(dest)) {
|
|
75
|
+
copyDirSync(localPath, dest);
|
|
76
|
+
}
|
|
77
|
+
return `${skillName}/`;
|
|
75
78
|
}
|
|
76
79
|
async deleteCollection(collection) {
|
|
77
80
|
if (fs.existsSync(collection.folderId)) {
|
|
78
81
|
fs.rmSync(collection.folderId, { recursive: true, force: true });
|
|
79
82
|
}
|
|
80
83
|
}
|
|
84
|
+
async deleteSkill(collection, skillName) {
|
|
85
|
+
const skillPath = path.join(collection.folderId, skillName);
|
|
86
|
+
if (fs.existsSync(skillPath)) {
|
|
87
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
81
90
|
// ── Registry operations ──────────────────────────────────────────────────
|
|
82
91
|
async discoverRegistries() {
|
|
83
92
|
if (!fs.existsSync(LOCAL_REGISTRY_PATH))
|
|
@@ -115,26 +124,22 @@ export class LocalBackend {
|
|
|
115
124
|
folderId: dir,
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
|
-
async createRegistry(
|
|
127
|
+
async createRegistry(options) {
|
|
128
|
+
const name = options?.name ?? "local";
|
|
119
129
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
120
130
|
const owner = await this.getOwner();
|
|
121
|
-
const data = {
|
|
122
|
-
name: name ?? "local",
|
|
123
|
-
owner,
|
|
124
|
-
source: "local",
|
|
125
|
-
collections: [],
|
|
126
|
-
};
|
|
131
|
+
const data = { name, owner, source: "local", collections: [] };
|
|
127
132
|
fs.writeFileSync(LOCAL_REGISTRY_PATH, serializeRegistryFile(data));
|
|
128
133
|
return {
|
|
129
134
|
id: randomUUID(),
|
|
130
|
-
name
|
|
135
|
+
name,
|
|
131
136
|
backend: "local",
|
|
132
137
|
folderId: CONFIG_DIR,
|
|
133
138
|
fileId: LOCAL_REGISTRY_PATH,
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
// ── Convenience: create a local collection ───────────────────────────────
|
|
137
|
-
async createCollection(name) {
|
|
142
|
+
async createCollection({ name }) {
|
|
138
143
|
const dir = path.join(COLLECTIONS_DIR, name);
|
|
139
144
|
fs.mkdirSync(dir, { recursive: true });
|
|
140
145
|
const owner = await this.getOwner();
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { StorageBackend } from "./interface.js";
|
|
2
2
|
export declare function resolveBackend(backendName: string): Promise<StorageBackend>;
|
|
3
|
+
/** Like resolveBackend but never triggers auth flows — returns null for unconfigured backends. */
|
|
4
|
+
export declare function tryResolveBackend(backendName: string): Promise<StorageBackend | null>;
|
package/dist/backends/resolve.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
import { ensureAuth } from "../auth.js";
|
|
1
|
+
import { ensureAuth, getAuthClient, hasToken } from "../auth.js";
|
|
2
|
+
import { credentialsExist } from "../config.js";
|
|
2
3
|
import { GDriveBackend } from "./gdrive.js";
|
|
3
4
|
import { GithubBackend } from "./github.js";
|
|
4
5
|
import { LocalBackend } from "./local.js";
|
|
6
|
+
import { RoutingBackend } from "./routing.js";
|
|
5
7
|
export async function resolveBackend(backendName) {
|
|
8
|
+
let inner;
|
|
6
9
|
if (backendName === "gdrive")
|
|
7
|
-
|
|
10
|
+
inner = new GDriveBackend(await ensureAuth());
|
|
11
|
+
else if (backendName === "github")
|
|
12
|
+
inner = new GithubBackend();
|
|
13
|
+
else
|
|
14
|
+
inner = new LocalBackend();
|
|
15
|
+
return new RoutingBackend(inner);
|
|
16
|
+
}
|
|
17
|
+
/** Like resolveBackend but never triggers auth flows — returns null for unconfigured backends. */
|
|
18
|
+
export async function tryResolveBackend(backendName) {
|
|
19
|
+
if (backendName === "gdrive") {
|
|
20
|
+
if (!credentialsExist() || !hasToken())
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
return new RoutingBackend(new GDriveBackend(getAuthClient()));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
if (backendName === "github")
|
|
9
|
-
return new GithubBackend();
|
|
10
|
-
return new LocalBackend();
|
|
30
|
+
return new RoutingBackend(new GithubBackend());
|
|
31
|
+
return new RoutingBackend(new LocalBackend());
|
|
11
32
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BackendStatus, CreateCollectionOptions, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
2
|
+
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* RoutingBackend — a transparent decorator over any StorageBackend.
|
|
5
|
+
*
|
|
6
|
+
* Collection-metadata operations (readCollection, writeCollection, registry ops, etc.)
|
|
7
|
+
* pass straight through to the inner backend — the YAML always lives where the
|
|
8
|
+
* collection was declared.
|
|
9
|
+
*
|
|
10
|
+
* Skill-file operations (downloadSkill, uploadSkill, deleteSkill) inspect col.type
|
|
11
|
+
* and dispatch to the appropriate handler:
|
|
12
|
+
* - col.type === "github" → GithubBackend helpers (clone/pull the skills repo)
|
|
13
|
+
* - col.type absent/same → inner backend (normal behaviour, no change)
|
|
14
|
+
*
|
|
15
|
+
* This means every backend gets cross-backend routing for free without any
|
|
16
|
+
* per-backend col.type checks.
|
|
17
|
+
*/
|
|
18
|
+
export declare class RoutingBackend implements StorageBackend {
|
|
19
|
+
private readonly inner;
|
|
20
|
+
constructor(inner: StorageBackend);
|
|
21
|
+
getOwner(): Promise<string>;
|
|
22
|
+
getStatus(): Promise<BackendStatus>;
|
|
23
|
+
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
24
|
+
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
25
|
+
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
26
|
+
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
27
|
+
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
28
|
+
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
29
|
+
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
30
|
+
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
31
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
32
|
+
createCollection(options: CreateCollectionOptions): Promise<CollectionInfo>;
|
|
33
|
+
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
34
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
35
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
36
|
+
private requireRepo;
|
|
37
|
+
private requireEntry;
|
|
38
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { GithubBackend } from "./github.js";
|
|
2
|
+
/**
|
|
3
|
+
* RoutingBackend — a transparent decorator over any StorageBackend.
|
|
4
|
+
*
|
|
5
|
+
* Collection-metadata operations (readCollection, writeCollection, registry ops, etc.)
|
|
6
|
+
* pass straight through to the inner backend — the YAML always lives where the
|
|
7
|
+
* collection was declared.
|
|
8
|
+
*
|
|
9
|
+
* Skill-file operations (downloadSkill, uploadSkill, deleteSkill) inspect col.type
|
|
10
|
+
* and dispatch to the appropriate handler:
|
|
11
|
+
* - col.type === "github" → GithubBackend helpers (clone/pull the skills repo)
|
|
12
|
+
* - col.type absent/same → inner backend (normal behaviour, no change)
|
|
13
|
+
*
|
|
14
|
+
* This means every backend gets cross-backend routing for free without any
|
|
15
|
+
* per-backend col.type checks.
|
|
16
|
+
*/
|
|
17
|
+
export class RoutingBackend {
|
|
18
|
+
inner;
|
|
19
|
+
constructor(inner) {
|
|
20
|
+
this.inner = inner;
|
|
21
|
+
}
|
|
22
|
+
// ── Pass-through: identity + collection metadata ───────────────────────────
|
|
23
|
+
getOwner() {
|
|
24
|
+
return this.inner.getOwner();
|
|
25
|
+
}
|
|
26
|
+
getStatus() {
|
|
27
|
+
return this.inner.getStatus();
|
|
28
|
+
}
|
|
29
|
+
discoverCollections() {
|
|
30
|
+
return this.inner.discoverCollections();
|
|
31
|
+
}
|
|
32
|
+
readCollection(collection) {
|
|
33
|
+
return this.inner.readCollection(collection);
|
|
34
|
+
}
|
|
35
|
+
writeCollection(collection, data) {
|
|
36
|
+
return this.inner.writeCollection(collection, data);
|
|
37
|
+
}
|
|
38
|
+
deleteCollection(collection) {
|
|
39
|
+
return this.inner.deleteCollection(collection);
|
|
40
|
+
}
|
|
41
|
+
// ── Pass-through: registry operations ─────────────────────────────────────
|
|
42
|
+
discoverRegistries() {
|
|
43
|
+
return this.inner.discoverRegistries();
|
|
44
|
+
}
|
|
45
|
+
readRegistry(registry) {
|
|
46
|
+
return this.inner.readRegistry(registry);
|
|
47
|
+
}
|
|
48
|
+
writeRegistry(registry, data) {
|
|
49
|
+
return this.inner.writeRegistry(registry, data);
|
|
50
|
+
}
|
|
51
|
+
resolveCollectionRef(ref) {
|
|
52
|
+
return this.inner.resolveCollectionRef(ref);
|
|
53
|
+
}
|
|
54
|
+
createRegistry(options) {
|
|
55
|
+
return this.inner.createRegistry(options);
|
|
56
|
+
}
|
|
57
|
+
createCollection(options) {
|
|
58
|
+
return this.inner.createCollection(options);
|
|
59
|
+
}
|
|
60
|
+
// ── Routed: dispatch on col.type for skill-file operations ─────────────────
|
|
61
|
+
async downloadSkill(collection, skillName, destDir) {
|
|
62
|
+
const col = await this.inner.readCollection(collection);
|
|
63
|
+
const skillType = col.type ?? collection.backend;
|
|
64
|
+
// Only cross-dispatch when the skill source differs from the collection's own backend.
|
|
65
|
+
// Same-backend collections (e.g. GitHub-native) handle routing internally.
|
|
66
|
+
if (skillType === "github" && collection.backend !== "github") {
|
|
67
|
+
const repo = this.requireRepo(col, collection.name);
|
|
68
|
+
const entry = this.requireEntry(col, skillName, collection.name);
|
|
69
|
+
await new GithubBackend().downloadSkillFromRepo(repo, entry.path, destDir);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
return this.inner.downloadSkill(collection, skillName, destDir);
|
|
73
|
+
}
|
|
74
|
+
async uploadSkill(collection, localPath, skillName) {
|
|
75
|
+
const col = await this.inner.readCollection(collection);
|
|
76
|
+
const skillType = col.type ?? collection.backend;
|
|
77
|
+
// Case 1: collection YAML in one backend, skills declared in another (col.type set)
|
|
78
|
+
if (skillType !== collection.backend) {
|
|
79
|
+
throw new Error(`Cannot upload skill to collection "${collection.name}": ` +
|
|
80
|
+
`its skills source type is "${skillType}". ` +
|
|
81
|
+
`Use --remote-path to register a skill path instead.`);
|
|
82
|
+
}
|
|
83
|
+
// Case 2: GitHub-native collection whose metadata.repo points to a foreign repo
|
|
84
|
+
if (skillType === "github" && col.metadata?.repo) {
|
|
85
|
+
const hostRepo = collection.folderId.split(":")[0];
|
|
86
|
+
const foreign = col.metadata.repo;
|
|
87
|
+
if (foreign !== hostRepo) {
|
|
88
|
+
throw new Error(`Cannot upload skill to collection "${collection.name}": ` +
|
|
89
|
+
`its skills source is "${foreign}" (a repo you may not own). ` +
|
|
90
|
+
`Use --remote-path to register a skill path instead.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return this.inner.uploadSkill(collection, localPath, skillName);
|
|
94
|
+
}
|
|
95
|
+
async deleteSkill(collection, skillName) {
|
|
96
|
+
const col = await this.inner.readCollection(collection);
|
|
97
|
+
const skillType = col.type ?? collection.backend;
|
|
98
|
+
// Only cross-dispatch when the skill source differs from the collection's own backend.
|
|
99
|
+
if (skillType === "github" && collection.backend !== "github") {
|
|
100
|
+
const repo = this.requireRepo(col, collection.name);
|
|
101
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
102
|
+
if (!entry)
|
|
103
|
+
return;
|
|
104
|
+
await new GithubBackend().deleteSkillFromRepo(repo, entry.path);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
return this.inner.deleteSkill(collection, skillName);
|
|
108
|
+
}
|
|
109
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
110
|
+
requireRepo(col, collectionName) {
|
|
111
|
+
const repo = col.metadata?.repo;
|
|
112
|
+
if (!repo) {
|
|
113
|
+
throw new Error(`Collection "${collectionName}" has type "github" but is missing metadata.repo`);
|
|
114
|
+
}
|
|
115
|
+
return repo;
|
|
116
|
+
}
|
|
117
|
+
requireEntry(col, skillName, collectionName) {
|
|
118
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
119
|
+
if (!entry) {
|
|
120
|
+
throw new Error(`Skill "${skillName}" not found in collection "${collectionName}"`);
|
|
121
|
+
}
|
|
122
|
+
return entry;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/add.js
CHANGED
|
@@ -4,9 +4,14 @@ import chalk from "chalk";
|
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import YAML from "yaml";
|
|
6
6
|
import { readConfig, trackSkill } from "../config.js";
|
|
7
|
-
import {
|
|
7
|
+
import { detectRepoContext } from "../utils/git.js";
|
|
8
8
|
import { resolveBackend } from "../backends/resolve.js";
|
|
9
9
|
export async function addCommand(skillPath, options) {
|
|
10
|
+
// ── Remote-path mode: register a skill from a foreign repo without local files ─
|
|
11
|
+
if (options.remotePath) {
|
|
12
|
+
await addRemotePath(options);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
10
15
|
const absPath = path.resolve(skillPath);
|
|
11
16
|
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
|
|
12
17
|
console.log(chalk.red(`"${skillPath}" is not a valid directory.`));
|
|
@@ -34,7 +39,7 @@ export async function addCommand(skillPath, options) {
|
|
|
34
39
|
// If no --collection specified and a matching GitHub collection exists in config,
|
|
35
40
|
// use it automatically (no prompt — agent-friendly).
|
|
36
41
|
if (!options.collection) {
|
|
37
|
-
const ctx =
|
|
42
|
+
const ctx = detectRepoContext(absPath);
|
|
38
43
|
if (ctx) {
|
|
39
44
|
let config;
|
|
40
45
|
try {
|
|
@@ -75,21 +80,136 @@ export async function addCommand(skillPath, options) {
|
|
|
75
80
|
console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
|
|
76
81
|
return;
|
|
77
82
|
}
|
|
83
|
+
// If the collection's skill type differs from its hosting backend, handle specially.
|
|
84
|
+
// col.type declares who handles skill files; col.metadata.repo gives the target repo.
|
|
85
|
+
{
|
|
86
|
+
const hostBackend = await resolveBackend(collection.backend);
|
|
87
|
+
const col = await hostBackend.readCollection(collection);
|
|
88
|
+
const skillType = col.type ?? collection.backend;
|
|
89
|
+
const foreignRepo = col.metadata?.repo;
|
|
90
|
+
if (skillType !== collection.backend && skillType === "github" && foreignRepo) {
|
|
91
|
+
const ctx = detectRepoContext(absPath);
|
|
92
|
+
if (!ctx || ctx.repo !== foreignRepo) {
|
|
93
|
+
console.log(chalk.red(`This collection's skills source is "${foreignRepo}" (type: ${skillType}). ` +
|
|
94
|
+
`The provided path does not belong to that repo.\n` +
|
|
95
|
+
chalk.dim(` To register a skill by path without a local clone, use:\n`) +
|
|
96
|
+
chalk.dim(` skillsmanager add --collection ${collection.name} --remote-path <rel/path> --name <name> --description <desc>`)));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Path is from the foreign repo (cloned locally) — register relative path only, no upload
|
|
100
|
+
const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
|
|
101
|
+
try {
|
|
102
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
103
|
+
const entry = { name: skillName, path: ctx.relPath, description };
|
|
104
|
+
if (existing >= 0) {
|
|
105
|
+
col.skills[existing] = entry;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
col.skills.push(entry);
|
|
109
|
+
}
|
|
110
|
+
await hostBackend.writeCollection(collection, col);
|
|
111
|
+
trackSkill(skillName, collection.id, absPath);
|
|
112
|
+
spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(ctx.relPath)}`);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
78
120
|
const backend = await resolveBackend(collection.backend);
|
|
79
121
|
await uploadToCollection(backend, collection, absPath, skillName, description);
|
|
80
122
|
}
|
|
123
|
+
// ── Remote-path mode: register a skill entry without local files ─────────────
|
|
124
|
+
async function addRemotePath(options) {
|
|
125
|
+
const { remotePath, name: skillName, description = "", collection: collectionName } = options;
|
|
126
|
+
if (!remotePath) {
|
|
127
|
+
console.log(chalk.red("--remote-path requires a relative path (e.g. tools/my-skill/)"));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!skillName) {
|
|
131
|
+
console.log(chalk.red("--remote-path requires --name <skill-name>"));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let config;
|
|
135
|
+
try {
|
|
136
|
+
config = readConfig();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
console.log(chalk.red("No config found. Run: skillsmanager collection create"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let collection = config.collections[0];
|
|
143
|
+
if (collectionName) {
|
|
144
|
+
const found = config.collections.find((c) => c.name === collectionName);
|
|
145
|
+
if (!found) {
|
|
146
|
+
console.log(chalk.red(`Collection "${collectionName}" not found.`));
|
|
147
|
+
console.log(chalk.dim(` Available: ${config.collections.map((c) => c.name).join(", ")}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
collection = found;
|
|
151
|
+
}
|
|
152
|
+
if (!collection) {
|
|
153
|
+
console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const hostBackend = await resolveBackend(collection.backend);
|
|
157
|
+
const colForType = await hostBackend.readCollection(collection);
|
|
158
|
+
const skillType = colForType.type ?? collection.backend;
|
|
159
|
+
if (skillType !== "github" && collection.backend !== "github") {
|
|
160
|
+
console.log(chalk.red(`--remote-path is only supported for collections with type "github".`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const spinner = ora(`Registering ${chalk.bold(skillName)} in ${collection.name} at ${chalk.dim(remotePath)}...`).start();
|
|
164
|
+
try {
|
|
165
|
+
const col = colForType;
|
|
166
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
167
|
+
const entry = { name: skillName, path: remotePath, description };
|
|
168
|
+
if (existing >= 0) {
|
|
169
|
+
col.skills[existing] = entry;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
col.skills.push(entry);
|
|
173
|
+
}
|
|
174
|
+
await hostBackend.writeCollection(collection, col);
|
|
175
|
+
spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(remotePath)}`);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
81
181
|
// ── GitHub path: register in-repo skill or copy external skill ────────────────
|
|
82
182
|
async function addToGithub(absPath, ctx, skillName, description, collection) {
|
|
83
|
-
const github =
|
|
183
|
+
const github = await resolveBackend("github");
|
|
84
184
|
const spinner = ora(`Adding ${chalk.bold(skillName)} to github:${collection.folderId}...`).start();
|
|
85
185
|
try {
|
|
86
|
-
// uploadSkill is a no-op for in-repo skills; copies if external
|
|
87
|
-
await github.uploadSkill(collection, absPath, skillName);
|
|
88
|
-
// Determine effective skill path in the repo
|
|
89
|
-
const skillEntry = absPath.startsWith(ctx.repoRoot)
|
|
90
|
-
? ctx.relPath // in-repo: use relative path
|
|
91
|
-
: `.agentskills/${skillName}`; // external: was copied here by uploadSkill
|
|
92
186
|
const col = await github.readCollection(collection);
|
|
187
|
+
const foreignRepo = col.metadata?.repo;
|
|
188
|
+
const hostRepo = collection.folderId.split(":")[0];
|
|
189
|
+
// If collection has metadata.repo pointing to a foreign repo, validate that
|
|
190
|
+
// the local skill belongs to that foreign repo (not the collection host repo).
|
|
191
|
+
if (foreignRepo && foreignRepo !== hostRepo) {
|
|
192
|
+
if (ctx.repo !== foreignRepo) {
|
|
193
|
+
spinner.fail(`This collection's skills source is "${foreignRepo}" but the provided path belongs to "${ctx.repo}".`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Skill is in the foreign repo (cloned locally) — register path only, no upload
|
|
197
|
+
const entry = { name: skillName, path: ctx.relPath, description };
|
|
198
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
199
|
+
if (existing >= 0) {
|
|
200
|
+
col.skills[existing] = entry;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
col.skills.push(entry);
|
|
204
|
+
}
|
|
205
|
+
await github.writeCollection(collection, col);
|
|
206
|
+
trackSkill(skillName, collection.id, absPath);
|
|
207
|
+
spinner.succeed(`${chalk.bold(skillName)} registered in github:${collection.folderId} at ${chalk.dim(ctx.relPath)}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Standard case: skill is in (or being added to) the collection's host repo
|
|
211
|
+
// uploadSkill returns the canonical in-repo path
|
|
212
|
+
const skillEntry = await github.uploadSkill(collection, absPath, skillName);
|
|
93
213
|
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
94
214
|
if (existing >= 0) {
|
|
95
215
|
col.skills[existing] = { name: skillName, path: skillEntry, description };
|
|
@@ -109,23 +229,7 @@ async function addToGithub(absPath, ctx, skillName, description, collection) {
|
|
|
109
229
|
async function uploadToCollection(backend, collection, absPath, skillName, description) {
|
|
110
230
|
const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
|
|
111
231
|
try {
|
|
112
|
-
await backend.uploadSkill(collection, absPath, skillName);
|
|
113
|
-
// For GitHub collections, determine the effective in-repo path
|
|
114
|
-
let skillPath;
|
|
115
|
-
if (collection.backend === "github") {
|
|
116
|
-
// If the skill is already inside the repo workdir, use its relative path
|
|
117
|
-
const ctx = GithubBackend.detectRepoContext(absPath);
|
|
118
|
-
const repoFromCollection = collection.folderId.split(":")[0];
|
|
119
|
-
if (ctx && ctx.repo === repoFromCollection) {
|
|
120
|
-
skillPath = ctx.relPath; // e.g. "src/my-inrepo-skill"
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
skillPath = `.agentskills/${skillName}`; // external → copied here by uploadSkill
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
skillPath = `${skillName}/`;
|
|
128
|
-
}
|
|
232
|
+
const skillPath = await backend.uploadSkill(collection, absPath, skillName);
|
|
129
233
|
const col = await backend.readCollection(collection);
|
|
130
234
|
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
131
235
|
if (existing >= 0) {
|
|
@@ -2,51 +2,28 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { writeConfig, CONFIG_PATH, readConfig } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
-
import { GDriveBackend } from "../backends/gdrive.js";
|
|
7
|
-
import { GithubBackend } from "../backends/github.js";
|
|
5
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
8
6
|
export async function collectionCreateCommand(name, options = {}) {
|
|
9
7
|
const backendName = options.backend ?? "gdrive";
|
|
10
|
-
if (backendName === "github") {
|
|
11
|
-
await createGithubCollection(name, options.repo);
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
await createGdriveCollection(name);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
async function createGithubCollection(name, repo) {
|
|
18
|
-
if (!repo) {
|
|
8
|
+
if (backendName === "github" && !options.repo) {
|
|
19
9
|
console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
|
|
20
10
|
console.log(chalk.dim(" Example: skillsmanager collection create my-skills --backend github --repo owner/my-repo"));
|
|
21
11
|
return;
|
|
22
12
|
}
|
|
23
|
-
const collectionName = name ?? "default";
|
|
24
|
-
const
|
|
25
|
-
console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo}...\n`));
|
|
13
|
+
const collectionName = name ?? (backendName === "gdrive" ? "MY_SKILLS" : "default");
|
|
14
|
+
const spinner = ora(`Creating collection "${collectionName}" in ${backendName}...`).start();
|
|
26
15
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
catch (err) {
|
|
35
|
-
console.log(chalk.red(`Failed: ${err.message}`));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function createGdriveCollection(name) {
|
|
39
|
-
const auth = await ensureAuth();
|
|
40
|
-
const backend = new GDriveBackend(auth);
|
|
41
|
-
const PREFIX = "SKILLS_";
|
|
42
|
-
const folderName = !name
|
|
43
|
-
? `${PREFIX}MY_SKILLS`
|
|
44
|
-
: name.startsWith(PREFIX) ? name : `${PREFIX}${name}`;
|
|
45
|
-
const spinner = ora(`Creating collection "${folderName}" in Google Drive...`).start();
|
|
46
|
-
try {
|
|
47
|
-
const collection = await backend.createCollection(folderName);
|
|
48
|
-
spinner.succeed(`Collection "${folderName}" created in Google Drive`);
|
|
16
|
+
const backend = await resolveBackend(backendName);
|
|
17
|
+
const collection = await backend.createCollection({
|
|
18
|
+
name: collectionName,
|
|
19
|
+
repo: options.repo,
|
|
20
|
+
skillsRepo: options.skillsRepo,
|
|
21
|
+
});
|
|
22
|
+
spinner.succeed(`Collection "${collection.name}" created (${backendName}:${collection.folderId})`);
|
|
49
23
|
const config = loadOrDefaultConfig();
|
|
24
|
+
const registry = await ensureRegistry(config);
|
|
25
|
+
await registerCollectionInRegistry(registry, collection, config);
|
|
26
|
+
collection.sourceRegistryId = registry.id;
|
|
50
27
|
upsertCollection(config, collection);
|
|
51
28
|
writeConfig(config);
|
|
52
29
|
console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
|
|
@@ -74,3 +51,32 @@ function upsertCollection(config, collection) {
|
|
|
74
51
|
config.collections.push(collection);
|
|
75
52
|
}
|
|
76
53
|
}
|
|
54
|
+
/** Returns the first registry in config, auto-creating a local one if none exists. */
|
|
55
|
+
async function ensureRegistry(config) {
|
|
56
|
+
if (config.registries.length > 0)
|
|
57
|
+
return config.registries[0];
|
|
58
|
+
console.log(chalk.dim(" No registry found — creating a local registry..."));
|
|
59
|
+
const local = await resolveBackend("local");
|
|
60
|
+
const registry = await local.createRegistry();
|
|
61
|
+
config.registries.push(registry);
|
|
62
|
+
console.log(chalk.green(" ✓ Local registry created"));
|
|
63
|
+
return registry;
|
|
64
|
+
}
|
|
65
|
+
/** Registers the collection ref in the given registry (writes directly to the registry's backend). */
|
|
66
|
+
async function registerCollectionInRegistry(registry, collection, config) {
|
|
67
|
+
const backend = await resolveBackend(registry.backend);
|
|
68
|
+
const registryData = await backend.readRegistry(registry);
|
|
69
|
+
if (registryData.collections.find((c) => c.name === collection.name))
|
|
70
|
+
return;
|
|
71
|
+
registryData.collections.push({
|
|
72
|
+
name: collection.name,
|
|
73
|
+
backend: collection.backend,
|
|
74
|
+
ref: collection.folderId,
|
|
75
|
+
});
|
|
76
|
+
await backend.writeRegistry(registry, registryData);
|
|
77
|
+
// Keep local config registry list in sync
|
|
78
|
+
if (!config.registries.find((r) => r.id === registry.id)) {
|
|
79
|
+
config.registries.push(registry);
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.dim(` Registered in registry "${registry.name}" (${registry.backend})`));
|
|
82
|
+
}
|