@skillsmanager/cli 0.0.4 → 0.0.5

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.
@@ -5,16 +5,18 @@ import { writeConfig, CONFIG_PATH, readConfig } from "../config.js";
5
5
  import { ensureAuth } from "../auth.js";
6
6
  import { GDriveBackend } from "../backends/gdrive.js";
7
7
  import { GithubBackend } from "../backends/github.js";
8
+ import { LocalBackend } from "../backends/local.js";
9
+ import { resolveBackend } from "../backends/resolve.js";
8
10
  export async function collectionCreateCommand(name, options = {}) {
9
11
  const backendName = options.backend ?? "gdrive";
10
12
  if (backendName === "github") {
11
- await createGithubCollection(name, options.repo);
13
+ await createGithubCollection(name, options.repo, options.skillsRepo);
12
14
  }
13
15
  else {
14
16
  await createGdriveCollection(name);
15
17
  }
16
18
  }
17
- async function createGithubCollection(name, repo) {
19
+ async function createGithubCollection(name, repo, skillsRepo) {
18
20
  if (!repo) {
19
21
  console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
20
22
  console.log(chalk.dim(" Example: skillsmanager collection create my-skills --backend github --repo owner/my-repo"));
@@ -22,12 +24,19 @@ async function createGithubCollection(name, repo) {
22
24
  }
23
25
  const collectionName = name ?? "default";
24
26
  const backend = new GithubBackend();
25
- console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo}...\n`));
27
+ if (skillsRepo && skillsRepo !== repo) {
28
+ console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo} (skills source: ${skillsRepo})...\n`));
29
+ }
30
+ else {
31
+ console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo}...\n`));
32
+ }
26
33
  try {
27
- const collection = await backend.createCollection(collectionName, repo);
34
+ const collection = await backend.createCollection(collectionName, repo, skillsRepo);
28
35
  console.log(chalk.green(`\n ✓ Collection "${collectionName}" created in github:${collection.folderId}`));
29
36
  const config = loadOrDefaultConfig();
30
37
  upsertCollection(config, collection);
38
+ const registry = await ensureRegistry(config);
39
+ await registerCollectionInRegistry(registry, collection, config);
31
40
  writeConfig(config);
32
41
  console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
33
42
  }
@@ -48,6 +57,8 @@ async function createGdriveCollection(name) {
48
57
  spinner.succeed(`Collection "${folderName}" created in Google Drive`);
49
58
  const config = loadOrDefaultConfig();
50
59
  upsertCollection(config, collection);
60
+ const registry = await ensureRegistry(config);
61
+ await registerCollectionInRegistry(registry, collection, config);
51
62
  writeConfig(config);
52
63
  console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
53
64
  }
@@ -74,3 +85,32 @@ function upsertCollection(config, collection) {
74
85
  config.collections.push(collection);
75
86
  }
76
87
  }
88
+ /** Returns the first registry in config, auto-creating a local one if none exists. */
89
+ async function ensureRegistry(config) {
90
+ if (config.registries.length > 0)
91
+ return config.registries[0];
92
+ console.log(chalk.dim(" No registry found — creating a local registry..."));
93
+ const local = new LocalBackend();
94
+ const registry = await local.createRegistry();
95
+ config.registries.push(registry);
96
+ console.log(chalk.green(" ✓ Local registry created"));
97
+ return registry;
98
+ }
99
+ /** Registers the collection ref in the given registry (writes directly to the registry's backend). */
100
+ async function registerCollectionInRegistry(registry, collection, config) {
101
+ const backend = await resolveBackend(registry.backend);
102
+ const registryData = await backend.readRegistry(registry);
103
+ if (registryData.collections.find((c) => c.name === collection.name))
104
+ return;
105
+ registryData.collections.push({
106
+ name: collection.name,
107
+ backend: collection.backend,
108
+ ref: collection.folderId,
109
+ });
110
+ await backend.writeRegistry(registry, registryData);
111
+ // Keep local config registry list in sync
112
+ if (!config.registries.find((r) => r.id === registry.id)) {
113
+ config.registries.push(registry);
114
+ }
115
+ console.log(chalk.dim(` Registered in registry "${registry.name}" (${registry.backend})`));
116
+ }
@@ -0,0 +1,4 @@
1
+ export declare function logoutGoogleCommand(options: {
2
+ all?: boolean;
3
+ }): Promise<void>;
4
+ export declare function logoutGithubCommand(): void;
@@ -0,0 +1,35 @@
1
+ import fs from "fs";
2
+ import chalk from "chalk";
3
+ import { CREDENTIALS_PATH, TOKEN_PATH } from "../config.js";
4
+ import { spawnSync } from "child_process";
5
+ export async function logoutGoogleCommand(options) {
6
+ const removeAll = options.all;
7
+ let removed = false;
8
+ if (fs.existsSync(TOKEN_PATH)) {
9
+ fs.unlinkSync(TOKEN_PATH);
10
+ console.log(chalk.green(" ✓ Removed token.json (OAuth session cleared)"));
11
+ removed = true;
12
+ }
13
+ else {
14
+ console.log(chalk.dim(" – token.json not found (already logged out)"));
15
+ }
16
+ if (removeAll) {
17
+ if (fs.existsSync(CREDENTIALS_PATH)) {
18
+ fs.unlinkSync(CREDENTIALS_PATH);
19
+ console.log(chalk.green(" ✓ Removed credentials.json (OAuth client cleared)"));
20
+ removed = true;
21
+ }
22
+ else {
23
+ console.log(chalk.dim(" – credentials.json not found"));
24
+ }
25
+ }
26
+ if (removed) {
27
+ console.log(chalk.dim("\n Run skillsmanager setup google to set up again."));
28
+ }
29
+ }
30
+ export function logoutGithubCommand() {
31
+ const r = spawnSync("gh", ["auth", "logout"], { stdio: "inherit" });
32
+ if (r.status !== 0) {
33
+ console.log(chalk.red(" gh auth logout failed. Try manually: gh auth logout"));
34
+ }
35
+ }
@@ -12,6 +12,7 @@ export declare function registryAddCollectionCommand(collectionName: string, opt
12
12
  }): Promise<void>;
13
13
  export declare function registryRemoveCollectionCommand(collectionName: string, options: {
14
14
  delete?: boolean;
15
+ backend?: string;
15
16
  }): Promise<void>;
16
17
  export declare function registryPushCommand(options: {
17
18
  backend?: string;
@@ -37,6 +37,25 @@ export async function registryCreateCommand(options) {
37
37
  }
38
38
  config.registries.push(registry);
39
39
  writeConfig(config);
40
+ if (backend !== "local") {
41
+ const localReg = config.registries.find((r) => r.backend === "local");
42
+ if (localReg) {
43
+ const local = new LocalBackend();
44
+ try {
45
+ const localData = await local.readRegistry(localReg);
46
+ const localCollections = localData.collections.filter((c) => c.backend === "local");
47
+ if (localCollections.length > 0) {
48
+ const names = localCollections.map((c) => chalk.cyan(c.name)).join(", ");
49
+ console.log(chalk.yellow(`\n Found local registry with ${localCollections.length} collection(s): ${names}`));
50
+ const pushCmd = backend === "github"
51
+ ? `skillsmanager registry push --backend github --repo ${options.repo}`
52
+ : `skillsmanager registry push --backend ${backend}`;
53
+ console.log(chalk.dim(` Run ${chalk.white(pushCmd)} to back them up to ${backend}.\n`));
54
+ }
55
+ }
56
+ catch { /* local registry unreadable, skip hint */ }
57
+ }
58
+ }
40
59
  }
41
60
  catch (err) {
42
61
  spinner.fail(`Failed: ${err.message}`);
@@ -149,12 +168,35 @@ export async function registryRemoveCollectionCommand(collectionName, options) {
149
168
  console.log(chalk.red("No registries configured."));
150
169
  return;
151
170
  }
152
- const reg = config.registries[0];
153
- const backend = await resolveBackend(reg.backend);
154
- const data = await backend.readRegistry(reg);
155
- const ref = data.collections.find((c) => c.name === collectionName);
171
+ // Search all registries for the collection; prefer one matching --backend if given
172
+ let reg = config.registries[0];
173
+ let data = null;
174
+ let backend = await resolveBackend(reg.backend);
175
+ for (const r of config.registries) {
176
+ if (options.backend && r.backend !== options.backend)
177
+ continue;
178
+ try {
179
+ const b = await resolveBackend(r.backend);
180
+ const d = await b.readRegistry(r);
181
+ if (d.collections.find((c) => c.name === collectionName)) {
182
+ reg = r;
183
+ backend = b;
184
+ data = d;
185
+ break;
186
+ }
187
+ }
188
+ catch { /* skip unreadable registries */ }
189
+ }
190
+ if (!data) {
191
+ // Fall back to reading the first (or backend-matched) registry for the error message
192
+ try {
193
+ data = await backend.readRegistry(reg);
194
+ }
195
+ catch { /**/ }
196
+ }
197
+ const ref = data?.collections.find((c) => c.name === collectionName);
156
198
  if (!ref) {
157
- console.log(chalk.yellow(`Collection "${collectionName}" not found in registry.`));
199
+ console.log(chalk.yellow(`Collection "${collectionName}" not found in any registry.`));
158
200
  return;
159
201
  }
160
202
  // If --delete, delete the actual collection and skills from the backend
@@ -243,10 +285,25 @@ export async function registryPushCommand(options) {
243
285
  spinner.text = `Creating registry in ${targetBackend}...`;
244
286
  targetReg = await remote.createRegistry();
245
287
  }
246
- // Phase 1: Upload all collections abort on any failure
288
+ // Read remote registry upfront to know what's already synced
289
+ let remoteData;
290
+ try {
291
+ remoteData = await remote.readRegistry(targetReg);
292
+ }
293
+ catch {
294
+ remoteData = { name: targetReg.name, owner: await remote.getOwner(), source: targetBackend, collections: [] };
295
+ }
296
+ const alreadySynced = new Set(remoteData.collections.map((c) => c.name));
297
+ // Skip collections already present in the remote registry
298
+ const toUpload = localCollectionRefs.filter((ref) => !alreadySynced.has(ref.name));
299
+ if (toUpload.length === 0) {
300
+ spinner.succeed("Remote registry is already up to date.");
301
+ return;
302
+ }
303
+ // Phase 1: Upload new collections — abort on any failure
247
304
  const pushed = [];
248
305
  try {
249
- for (const ref of localCollectionRefs) {
306
+ for (const ref of toUpload) {
250
307
  spinner.text = `Uploading collection "${ref.name}"...`;
251
308
  const collInfo = await local.resolveCollectionRef(ref);
252
309
  if (!collInfo)
@@ -281,17 +338,12 @@ export async function registryPushCommand(options) {
281
338
  // Phase 2: Commit — update registry and config atomically
282
339
  spinner.text = "Updating registry...";
283
340
  try {
284
- let targetData;
285
- try {
286
- targetData = await remote.readRegistry(targetReg);
287
- }
288
- catch {
289
- targetData = { name: targetReg.name, owner: await remote.getOwner(), source: targetBackend, collections: [] };
290
- }
291
341
  for (const { ref, remoteCol } of pushed) {
292
- targetData.collections.push({ name: ref.name, backend: targetBackend, ref: remoteCol.folderId });
342
+ if (!remoteData.collections.find((c) => c.name === ref.name)) {
343
+ remoteData.collections.push({ name: ref.name, backend: targetBackend, ref: remoteCol.folderId });
344
+ }
293
345
  }
294
- await remote.writeRegistry(targetReg, targetData);
346
+ await remote.writeRegistry(targetReg, remoteData);
295
347
  if (!config.registries.find((r) => r.id === targetReg.id)) {
296
348
  config.registries.push(targetReg);
297
349
  }
@@ -1 +1,4 @@
1
+ export declare function ghInstalled(): boolean;
2
+ export declare function ghAuthed(): boolean;
3
+ export declare function ghGetLogin(): string;
1
4
  export declare function setupGithubCommand(): Promise<void>;
@@ -1,12 +1,18 @@
1
1
  import { spawnSync } from "child_process";
2
2
  import chalk from "chalk";
3
3
  // ── helpers ───────────────────────────────────────────────────────────────────
4
- function ghInstalled() {
4
+ export function ghInstalled() {
5
5
  return spawnSync("gh", ["--version"], { stdio: "pipe" }).status === 0;
6
6
  }
7
- function ghAuthed() {
7
+ export function ghAuthed() {
8
8
  return spawnSync("gh", ["auth", "status"], { stdio: "pipe" }).status === 0;
9
9
  }
10
+ export function ghGetLogin() {
11
+ const r = spawnSync("gh", ["api", "user", "--jq", ".login"], {
12
+ encoding: "utf-8", stdio: "pipe",
13
+ });
14
+ return r.status === 0 ? (r.stdout?.trim() ?? "") : "";
15
+ }
10
16
  async function installGh() {
11
17
  if (process.platform !== "darwin") {
12
18
  console.log(chalk.yellow(" Auto-install is only supported on macOS."));
@@ -4,9 +4,10 @@ import os from "os";
4
4
  import readline from "readline";
5
5
  import { execSync, spawnSync } from "child_process";
6
6
  import chalk from "chalk";
7
- import { CREDENTIALS_PATH, ensureConfigDir } from "../../config.js";
8
- import { runAuthFlow, hasToken } from "../../auth.js";
7
+ import { CREDENTIALS_PATH, ensureConfigDir, CONFIG_PATH, readConfig } from "../../config.js";
8
+ import { runAuthFlow, hasToken, getAuthedEmail } from "../../auth.js";
9
9
  import { credentialsExist } from "../../config.js";
10
+ import { LocalBackend } from "../../backends/local.js";
10
11
  // ─── Prompt helpers ──────────────────────────────────────────────────────────
11
12
  function ask(question) {
12
13
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -123,6 +124,26 @@ function openUrl(url) {
123
124
  }
124
125
  catch { /* ignore */ }
125
126
  }
127
+ // ─── Post-auth nudge ─────────────────────────────────────────────────────────
128
+ async function printPushNudgeIfNeeded() {
129
+ if (!fs.existsSync(CONFIG_PATH))
130
+ return;
131
+ try {
132
+ const config = readConfig();
133
+ const localReg = config.registries.find((r) => r.backend === "local");
134
+ if (!localReg)
135
+ return;
136
+ const local = new LocalBackend();
137
+ const localData = await local.readRegistry(localReg);
138
+ const localCollections = localData.collections.filter((c) => c.backend === "local");
139
+ if (localCollections.length === 0)
140
+ return;
141
+ const names = localCollections.map((c) => chalk.cyan(c.name)).join(", ");
142
+ console.log(chalk.yellow(`\n Found local registry with ${localCollections.length} collection(s): ${names}`));
143
+ console.log(chalk.dim(` Run ${chalk.white("skillsmanager registry push --backend gdrive")} to back them up to Google Drive.\n`));
144
+ }
145
+ catch { /* unreadable config, skip hint */ }
146
+ }
126
147
  // ─── Main command ─────────────────────────────────────────────────────────────
127
148
  export async function setupGoogleCommand() {
128
149
  console.log(chalk.bold("\nSkills Manager — Google Drive Setup\n"));
@@ -131,13 +152,16 @@ export async function setupGoogleCommand() {
131
152
  console.log(chalk.green(" ✓ credentials.json found"));
132
153
  if (hasToken()) {
133
154
  console.log(chalk.green(" ✓ Already authenticated — nothing to do."));
134
- console.log(`\nRun ${chalk.bold("skillsmanager init")} to discover registries.\n`);
155
+ await printPushNudgeIfNeeded();
156
+ console.log(`Run ${chalk.bold("skillsmanager refresh")} to discover registries.\n`);
135
157
  return;
136
158
  }
137
159
  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`);
160
+ const client = await runAuthFlow();
161
+ const authedEmail = await getAuthedEmail(client);
162
+ console.log(chalk.green(`\n ✓ Authenticated successfully${authedEmail ? ` as ${chalk.white(authedEmail)}` : ""}.`));
163
+ await printPushNudgeIfNeeded();
164
+ console.log(`Run ${chalk.bold("skillsmanager refresh")} to discover registries.\n`);
141
165
  return;
142
166
  }
143
167
  // ── Case 2: No credentials.json ───────────────────────────────────────────
@@ -189,33 +213,54 @@ export async function setupGoogleCommand() {
189
213
  const projectId = await selectOrCreateProject();
190
214
  if (!projectId)
191
215
  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"));
216
+ // ── Enable APIs ───────────────────────────────────────────────────────────
217
+ console.log(chalk.bold("\nStep 4 — Enable Required APIs\n"));
218
+ const REQUIRED_APIS = [
219
+ { api: "drive.googleapis.com", label: "Google Drive API" },
220
+ ];
221
+ for (const { api, label } of REQUIRED_APIS) {
222
+ if (apiEnabled(projectId, api)) {
223
+ console.log(chalk.green(` ✓ ${label} already enabled`));
202
224
  }
203
225
  else {
204
- console.log(chalk.red(" Failed to enable Google Drive API. Check that billing is set up for the project."));
205
- return;
226
+ const ok = enableApi(projectId, api);
227
+ if (ok) {
228
+ console.log(chalk.green(` ✓ ${label} enabled`));
229
+ }
230
+ else {
231
+ console.log(chalk.red(` Failed to enable ${label}. Check that billing is set up for the project.`));
232
+ return;
233
+ }
206
234
  }
207
235
  }
236
+ // ── Configure OAuth Consent Screen ────────────────────────────────────────
237
+ console.log(chalk.bold("\nStep 5 — Configure OAuth Consent Screen\n"));
238
+ console.log(" Before creating credentials, Google requires you to configure the");
239
+ console.log(" OAuth consent screen (the login screen your users will see).\n");
240
+ console.log(` ${chalk.yellow("Note:")} Personal Google accounts can only create ${chalk.white("External")} apps.`);
241
+ console.log(` External apps start in Testing mode — only test users you add can sign in.\n`);
242
+ const consentUrl = `https://console.cloud.google.com/apis/credentials/consent?project=${projectId}`;
243
+ console.log(` URL: ${chalk.cyan(consentUrl)}\n`);
244
+ console.log(chalk.dim(" Instructions:"));
245
+ console.log(chalk.dim(` 1. Audience → select ${chalk.white("External")} (required for personal accounts)`));
246
+ console.log(chalk.dim(` 2. App name → ${chalk.white("Skills Manager")}`));
247
+ console.log(chalk.dim(` 3. User support email → ${chalk.white(account)}`));
248
+ console.log(chalk.dim(` 4. Contact email → ${chalk.white(account)}`));
249
+ console.log(chalk.dim(` 5. Click "Create"`));
250
+ console.log(chalk.dim(` 6. Scroll to "Test users" → "Add users" → enter ${chalk.white(account)} → Save\n`));
251
+ openUrl(consentUrl);
252
+ await ask("Press Enter once you have configured the consent screen and added yourself as a test user...");
208
253
  // ── 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");
254
+ console.log(chalk.bold("\nStep 6 — Create OAuth 2.0 Credentials\n"));
255
+ console.log(" Now create the OAuth client ID that Skills Manager will use to authenticate.");
256
+ console.log(" Opening the Google Cloud Console...\n");
212
257
  const credentialsUrl = `https://console.cloud.google.com/apis/credentials/oauthclient?project=${projectId}`;
213
258
  console.log(` URL: ${chalk.cyan(credentialsUrl)}\n`);
214
259
  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"));
260
+ console.log(chalk.dim(` 1. Application type → ${chalk.white("Desktop app")}`));
261
+ console.log(chalk.dim(` 2. Name → ${chalk.white("Skills Manager")} (or anything)`));
262
+ console.log(chalk.dim(` 3. Click "Create" → then "Download JSON"`));
263
+ console.log(chalk.dim(` 4. Save the file (it will land in your Downloads folder)\n`));
219
264
  openUrl(credentialsUrl);
220
265
  await ask("Press Enter once you have downloaded the credentials JSON file...");
221
266
  // ── Locate and copy the file ──────────────────────────────────────────────
@@ -248,23 +293,13 @@ export async function setupGoogleCommand() {
248
293
  ensureConfigDir();
249
294
  fs.copyFileSync(credSrc, CREDENTIALS_PATH);
250
295
  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
296
  // ── OAuth flow ────────────────────────────────────────────────────────────
264
297
  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`);
298
+ const client = await runAuthFlow();
299
+ const authedEmail = await getAuthedEmail(client);
300
+ console.log(chalk.green(`\n ✓ Setup complete! Authenticated as ${chalk.white(authedEmail ?? account)}`));
301
+ await printPushNudgeIfNeeded();
302
+ console.log(`Run ${chalk.bold("skillsmanager refresh")} to discover your registries.\n`);
268
303
  }
269
304
  function printManualInstructions() {
270
305
  console.log(chalk.bold("\nManual Setup Instructions\n"));
@@ -272,10 +307,15 @@ function printManualInstructions() {
272
307
  console.log(" 2. Create a project (or select an existing one)");
273
308
  console.log(" 3. Enable the Google Drive API:");
274
309
  console.log(" APIs & Services → Library → search \"Google Drive API\" → Enable");
275
- console.log(" 4. Create OAuth credentials:");
310
+ console.log(" 4. Configure the OAuth consent screen:");
311
+ console.log(" APIs & Services → OAuth consent screen");
312
+ console.log(" Audience: External (required for personal Google accounts)");
313
+ console.log(" Fill in App name, support email, and contact email");
314
+ console.log(" Under \"Test users\", add your own Google account email");
315
+ console.log(" 5. Create OAuth credentials:");
276
316
  console.log(" APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID");
277
317
  console.log(" Application type: Desktop app");
278
- console.log(" 5. Download the JSON and save it:");
318
+ console.log(" 6. Download the JSON and save it:");
279
319
  console.log(chalk.cyan(` ~/.skillsmanager/credentials.json`));
280
320
  console.log(`\n Then run: ${chalk.bold("skillsmanager setup google")}\n`);
281
321
  }
@@ -0,0 +1,3 @@
1
+ export declare function skillDeleteCommand(skillName: string, options: {
2
+ collection?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,76 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { readConfig, writeConfig, CACHE_DIR } from "../config.js";
6
+ import { resolveBackend } from "../backends/resolve.js";
7
+ export async function skillDeleteCommand(skillName, options) {
8
+ let config;
9
+ try {
10
+ config = readConfig();
11
+ }
12
+ catch {
13
+ console.log(chalk.red("No config found. Run `skillsmanager refresh` first."));
14
+ return;
15
+ }
16
+ // Resolve which collection to target
17
+ let collection = config.collections.find((c) => c.name === options.collection);
18
+ if (options.collection && !collection) {
19
+ console.log(chalk.red(`Collection "${options.collection}" not found.`));
20
+ return;
21
+ }
22
+ if (!collection) {
23
+ const locations = config.skills[skillName] ?? [];
24
+ if (locations.length === 0) {
25
+ console.log(chalk.red(`Skill "${skillName}" not found in any collection.`));
26
+ return;
27
+ }
28
+ if (locations.length > 1) {
29
+ const names = locations
30
+ .map((l) => config.collections.find((c) => c.id === l.collectionId)?.name ?? l.collectionId)
31
+ .join(", ");
32
+ console.log(chalk.red(`Skill "${skillName}" exists in multiple collections: ${names}`));
33
+ console.log(chalk.dim(` Use --collection <name> to specify which one.`));
34
+ return;
35
+ }
36
+ collection = config.collections.find((c) => c.id === locations[0].collectionId);
37
+ if (!collection) {
38
+ console.log(chalk.red(`Collection for skill "${skillName}" not found in config.`));
39
+ return;
40
+ }
41
+ }
42
+ const backend = await resolveBackend(collection.backend);
43
+ // Delete from backend storage
44
+ const spinner = ora(`Deleting skill "${skillName}" from ${collection.backend}...`).start();
45
+ try {
46
+ await backend.deleteSkill(collection, skillName);
47
+ spinner.succeed(`Deleted "${skillName}" from ${collection.backend}`);
48
+ }
49
+ catch (err) {
50
+ spinner.fail(`Failed to delete from backend: ${err.message}`);
51
+ return;
52
+ }
53
+ // Remove from collection YAML
54
+ try {
55
+ const col = await backend.readCollection(collection);
56
+ col.skills = col.skills.filter((s) => s.name !== skillName);
57
+ await backend.writeCollection(collection, col);
58
+ }
59
+ catch {
60
+ // Non-fatal: backend data already removed
61
+ }
62
+ // Clean up local cache
63
+ const cachePath = path.join(CACHE_DIR, collection.id, skillName);
64
+ if (fs.existsSync(cachePath)) {
65
+ fs.rmSync(cachePath, { recursive: true, force: true });
66
+ }
67
+ // Update config skills index
68
+ if (config.skills[skillName]) {
69
+ config.skills[skillName] = config.skills[skillName].filter((l) => l.collectionId !== collection.id);
70
+ if (config.skills[skillName].length === 0) {
71
+ delete config.skills[skillName];
72
+ }
73
+ }
74
+ writeConfig(config);
75
+ console.log(chalk.green(`\n ✓ Skill "${skillName}" removed from collection "${collection.name}".\n`));
76
+ }
@@ -0,0 +1 @@
1
+ export declare function statusCommand(): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import os from "os";
2
+ import chalk from "chalk";
3
+ import { credentialsExist } from "../config.js";
4
+ import { hasToken, getAuthClient } from "../auth.js";
5
+ import { GDriveBackend } from "../backends/gdrive.js";
6
+ import { ghInstalled, ghAuthed, ghGetLogin } from "./setup/github.js";
7
+ async function getGdriveStatus() {
8
+ if (!credentialsExist()) {
9
+ return { name: "gdrive", loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
10
+ }
11
+ if (!hasToken()) {
12
+ return { name: "gdrive", loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
13
+ }
14
+ try {
15
+ const client = getAuthClient();
16
+ const backend = new GDriveBackend(client);
17
+ const email = await backend.getOwner();
18
+ return { name: "gdrive", loggedIn: true, identity: email };
19
+ }
20
+ catch {
21
+ return { name: "gdrive", loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
22
+ }
23
+ }
24
+ function getGithubStatus() {
25
+ if (!ghInstalled()) {
26
+ return { name: "github", loggedIn: false, identity: "", hint: "install gh CLI first" };
27
+ }
28
+ if (!ghAuthed()) {
29
+ return { name: "github", loggedIn: false, identity: "", hint: "run: skillsmanager setup github" };
30
+ }
31
+ const login = ghGetLogin();
32
+ return { name: "github", loggedIn: true, identity: login };
33
+ }
34
+ export async function statusCommand() {
35
+ const localStatus = {
36
+ name: "local",
37
+ loggedIn: true,
38
+ identity: os.userInfo().username,
39
+ };
40
+ const [gdriveStatus, githubStatus] = await Promise.all([
41
+ getGdriveStatus(),
42
+ Promise.resolve(getGithubStatus()),
43
+ ]);
44
+ const rows = [localStatus, gdriveStatus, githubStatus];
45
+ const col1 = 8;
46
+ const col2 = 24;
47
+ const header = chalk.bold("Backend".padEnd(col1)) + " " +
48
+ chalk.bold("Status".padEnd(col2)) + " " +
49
+ chalk.bold("Identity");
50
+ const divider = "─".repeat(col1) + " " + "─".repeat(col2) + " " + "─".repeat(30);
51
+ console.log();
52
+ console.log(header);
53
+ console.log(chalk.dim(divider));
54
+ for (const row of rows) {
55
+ const status = row.loggedIn
56
+ ? chalk.green("✓ logged in")
57
+ : chalk.red("✗ not logged in");
58
+ const identity = row.loggedIn
59
+ ? chalk.white(row.identity)
60
+ : chalk.dim(row.hint ?? "");
61
+ const statusLabel = row.loggedIn ? "✓ logged in" : "✗ not logged in";
62
+ console.log(row.name.padEnd(col1) + " " + status + " ".repeat(Math.max(0, col2 - statusLabel.length)) + " " + identity);
63
+ }
64
+ console.log();
65
+ }