@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
package/dist/commands/init.js
CHANGED
|
@@ -2,13 +2,13 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { writeConfig, mergeCollections, readConfig } from "../config.js";
|
|
4
4
|
import { ensureAuth } from "../auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
6
6
|
export async function initCommand() {
|
|
7
7
|
console.log(chalk.bold("\nSkills Manager Init\n"));
|
|
8
8
|
const auth = await ensureAuth();
|
|
9
9
|
console.log(chalk.green(" ✓ Authenticated"));
|
|
10
10
|
const spinner = ora(" Discovering collections...").start();
|
|
11
|
-
const backend =
|
|
11
|
+
const backend = await resolveBackend("gdrive");
|
|
12
12
|
const fresh = await backend.discoverCollections();
|
|
13
13
|
let existing = [];
|
|
14
14
|
try {
|
|
@@ -25,7 +25,7 @@ export async function initCommand() {
|
|
|
25
25
|
console.log(chalk.green(` ✓ Found ${collections.length} collection(s):`));
|
|
26
26
|
for (const c of collections) {
|
|
27
27
|
const col = await backend.readCollection(c);
|
|
28
|
-
console.log(`
|
|
28
|
+
console.log(` ${c.backend}:${c.name} (${col.skills.length} skills)`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
let existingSkills = {};
|
package/dist/commands/list.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "os";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import ora from "ora";
|
|
3
4
|
import { readConfig } from "../config.js";
|
|
@@ -14,24 +15,93 @@ export async function getAllSkills() {
|
|
|
14
15
|
}
|
|
15
16
|
return allSkills;
|
|
16
17
|
}
|
|
18
|
+
function collectionTag(col) {
|
|
19
|
+
if (col.backend === "github") {
|
|
20
|
+
const repo = col.folderId.split(":")[0];
|
|
21
|
+
return `[github: ${repo}]`;
|
|
22
|
+
}
|
|
23
|
+
return `[${col.backend}]`;
|
|
24
|
+
}
|
|
25
|
+
const HOME = os.homedir();
|
|
26
|
+
function shortenPath(p) {
|
|
27
|
+
return p.startsWith(HOME) ? "~" + p.slice(HOME.length) : p;
|
|
28
|
+
}
|
|
29
|
+
function installedPaths(skillName, collectionId, skillIndex) {
|
|
30
|
+
const entries = skillIndex[skillName] ?? [];
|
|
31
|
+
const paths = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.collectionId !== collectionId)
|
|
34
|
+
continue;
|
|
35
|
+
for (const p of entry.installedAt) {
|
|
36
|
+
paths.push(shortenPath(p));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return paths;
|
|
40
|
+
}
|
|
41
|
+
function renderCollections(cols, collectionSkills, skillIndex) {
|
|
42
|
+
for (let ci = 0; ci < cols.length; ci++) {
|
|
43
|
+
const col = cols[ci];
|
|
44
|
+
const isLastCol = ci === cols.length - 1;
|
|
45
|
+
const colBranch = isLastCol ? "└──" : "├──";
|
|
46
|
+
const childPad = isLastCol ? " " : "│ ";
|
|
47
|
+
console.log(`${colBranch} ${chalk.bold.yellow(col.name)} ${chalk.dim(collectionTag(col))}`);
|
|
48
|
+
const skills = (collectionSkills.get(col.id) ?? []).sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
for (let si = 0; si < skills.length; si++) {
|
|
50
|
+
const skill = skills[si];
|
|
51
|
+
const isLastSkill = si === skills.length - 1;
|
|
52
|
+
const skillBranch = isLastSkill ? "└──" : "├──";
|
|
53
|
+
const paths = installedPaths(skill.name, col.id, skillIndex);
|
|
54
|
+
console.log(`${childPad}${skillBranch} ${chalk.cyan(skill.name)} ${chalk.dim(skill.description)}`);
|
|
55
|
+
if (paths.length > 0) {
|
|
56
|
+
const installPad = childPad + (isLastSkill ? " " : "│ ");
|
|
57
|
+
console.log(`${installPad}${chalk.magenta(`(${paths.join(", ")})`)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
17
62
|
export async function listCommand() {
|
|
18
63
|
const spinner = ora("Fetching skills...").start();
|
|
19
64
|
try {
|
|
20
|
-
const
|
|
65
|
+
const config = readConfig();
|
|
66
|
+
// Fetch skills per collection
|
|
67
|
+
const collectionSkills = new Map();
|
|
68
|
+
for (const col of config.collections) {
|
|
69
|
+
const backend = await resolveBackend(col.backend);
|
|
70
|
+
const colFile = await backend.readCollection(col);
|
|
71
|
+
collectionSkills.set(col.id, colFile.skills);
|
|
72
|
+
}
|
|
21
73
|
spinner.stop();
|
|
22
|
-
|
|
74
|
+
const totalSkills = [...collectionSkills.values()].reduce((n, s) => n + s.length, 0);
|
|
75
|
+
if (totalSkills === 0) {
|
|
23
76
|
console.log(chalk.yellow("No skills found across any collections."));
|
|
24
77
|
console.log(chalk.dim('Run "skillsmanager collection create <name>" to create a collection, then "skillsmanager add <path>" to add skills.'));
|
|
25
78
|
return;
|
|
26
79
|
}
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
80
|
+
// Group collections by their source registry
|
|
81
|
+
const byRegistry = new Map();
|
|
82
|
+
for (const col of config.collections) {
|
|
83
|
+
const key = col.sourceRegistryId ?? null;
|
|
84
|
+
if (!byRegistry.has(key))
|
|
85
|
+
byRegistry.set(key, []);
|
|
86
|
+
byRegistry.get(key).push(col);
|
|
33
87
|
}
|
|
34
88
|
console.log();
|
|
89
|
+
// Render each registry and its collections
|
|
90
|
+
for (const reg of config.registries) {
|
|
91
|
+
const cols = byRegistry.get(reg.id);
|
|
92
|
+
if (!cols || cols.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
console.log(`${chalk.bold.white(reg.name)} ${chalk.dim(`${reg.backend}`)}`);
|
|
95
|
+
renderCollections(cols, collectionSkills, config.skills);
|
|
96
|
+
console.log();
|
|
97
|
+
}
|
|
98
|
+
// Collections not associated with any registry
|
|
99
|
+
const orphans = byRegistry.get(null);
|
|
100
|
+
if (orphans && orphans.length > 0) {
|
|
101
|
+
console.log(`${chalk.bold.white("(unregistered)")} ${chalk.dim("run 'skillsmanager refresh' to link to a registry")}`);
|
|
102
|
+
renderCollections(orphans, collectionSkills, config.skills);
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
35
105
|
}
|
|
36
106
|
catch (err) {
|
|
37
107
|
spinner.stop();
|
|
@@ -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
|
+
}
|
package/dist/commands/refresh.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function refreshCommand() {
|
|
|
46
46
|
const refBackend = await resolveBackend(ref.backend);
|
|
47
47
|
const colInfo = await refBackend.resolveCollectionRef(ref);
|
|
48
48
|
if (colInfo) {
|
|
49
|
-
freshCollections.push(colInfo);
|
|
49
|
+
freshCollections.push({ ...colInfo, sourceRegistryId: registry.id });
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
52
|
console.log(chalk.dim(`\n Warning: collection "${ref.name}" listed in registry "${registry.name}" could not be resolved`));
|
|
@@ -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;
|
|
@@ -2,8 +2,6 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { readConfig, writeConfig, mergeRegistries, CONFIG_PATH, CACHE_DIR } from "../config.js";
|
|
5
|
-
import { GithubBackend } from "../backends/github.js";
|
|
6
|
-
import { LocalBackend } from "../backends/local.js";
|
|
7
5
|
import { resolveBackend } from "../backends/resolve.js";
|
|
8
6
|
export async function registryCreateCommand(options) {
|
|
9
7
|
const backend = options.backend ?? "local";
|
|
@@ -20,13 +18,7 @@ export async function registryCreateCommand(options) {
|
|
|
20
18
|
const label = backend === "local" ? "locally" : `in ${backend}`;
|
|
21
19
|
const spinner = ora(`Creating registry ${label}...`).start();
|
|
22
20
|
try {
|
|
23
|
-
|
|
24
|
-
if (backend === "github") {
|
|
25
|
-
registry = await new GithubBackend().createRegistry(undefined, options.repo);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
registry = await (await resolveBackend(backend)).createRegistry();
|
|
29
|
-
}
|
|
21
|
+
const registry = await (await resolveBackend(backend)).createRegistry({ repo: options.repo });
|
|
30
22
|
spinner.succeed(`Registry created ${label}`);
|
|
31
23
|
let config = { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
|
|
32
24
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
@@ -37,6 +29,25 @@ export async function registryCreateCommand(options) {
|
|
|
37
29
|
}
|
|
38
30
|
config.registries.push(registry);
|
|
39
31
|
writeConfig(config);
|
|
32
|
+
if (backend !== "local") {
|
|
33
|
+
const localReg = config.registries.find((r) => r.backend === "local");
|
|
34
|
+
if (localReg) {
|
|
35
|
+
const local = await resolveBackend("local");
|
|
36
|
+
try {
|
|
37
|
+
const localData = await local.readRegistry(localReg);
|
|
38
|
+
const localCollections = localData.collections.filter((c) => c.backend === "local");
|
|
39
|
+
if (localCollections.length > 0) {
|
|
40
|
+
const names = localCollections.map((c) => chalk.cyan(c.name)).join(", ");
|
|
41
|
+
console.log(chalk.yellow(`\n Found local registry with ${localCollections.length} collection(s): ${names}`));
|
|
42
|
+
const pushCmd = backend === "github"
|
|
43
|
+
? `skillsmanager registry push --backend github --repo ${options.repo}`
|
|
44
|
+
: `skillsmanager registry push --backend ${backend}`;
|
|
45
|
+
console.log(chalk.dim(` Run ${chalk.white(pushCmd)} to back them up to ${backend}.\n`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { /* local registry unreadable, skip hint */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
40
51
|
}
|
|
41
52
|
catch (err) {
|
|
42
53
|
spinner.fail(`Failed: ${err.message}`);
|
|
@@ -149,12 +160,35 @@ export async function registryRemoveCollectionCommand(collectionName, options) {
|
|
|
149
160
|
console.log(chalk.red("No registries configured."));
|
|
150
161
|
return;
|
|
151
162
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
// Search all registries for the collection; prefer one matching --backend if given
|
|
164
|
+
let reg = config.registries[0];
|
|
165
|
+
let data = null;
|
|
166
|
+
let backend = await resolveBackend(reg.backend);
|
|
167
|
+
for (const r of config.registries) {
|
|
168
|
+
if (options.backend && r.backend !== options.backend)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const b = await resolveBackend(r.backend);
|
|
172
|
+
const d = await b.readRegistry(r);
|
|
173
|
+
if (d.collections.find((c) => c.name === collectionName)) {
|
|
174
|
+
reg = r;
|
|
175
|
+
backend = b;
|
|
176
|
+
data = d;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch { /* skip unreadable registries */ }
|
|
181
|
+
}
|
|
182
|
+
if (!data) {
|
|
183
|
+
// Fall back to reading the first (or backend-matched) registry for the error message
|
|
184
|
+
try {
|
|
185
|
+
data = await backend.readRegistry(reg);
|
|
186
|
+
}
|
|
187
|
+
catch { /**/ }
|
|
188
|
+
}
|
|
189
|
+
const ref = data?.collections.find((c) => c.name === collectionName);
|
|
156
190
|
if (!ref) {
|
|
157
|
-
console.log(chalk.yellow(`Collection "${collectionName}" not found in registry.`));
|
|
191
|
+
console.log(chalk.yellow(`Collection "${collectionName}" not found in any registry.`));
|
|
158
192
|
return;
|
|
159
193
|
}
|
|
160
194
|
// If --delete, delete the actual collection and skills from the backend
|
|
@@ -228,7 +262,7 @@ export async function registryPushCommand(options) {
|
|
|
228
262
|
console.log(chalk.yellow("No local registry to push."));
|
|
229
263
|
return;
|
|
230
264
|
}
|
|
231
|
-
const local =
|
|
265
|
+
const local = await resolveBackend("local");
|
|
232
266
|
const localData = await local.readRegistry(localReg);
|
|
233
267
|
const localCollectionRefs = localData.collections.filter((c) => c.backend === "local");
|
|
234
268
|
if (localCollectionRefs.length === 0) {
|
|
@@ -243,24 +277,33 @@ export async function registryPushCommand(options) {
|
|
|
243
277
|
spinner.text = `Creating registry in ${targetBackend}...`;
|
|
244
278
|
targetReg = await remote.createRegistry();
|
|
245
279
|
}
|
|
246
|
-
//
|
|
280
|
+
// Read remote registry upfront to know what's already synced
|
|
281
|
+
let remoteData;
|
|
282
|
+
try {
|
|
283
|
+
remoteData = await remote.readRegistry(targetReg);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
remoteData = { name: targetReg.name, owner: await remote.getOwner(), source: targetBackend, collections: [] };
|
|
287
|
+
}
|
|
288
|
+
const alreadySynced = new Set(remoteData.collections.map((c) => c.name));
|
|
289
|
+
// Skip collections already present in the remote registry
|
|
290
|
+
const toUpload = localCollectionRefs.filter((ref) => !alreadySynced.has(ref.name));
|
|
291
|
+
if (toUpload.length === 0) {
|
|
292
|
+
spinner.succeed("Remote registry is already up to date.");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Phase 1: Upload new collections — abort on any failure
|
|
247
296
|
const pushed = [];
|
|
248
297
|
try {
|
|
249
|
-
for (const ref of
|
|
298
|
+
for (const ref of toUpload) {
|
|
250
299
|
spinner.text = `Uploading collection "${ref.name}"...`;
|
|
251
300
|
const collInfo = await local.resolveCollectionRef(ref);
|
|
252
301
|
if (!collInfo)
|
|
253
302
|
throw new Error(`Collection "${ref.name}" not found locally`);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
remoteCol = await gdrive.createCollection(folderName);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
const github = remote;
|
|
262
|
-
remoteCol = await github.createCollection(ref.name, options.repo);
|
|
263
|
-
}
|
|
303
|
+
const remoteCol = await remote.createCollection({
|
|
304
|
+
name: ref.name,
|
|
305
|
+
repo: options.repo,
|
|
306
|
+
});
|
|
264
307
|
const colData = await local.readCollection({ ...collInfo, id: "temp" });
|
|
265
308
|
for (const skill of colData.skills) {
|
|
266
309
|
const localSkillPath = path.join(collInfo.folderId, skill.name);
|
|
@@ -281,17 +324,12 @@ export async function registryPushCommand(options) {
|
|
|
281
324
|
// Phase 2: Commit — update registry and config atomically
|
|
282
325
|
spinner.text = "Updating registry...";
|
|
283
326
|
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
327
|
for (const { ref, remoteCol } of pushed) {
|
|
292
|
-
|
|
328
|
+
if (!remoteData.collections.find((c) => c.name === ref.name)) {
|
|
329
|
+
remoteData.collections.push({ name: ref.name, backend: targetBackend, ref: remoteCol.folderId });
|
|
330
|
+
}
|
|
293
331
|
}
|
|
294
|
-
await remote.writeRegistry(targetReg,
|
|
332
|
+
await remote.writeRegistry(targetReg, remoteData);
|
|
295
333
|
if (!config.registries.find((r) => r.id === targetReg.id)) {
|
|
296
334
|
config.registries.push(targetReg);
|
|
297
335
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function searchCommand(query) {
|
|
|
26
26
|
console.log(`\n ${chalk.dim("NAME".padEnd(maxName + 2))}${chalk.dim("DESCRIPTION".padEnd(maxDesc + 2))}${chalk.dim("SOURCE")}`);
|
|
27
27
|
console.log(` ${chalk.dim("-".repeat(maxName + maxDesc + 30))}`);
|
|
28
28
|
for (const s of ranked) {
|
|
29
|
-
console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(
|
|
29
|
+
console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(`${s.collection.backend}:${s.collection.name}`)}`);
|
|
30
30
|
}
|
|
31
31
|
console.log();
|
|
32
32
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
140
|
-
console.log(`\
|
|
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
|
|
193
|
-
console.log(chalk.bold("\nStep 4 — Enable
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
210
|
-
console.log("
|
|
211
|
-
console.log(" Opening the Google Cloud Console
|
|
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(
|
|
216
|
-
console.log(chalk.dim(
|
|
217
|
-
console.log(chalk.dim(
|
|
218
|
-
console.log(chalk.dim(
|
|
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
|
-
|
|
267
|
-
console.log(`\
|
|
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.
|
|
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("
|
|
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
|
}
|