@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,281 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import readline from "readline";
|
|
5
|
+
import { execSync, spawnSync } from "child_process";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { CREDENTIALS_PATH, ensureConfigDir } from "../../config.js";
|
|
8
|
+
import { runAuthFlow, hasToken } from "../../auth.js";
|
|
9
|
+
import { credentialsExist } from "../../config.js";
|
|
10
|
+
// ─── Prompt helpers ──────────────────────────────────────────────────────────
|
|
11
|
+
function ask(question) {
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async function confirm(question) {
|
|
18
|
+
const ans = await ask(`${question} ${chalk.dim("[y/n]")} `);
|
|
19
|
+
return ans.toLowerCase().startsWith("y");
|
|
20
|
+
}
|
|
21
|
+
// ─── gcloud helpers ──────────────────────────────────────────────────────────
|
|
22
|
+
function gcloudInstalled() {
|
|
23
|
+
const r = spawnSync("gcloud", ["version"], { stdio: "pipe" });
|
|
24
|
+
return r.status === 0;
|
|
25
|
+
}
|
|
26
|
+
function gcloudExec(args, opts) {
|
|
27
|
+
const r = spawnSync("gcloud", args, {
|
|
28
|
+
stdio: opts?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"],
|
|
29
|
+
input: opts?.input,
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
});
|
|
32
|
+
return { ok: r.status === 0, stdout: (r.stdout ?? "").trim(), stderr: (r.stderr ?? "").trim() };
|
|
33
|
+
}
|
|
34
|
+
function getActiveGcloudAccount() {
|
|
35
|
+
const r = gcloudExec(["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"]);
|
|
36
|
+
return r.ok && r.stdout ? r.stdout.split("\n")[0].trim() : null;
|
|
37
|
+
}
|
|
38
|
+
function listProjects() {
|
|
39
|
+
const r = gcloudExec(["projects", "list", "--format=value(projectId,name)"]);
|
|
40
|
+
if (!r.ok || !r.stdout)
|
|
41
|
+
return [];
|
|
42
|
+
return r.stdout
|
|
43
|
+
.split("\n")
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.map((line) => {
|
|
46
|
+
const [projectId, ...nameParts] = line.split(/\s+/);
|
|
47
|
+
return { projectId, name: nameParts.join(" ") || projectId };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function apiEnabled(projectId, api) {
|
|
51
|
+
const r = gcloudExec([
|
|
52
|
+
"services", "list",
|
|
53
|
+
`--project=${projectId}`,
|
|
54
|
+
"--enabled",
|
|
55
|
+
`--filter=config.name:${api}`,
|
|
56
|
+
"--format=value(config.name)",
|
|
57
|
+
]);
|
|
58
|
+
return r.ok && r.stdout.includes(api);
|
|
59
|
+
}
|
|
60
|
+
function enableApi(projectId, api) {
|
|
61
|
+
console.log(chalk.dim(` Enabling ${api}...`));
|
|
62
|
+
const r = gcloudExec(["services", "enable", api, `--project=${projectId}`]);
|
|
63
|
+
return r.ok;
|
|
64
|
+
}
|
|
65
|
+
// ─── Install gcloud ───────────────────────────────────────────────────────────
|
|
66
|
+
async function installGcloud() {
|
|
67
|
+
const isMac = process.platform === "darwin";
|
|
68
|
+
if (!isMac) {
|
|
69
|
+
console.log(chalk.yellow(" Auto-install is only supported on macOS."));
|
|
70
|
+
console.log(chalk.dim(" Install gcloud manually: https://cloud.google.com/sdk/docs/install"));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
// Check homebrew
|
|
74
|
+
const brewCheck = spawnSync("brew", ["--version"], { stdio: "pipe" });
|
|
75
|
+
if (brewCheck.status !== 0) {
|
|
76
|
+
console.log(chalk.yellow(" Homebrew not found. Install it from https://brew.sh then re-run."));
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.dim(" Installing google-cloud-sdk via Homebrew (this may take a minute)..."));
|
|
80
|
+
const r = spawnSync("brew", ["install", "--cask", "google-cloud-sdk"], { stdio: "inherit" });
|
|
81
|
+
if (r.status !== 0) {
|
|
82
|
+
console.log(chalk.red(" Install failed. Try manually: brew install --cask google-cloud-sdk"));
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
// After cask install, gcloud lives in /usr/local/Caskroom path; add to PATH hint
|
|
86
|
+
console.log(chalk.dim(" You may need to open a new terminal or source your shell profile for gcloud to be in PATH."));
|
|
87
|
+
console.log(chalk.dim(" Trying to continue..."));
|
|
88
|
+
return gcloudInstalled();
|
|
89
|
+
}
|
|
90
|
+
// ─── Project selection ────────────────────────────────────────────────────────
|
|
91
|
+
async function selectOrCreateProject() {
|
|
92
|
+
const projects = listProjects();
|
|
93
|
+
if (projects.length > 0) {
|
|
94
|
+
console.log(chalk.bold("\nExisting projects:"));
|
|
95
|
+
projects.forEach((p, i) => console.log(` ${chalk.cyan(i + 1)}. ${p.name} ${chalk.dim(`(${p.projectId})`)}`));
|
|
96
|
+
console.log(` ${chalk.cyan(projects.length + 1)}. Create a new project`);
|
|
97
|
+
const ans = await ask(`\nSelect a project [1-${projects.length + 1}]: `);
|
|
98
|
+
const idx = parseInt(ans, 10);
|
|
99
|
+
if (idx >= 1 && idx <= projects.length) {
|
|
100
|
+
return projects[idx - 1].projectId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Create new
|
|
104
|
+
const rawName = await ask(`\nProject name ${chalk.dim('(leave blank for "Skills Manager")')}: `);
|
|
105
|
+
const name = rawName || "Skills Manager";
|
|
106
|
+
const projectId = name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")
|
|
107
|
+
+ "-" + Date.now().toString().slice(-6);
|
|
108
|
+
console.log(chalk.dim(`\n Creating project ${projectId}...`));
|
|
109
|
+
const r = gcloudExec(["projects", "create", projectId, `--name=${name}`]);
|
|
110
|
+
if (!r.ok) {
|
|
111
|
+
console.log(chalk.red(` Failed to create project: ${r.stderr}`));
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
console.log(chalk.green(` ✓ Project "${name}" created (${projectId})`));
|
|
115
|
+
return projectId;
|
|
116
|
+
}
|
|
117
|
+
// ─── Open browser helper ──────────────────────────────────────────────────────
|
|
118
|
+
function openUrl(url) {
|
|
119
|
+
const cmd = process.platform === "darwin" ? "open" :
|
|
120
|
+
process.platform === "win32" ? "start" : "xdg-open";
|
|
121
|
+
try {
|
|
122
|
+
execSync(`${cmd} "${url}"`, { stdio: "ignore" });
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore */ }
|
|
125
|
+
}
|
|
126
|
+
// ─── Main command ─────────────────────────────────────────────────────────────
|
|
127
|
+
export async function setupGoogleCommand() {
|
|
128
|
+
console.log(chalk.bold("\nSkills Manager — Google Drive Setup\n"));
|
|
129
|
+
// ── Case 1: credentials.json already present ──────────────────────────────
|
|
130
|
+
if (credentialsExist()) {
|
|
131
|
+
console.log(chalk.green(" ✓ credentials.json found"));
|
|
132
|
+
if (hasToken()) {
|
|
133
|
+
console.log(chalk.green(" ✓ Already authenticated — nothing to do."));
|
|
134
|
+
console.log(`\nRun ${chalk.bold("skillsmanager init")} to discover registries.\n`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.yellow(" ✗ Not yet authenticated — starting OAuth flow...\n"));
|
|
138
|
+
await runAuthFlow();
|
|
139
|
+
console.log(chalk.green("\n ✓ Authenticated successfully."));
|
|
140
|
+
console.log(`\nRun ${chalk.bold("skillsmanager init")} to discover registries.\n`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// ── Case 2: No credentials.json ───────────────────────────────────────────
|
|
144
|
+
console.log(chalk.yellow(" ✗ No credentials.json found at ~/.skillsmanager/credentials.json\n"));
|
|
145
|
+
const wantHelp = await confirm("Would you like Skills Manager to help you set up a Google Cloud project?");
|
|
146
|
+
if (!wantHelp) {
|
|
147
|
+
printManualInstructions();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// ── Automated setup ───────────────────────────────────────────────────────
|
|
151
|
+
console.log(chalk.bold("\nStep 1 — gcloud CLI\n"));
|
|
152
|
+
if (!gcloudInstalled()) {
|
|
153
|
+
console.log(chalk.yellow(" gcloud CLI is not installed."));
|
|
154
|
+
const install = await confirm(" Install it now via Homebrew?");
|
|
155
|
+
if (!install) {
|
|
156
|
+
console.log(chalk.dim(" Install manually: https://cloud.google.com/sdk/docs/install"));
|
|
157
|
+
console.log(chalk.dim(" Then re-run: skillsmanager setup google"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const ok = await installGcloud();
|
|
161
|
+
if (!ok)
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.log(chalk.green(" ✓ gcloud is installed"));
|
|
166
|
+
}
|
|
167
|
+
// ── gcloud auth ───────────────────────────────────────────────────────────
|
|
168
|
+
console.log(chalk.bold("\nStep 2 — Google Account\n"));
|
|
169
|
+
let account = getActiveGcloudAccount();
|
|
170
|
+
if (account) {
|
|
171
|
+
console.log(chalk.green(` ✓ Signed in as ${account}`));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(chalk.dim(" Opening browser for gcloud login..."));
|
|
175
|
+
const r = spawnSync("gcloud", ["auth", "login"], { stdio: "inherit" });
|
|
176
|
+
if (r.status !== 0) {
|
|
177
|
+
console.log(chalk.red(" gcloud auth login failed. Please try manually."));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
account = getActiveGcloudAccount();
|
|
181
|
+
if (!account) {
|
|
182
|
+
console.log(chalk.red(" Could not determine active account after login."));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
console.log(chalk.green(` ✓ Signed in as ${account}`));
|
|
186
|
+
}
|
|
187
|
+
// ── Project ───────────────────────────────────────────────────────────────
|
|
188
|
+
console.log(chalk.bold("\nStep 3 — Google Cloud Project\n"));
|
|
189
|
+
const projectId = await selectOrCreateProject();
|
|
190
|
+
if (!projectId)
|
|
191
|
+
return;
|
|
192
|
+
// ── Enable Drive API ──────────────────────────────────────────────────────
|
|
193
|
+
console.log(chalk.bold("\nStep 4 — Enable Google Drive API\n"));
|
|
194
|
+
const DRIVE_API = "drive.googleapis.com";
|
|
195
|
+
if (apiEnabled(projectId, DRIVE_API)) {
|
|
196
|
+
console.log(chalk.green(" ✓ Google Drive API already enabled"));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const ok = enableApi(projectId, DRIVE_API);
|
|
200
|
+
if (ok) {
|
|
201
|
+
console.log(chalk.green(" ✓ Google Drive API enabled"));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(chalk.red(" Failed to enable Google Drive API. Check that billing is set up for the project."));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ── OAuth credentials (browser) ───────────────────────────────────────────
|
|
209
|
+
console.log(chalk.bold("\nStep 5 — Create OAuth 2.0 Credentials\n"));
|
|
210
|
+
console.log(" Google does not allow creating OAuth client credentials from the CLI.");
|
|
211
|
+
console.log(" Opening the Google Cloud Console to create them...\n");
|
|
212
|
+
const credentialsUrl = `https://console.cloud.google.com/apis/credentials/oauthclient?project=${projectId}`;
|
|
213
|
+
console.log(` URL: ${chalk.cyan(credentialsUrl)}\n`);
|
|
214
|
+
console.log(chalk.dim(" Instructions:"));
|
|
215
|
+
console.log(chalk.dim(" 1. Application type → Desktop app"));
|
|
216
|
+
console.log(chalk.dim(' 2. Name → "Skills Manager" (or anything)'));
|
|
217
|
+
console.log(chalk.dim(' 3. Click "Create" → then "Download JSON"'));
|
|
218
|
+
console.log(chalk.dim(" 4. Note where the file is saved\n"));
|
|
219
|
+
openUrl(credentialsUrl);
|
|
220
|
+
await ask("Press Enter once you have downloaded the credentials JSON file...");
|
|
221
|
+
// ── Locate and copy the file ──────────────────────────────────────────────
|
|
222
|
+
console.log();
|
|
223
|
+
const defaultDownload = path.join(os.homedir(), "Downloads");
|
|
224
|
+
const downloadsFiles = fs.existsSync(defaultDownload)
|
|
225
|
+
? fs.readdirSync(defaultDownload).filter((f) => f.startsWith("client_secret") && f.endsWith(".json"))
|
|
226
|
+
: [];
|
|
227
|
+
let credSrc = null;
|
|
228
|
+
if (downloadsFiles.length > 0) {
|
|
229
|
+
// Most recently modified one
|
|
230
|
+
const sorted = downloadsFiles
|
|
231
|
+
.map((f) => ({ f, mtime: fs.statSync(path.join(defaultDownload, f)).mtimeMs }))
|
|
232
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
233
|
+
const candidate = path.join(defaultDownload, sorted[0].f);
|
|
234
|
+
console.log(chalk.dim(` Found: ${candidate}`));
|
|
235
|
+
const use = await confirm(" Use this file?");
|
|
236
|
+
if (use)
|
|
237
|
+
credSrc = candidate;
|
|
238
|
+
}
|
|
239
|
+
if (!credSrc) {
|
|
240
|
+
const entered = await ask(" Enter full path to the downloaded credentials JSON: ");
|
|
241
|
+
credSrc = entered.replace(/^~/, os.homedir());
|
|
242
|
+
}
|
|
243
|
+
if (!credSrc || !fs.existsSync(credSrc)) {
|
|
244
|
+
console.log(chalk.red(` File not found: ${credSrc}`));
|
|
245
|
+
console.log(chalk.dim(` Copy it manually: cp <path> ~/.skillsmanager/credentials.json`));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
ensureConfigDir();
|
|
249
|
+
fs.copyFileSync(credSrc, CREDENTIALS_PATH);
|
|
250
|
+
console.log(chalk.green(`\n ✓ Credentials saved to ~/.skillsmanager/credentials.json`));
|
|
251
|
+
// ── Add test user ─────────────────────────────────────────────────────────
|
|
252
|
+
console.log(chalk.bold("\nStep 6 — Add Test User\n"));
|
|
253
|
+
console.log(" Your app is in Testing mode. You must add your Google account as a test user.");
|
|
254
|
+
console.log(" Opening the OAuth consent screen...\n");
|
|
255
|
+
console.log(chalk.dim(" Instructions:"));
|
|
256
|
+
console.log(chalk.dim(" 1. Scroll down to \"Test users\""));
|
|
257
|
+
console.log(chalk.dim(` 2. Click \"Add users\" → enter ${chalk.white(account)}`));
|
|
258
|
+
console.log(chalk.dim(" 3. Click \"Save\"\n"));
|
|
259
|
+
const consentUrl = `https://console.cloud.google.com/apis/credentials/consent?project=${projectId}`;
|
|
260
|
+
console.log(` URL: ${chalk.cyan(consentUrl)}\n`);
|
|
261
|
+
openUrl(consentUrl);
|
|
262
|
+
await ask("Press Enter once you have added your email as a test user...");
|
|
263
|
+
// ── OAuth flow ────────────────────────────────────────────────────────────
|
|
264
|
+
console.log(chalk.bold("\nStep 7 — Authorize Skills Manager\n"));
|
|
265
|
+
await runAuthFlow();
|
|
266
|
+
console.log(chalk.green("\n ✓ Setup complete!"));
|
|
267
|
+
console.log(`\nRun ${chalk.bold("skillsmanager init")} to discover your registries.\n`);
|
|
268
|
+
}
|
|
269
|
+
function printManualInstructions() {
|
|
270
|
+
console.log(chalk.bold("\nManual Setup Instructions\n"));
|
|
271
|
+
console.log(" 1. Go to https://console.cloud.google.com/");
|
|
272
|
+
console.log(" 2. Create a project (or select an existing one)");
|
|
273
|
+
console.log(" 3. Enable the Google Drive API:");
|
|
274
|
+
console.log(" APIs & Services → Library → search \"Google Drive API\" → Enable");
|
|
275
|
+
console.log(" 4. Create OAuth credentials:");
|
|
276
|
+
console.log(" APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID");
|
|
277
|
+
console.log(" Application type: Desktop app");
|
|
278
|
+
console.log(" 5. Download the JSON and save it:");
|
|
279
|
+
console.log(chalk.cyan(` ~/.skillsmanager/credentials.json`));
|
|
280
|
+
console.log(`\n Then run: ${chalk.bold("skillsmanager setup google")}\n`);
|
|
281
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { ensureReady } from "../ready.js";
|
|
7
|
+
import { getCachePath, ensureCachePath } from "../cache.js";
|
|
8
|
+
export async function updateCommand(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
|
+
if (!skillName) {
|
|
28
|
+
console.log(chalk.red("SKILL.md frontmatter is missing 'name' field."));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const { config, backend } = await ensureReady();
|
|
32
|
+
// Find the collection — --collection override, or look up by installedAt path, or by name
|
|
33
|
+
let collection = config.collections.find((c) => c.name === options.collection) ?? null;
|
|
34
|
+
if (!collection) {
|
|
35
|
+
const entries = config.skills?.[skillName] ?? [];
|
|
36
|
+
// Prefer the entry whose installedAt includes this exact path
|
|
37
|
+
const byPath = entries.find((e) => e.installedAt.includes(absPath));
|
|
38
|
+
// Fall back to the only entry if unambiguous
|
|
39
|
+
const byName = entries.length === 1 ? entries[0] : null;
|
|
40
|
+
const entry = byPath ?? byName;
|
|
41
|
+
if (!entry) {
|
|
42
|
+
if (entries.length > 1) {
|
|
43
|
+
const names = entries.map((e) => {
|
|
44
|
+
const col = config.collections.find((c) => c.id === e.collectionId);
|
|
45
|
+
return col?.name ?? e.collectionId;
|
|
46
|
+
}).join(", ");
|
|
47
|
+
console.log(chalk.red(`"${skillName}" exists in multiple collections: ${names}`));
|
|
48
|
+
console.log(chalk.dim(` Use: skillsmanager update ${skillPath} --collection <name>`));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(chalk.red(`Skill "${skillName}" is not tracked in the skills index.`));
|
|
52
|
+
console.log(chalk.dim(` Run: skillsmanager fetch ${skillName} --agent <agent> first.`));
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
collection = config.collections.find((c) => c.id === entry.collectionId) ?? null;
|
|
57
|
+
if (!collection) {
|
|
58
|
+
console.log(chalk.red(`Collection not found. Run: skillsmanager refresh`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const spinner = ora(`Updating ${chalk.bold(skillName)} in gdrive:${collection.name}...`).start();
|
|
63
|
+
try {
|
|
64
|
+
await backend.uploadSkill(collection, absPath, skillName);
|
|
65
|
+
// Sync updated files into the local cache so symlinks reflect the change immediately
|
|
66
|
+
ensureCachePath(collection);
|
|
67
|
+
const cachePath = getCachePath(collection, skillName);
|
|
68
|
+
await backend.downloadSkill(collection, skillName, cachePath);
|
|
69
|
+
// Update description in SKILLS_SYNC.yaml if it changed
|
|
70
|
+
if (frontmatter.description) {
|
|
71
|
+
const col = await backend.readCollection(collection);
|
|
72
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
73
|
+
if (entry && entry.description !== frontmatter.description) {
|
|
74
|
+
entry.description = frontmatter.description;
|
|
75
|
+
await backend.writeCollection(collection, col);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
spinner.succeed(`${chalk.bold(skillName)} updated in gdrive:${collection.name}`);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Config, CollectionInfo, RegistryInfo } from "./types.js";
|
|
2
|
+
export declare const CONFIG_DIR: string;
|
|
3
|
+
export declare const CREDENTIALS_PATH: string;
|
|
4
|
+
export declare const TOKEN_PATH: string;
|
|
5
|
+
export declare const CONFIG_PATH: string;
|
|
6
|
+
export declare const CACHE_DIR: string;
|
|
7
|
+
export declare function ensureConfigDir(): void;
|
|
8
|
+
export declare function ensureCacheDir(): void;
|
|
9
|
+
export declare function readConfig(): Config;
|
|
10
|
+
/**
|
|
11
|
+
* Merges freshly discovered collections with existing ones, preserving UUIDs
|
|
12
|
+
* for collections already known (matched by folderId). New collections get a
|
|
13
|
+
* fresh UUID. This keeps cache paths stable across refreshes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mergeCollections(fresh: Omit<CollectionInfo, "id">[], existing: CollectionInfo[]): CollectionInfo[];
|
|
16
|
+
/**
|
|
17
|
+
* Merges freshly discovered registries with existing ones, preserving UUIDs
|
|
18
|
+
* for registries already known (matched by folderId).
|
|
19
|
+
*/
|
|
20
|
+
export declare function mergeRegistries(fresh: Omit<RegistryInfo, "id">[], existing: RegistryInfo[]): RegistryInfo[];
|
|
21
|
+
export declare function trackSkill(skillName: string, collectionId: string, installedPath?: string): void;
|
|
22
|
+
export declare function writeConfig(config: Config): void;
|
|
23
|
+
export declare function credentialsExist(): boolean;
|
|
24
|
+
export declare function readCredentials(): {
|
|
25
|
+
client_id: string;
|
|
26
|
+
client_secret: string;
|
|
27
|
+
redirect_uris: string[];
|
|
28
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".skillsmanager");
|
|
6
|
+
export const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
7
|
+
export const TOKEN_PATH = path.join(CONFIG_DIR, "token.json");
|
|
8
|
+
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
9
|
+
export const CACHE_DIR = path.join(CONFIG_DIR, "cache");
|
|
10
|
+
export function ensureConfigDir() {
|
|
11
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
export function ensureCacheDir() {
|
|
14
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
export function readConfig() {
|
|
17
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
18
|
+
throw new Error(`No config found. Run "skillsmanager init" first.`);
|
|
19
|
+
}
|
|
20
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
21
|
+
// Backwards compat: very old configs used "registries" key for collections
|
|
22
|
+
if (raw.registries && !raw.collections && Array.isArray(raw.registries) && raw.registries.length > 0 && raw.registries[0].folderId) {
|
|
23
|
+
// Old format: registries was actually collections
|
|
24
|
+
raw.collections = raw.registries;
|
|
25
|
+
raw.registries = [];
|
|
26
|
+
}
|
|
27
|
+
let needsWrite = false;
|
|
28
|
+
// Backwards compat: ensure registries array exists
|
|
29
|
+
if (!raw.registries || !Array.isArray(raw.registries)) {
|
|
30
|
+
raw.registries = [];
|
|
31
|
+
needsWrite = true;
|
|
32
|
+
}
|
|
33
|
+
// Backfill UUIDs on registries
|
|
34
|
+
const registries = raw.registries;
|
|
35
|
+
registries.forEach((r) => { if (!r.id) {
|
|
36
|
+
r.id = randomUUID();
|
|
37
|
+
needsWrite = true;
|
|
38
|
+
} });
|
|
39
|
+
// Backwards compat: assign stable UUIDs to collections missing an id
|
|
40
|
+
const collections = raw.collections;
|
|
41
|
+
if (Array.isArray(collections)) {
|
|
42
|
+
collections.forEach((c) => { if (!c.id) {
|
|
43
|
+
c.id = randomUUID();
|
|
44
|
+
needsWrite = true;
|
|
45
|
+
} });
|
|
46
|
+
}
|
|
47
|
+
// Backwards compat: old configs have no skills index
|
|
48
|
+
if (!raw.skills) {
|
|
49
|
+
raw.skills = {};
|
|
50
|
+
needsWrite = true;
|
|
51
|
+
}
|
|
52
|
+
// Backwards compat: old skills index used flat { collectionId } instead of array
|
|
53
|
+
const skills = raw.skills;
|
|
54
|
+
for (const key of Object.keys(skills)) {
|
|
55
|
+
const val = skills[key];
|
|
56
|
+
if (!Array.isArray(val)) {
|
|
57
|
+
skills[key] = [{ collectionId: val.collectionId, installedAt: [] }];
|
|
58
|
+
needsWrite = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const config = raw;
|
|
62
|
+
// Persist any backfilled values so they are stable on subsequent reads
|
|
63
|
+
if (needsWrite)
|
|
64
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Merges freshly discovered collections with existing ones, preserving UUIDs
|
|
69
|
+
* for collections already known (matched by folderId). New collections get a
|
|
70
|
+
* fresh UUID. This keeps cache paths stable across refreshes.
|
|
71
|
+
*/
|
|
72
|
+
export function mergeCollections(fresh, existing) {
|
|
73
|
+
return fresh.map((c) => {
|
|
74
|
+
const prev = existing.find((e) => e.folderId === c.folderId);
|
|
75
|
+
return { ...c, id: prev?.id ?? randomUUID() };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Merges freshly discovered registries with existing ones, preserving UUIDs
|
|
80
|
+
* for registries already known (matched by folderId).
|
|
81
|
+
*/
|
|
82
|
+
export function mergeRegistries(fresh, existing) {
|
|
83
|
+
return fresh.map((r) => {
|
|
84
|
+
const prev = existing.find((e) => e.folderId === r.folderId);
|
|
85
|
+
return { ...r, id: prev?.id ?? randomUUID() };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function trackSkill(skillName, collectionId, installedPath) {
|
|
89
|
+
let config;
|
|
90
|
+
try {
|
|
91
|
+
config = readConfig();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
config = { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
|
|
95
|
+
}
|
|
96
|
+
if (!config.skills)
|
|
97
|
+
config.skills = {};
|
|
98
|
+
const entries = config.skills[skillName] ?? [];
|
|
99
|
+
// Find existing entry for this collection
|
|
100
|
+
const existing = entries.find((e) => e.collectionId === collectionId);
|
|
101
|
+
if (existing) {
|
|
102
|
+
if (installedPath && !existing.installedAt.includes(installedPath)) {
|
|
103
|
+
existing.installedAt.push(installedPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
entries.push({ collectionId, installedAt: installedPath ? [installedPath] : [] });
|
|
108
|
+
}
|
|
109
|
+
config.skills[skillName] = entries;
|
|
110
|
+
writeConfig(config);
|
|
111
|
+
}
|
|
112
|
+
export function writeConfig(config) {
|
|
113
|
+
ensureConfigDir();
|
|
114
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
115
|
+
}
|
|
116
|
+
export function credentialsExist() {
|
|
117
|
+
return fs.existsSync(CREDENTIALS_PATH);
|
|
118
|
+
}
|
|
119
|
+
export function readCredentials() {
|
|
120
|
+
if (!credentialsExist()) {
|
|
121
|
+
throw new Error(`No credentials file found at ${CREDENTIALS_PATH}.\n\n` +
|
|
122
|
+
`To set up Google Drive:\n` +
|
|
123
|
+
` 1. Go to https://console.cloud.google.com/\n` +
|
|
124
|
+
` 2. Create a project and enable the Google Drive API\n` +
|
|
125
|
+
` 3. Create OAuth 2.0 credentials (Desktop app type)\n` +
|
|
126
|
+
` 4. Download the JSON and save as ${CREDENTIALS_PATH}\n` +
|
|
127
|
+
` 5. Run "skillsmanager init" again`);
|
|
128
|
+
}
|
|
129
|
+
const raw = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
130
|
+
const creds = raw.installed ?? raw.web ?? raw;
|
|
131
|
+
return {
|
|
132
|
+
client_id: creds.client_id,
|
|
133
|
+
client_secret: creds.client_secret,
|
|
134
|
+
redirect_uris: creds.redirect_uris ?? ["urn:ietf:wg:oauth:2.0:oob"],
|
|
135
|
+
};
|
|
136
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { program } from "commander";
|
|
7
|
+
import { AGENT_PATHS } from "./types.js";
|
|
8
|
+
import { initCommand } from "./commands/init.js";
|
|
9
|
+
import { listCommand } from "./commands/list.js";
|
|
10
|
+
import { searchCommand } from "./commands/search.js";
|
|
11
|
+
import { fetchCommand } from "./commands/fetch.js";
|
|
12
|
+
import { addCommand } from "./commands/add.js";
|
|
13
|
+
import { updateCommand } from "./commands/update.js";
|
|
14
|
+
import { refreshCommand } from "./commands/refresh.js";
|
|
15
|
+
import { setupGoogleCommand } from "./commands/setup/google.js";
|
|
16
|
+
import { collectionCreateCommand } from "./commands/collection.js";
|
|
17
|
+
import { installCommand, uninstallCommand } from "./commands/install.js";
|
|
18
|
+
import { registryCreateCommand, registryListCommand, registryDiscoverCommand, registryAddCollectionCommand, registryPushCommand, } from "./commands/registry.js";
|
|
19
|
+
const supportedAgents = Object.keys(AGENT_PATHS).join(", ");
|
|
20
|
+
// Read the bundled SKILL.md as the CLI help — single source of truth
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const skillMdPath = path.resolve(__dirname, "..", "skills", "skillsmanager", "SKILL.md");
|
|
23
|
+
let helpText = "Discover, fetch, and manage agent skills from local or remote storage.";
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(skillMdPath, "utf-8");
|
|
26
|
+
// Strip YAML frontmatter
|
|
27
|
+
helpText = raw.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
|
|
28
|
+
}
|
|
29
|
+
catch { /* use fallback */ }
|
|
30
|
+
program
|
|
31
|
+
.name("skillsmanager")
|
|
32
|
+
.description(helpText)
|
|
33
|
+
.version("0.0.1");
|
|
34
|
+
// ── Setup ────────────────────────────────────────────────────────────────────
|
|
35
|
+
const setup = program
|
|
36
|
+
.command("setup")
|
|
37
|
+
.description("Set up storage backends");
|
|
38
|
+
setup
|
|
39
|
+
.command("google")
|
|
40
|
+
.description("One-time Google Drive setup (human-facing, not for agents)")
|
|
41
|
+
.action(setupGoogleCommand);
|
|
42
|
+
// ── Core commands ────────────────────────────────────────────────────────────
|
|
43
|
+
program
|
|
44
|
+
.command("init")
|
|
45
|
+
.description("Authenticate and discover collections (runs automatically when needed)")
|
|
46
|
+
.action(initCommand);
|
|
47
|
+
program
|
|
48
|
+
.command("list")
|
|
49
|
+
.description("List all available skills across all collections")
|
|
50
|
+
.action(listCommand);
|
|
51
|
+
program
|
|
52
|
+
.command("search <query>")
|
|
53
|
+
.description("Search skills by name or description (BM25 ranked)")
|
|
54
|
+
.action(searchCommand);
|
|
55
|
+
program
|
|
56
|
+
.command("fetch <names...>")
|
|
57
|
+
.description("Download and install a skill via symlink")
|
|
58
|
+
.requiredOption("--agent <agent>", `Agent to install for (${supportedAgents})`)
|
|
59
|
+
.option("--scope <scope>", "global (~/.agent/skills/) or project (./.agent/skills/)", "global")
|
|
60
|
+
.action((names, options) => fetchCommand(names, options));
|
|
61
|
+
program
|
|
62
|
+
.command("add <path>")
|
|
63
|
+
.description("Upload a local skill directory to a collection")
|
|
64
|
+
.option("--collection <name>", "Target collection (default: first available)")
|
|
65
|
+
.action((skillPath, options) => addCommand(skillPath, options));
|
|
66
|
+
program
|
|
67
|
+
.command("update <path>")
|
|
68
|
+
.description("Push local edits to a skill back to storage and refresh cache")
|
|
69
|
+
.option("--collection <name>", "Override target collection")
|
|
70
|
+
.action((skillPath, options) => updateCommand(skillPath, options));
|
|
71
|
+
program
|
|
72
|
+
.command("refresh")
|
|
73
|
+
.description("Re-discover collections from storage")
|
|
74
|
+
.action(refreshCommand);
|
|
75
|
+
// ── Collection ───────────────────────────────────────────────────────────────
|
|
76
|
+
const collection = program
|
|
77
|
+
.command("collection")
|
|
78
|
+
.description("Manage collections");
|
|
79
|
+
collection
|
|
80
|
+
.command("create [name]")
|
|
81
|
+
.description("Create a new collection (defaults to SKILLS_MY_SKILLS)")
|
|
82
|
+
.action(collectionCreateCommand);
|
|
83
|
+
// ── Registry ─────────────────────────────────────────────────────────────────
|
|
84
|
+
const registry = program
|
|
85
|
+
.command("registry")
|
|
86
|
+
.description("Manage registries (root indexes pointing to collections)");
|
|
87
|
+
registry
|
|
88
|
+
.command("create")
|
|
89
|
+
.description("Create a new registry (default: local, --backend gdrive for Drive)")
|
|
90
|
+
.option("--backend <backend>", "local (default) or gdrive", "local")
|
|
91
|
+
.action((options) => registryCreateCommand(options));
|
|
92
|
+
registry
|
|
93
|
+
.command("list")
|
|
94
|
+
.description("Show all registries and their collection references")
|
|
95
|
+
.action(registryListCommand);
|
|
96
|
+
registry
|
|
97
|
+
.command("discover")
|
|
98
|
+
.description("Search a backend for registries owned by the current user")
|
|
99
|
+
.option("--backend <backend>", "local (default) or gdrive", "local")
|
|
100
|
+
.action((options) => registryDiscoverCommand(options));
|
|
101
|
+
registry
|
|
102
|
+
.command("add-collection <name>")
|
|
103
|
+
.description("Add a collection reference to the registry")
|
|
104
|
+
.option("--backend <backend>", "Backend where the collection lives")
|
|
105
|
+
.option("--ref <ref>", "Backend-specific reference (folder name, repo path)")
|
|
106
|
+
.action((name, options) => registryAddCollectionCommand(name, options));
|
|
107
|
+
registry
|
|
108
|
+
.command("push")
|
|
109
|
+
.description("Push local registry and collections to a remote backend")
|
|
110
|
+
.option("--backend <backend>", "Target backend (default: gdrive)", "gdrive")
|
|
111
|
+
.action((options) => registryPushCommand(options));
|
|
112
|
+
// ── Install/Uninstall ────────────────────────────────────────────────────────
|
|
113
|
+
program
|
|
114
|
+
.command("install")
|
|
115
|
+
.description("Install the skillsmanager skill to agent directories")
|
|
116
|
+
.option("--agent <agents>", "Comma-separated agents (default: all)")
|
|
117
|
+
.option("--path <dir>", "Custom directory to install to")
|
|
118
|
+
.action((options) => installCommand(options));
|
|
119
|
+
program
|
|
120
|
+
.command("uninstall")
|
|
121
|
+
.description("Remove the skillsmanager skill from agent directories")
|
|
122
|
+
.option("--agent <agents>", "Comma-separated agents (default: all)")
|
|
123
|
+
.option("--path <dir>", "Custom directory to remove from")
|
|
124
|
+
.action((options) => uninstallCommand(options));
|
|
125
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
126
|
+
console.error(chalk.red("Error:"), err.message);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|