@skillsmanager/cli 0.0.2 → 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.
- package/LICENSE +10 -18
- package/README.md +89 -35
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +54 -14
- package/dist/backends/gdrive.d.ts +2 -1
- package/dist/backends/gdrive.js +15 -4
- package/dist/backends/github.d.ts +27 -0
- package/dist/backends/github.js +407 -0
- package/dist/backends/interface.d.ts +2 -0
- package/dist/backends/local.d.ts +2 -0
- package/dist/backends/local.js +11 -0
- package/dist/backends/resolve.d.ts +2 -0
- package/dist/backends/resolve.js +11 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +209 -7
- package/dist/commands/collection.d.ts +5 -1
- package/dist/commands/collection.js +99 -25
- package/dist/commands/fetch.js +7 -6
- package/dist/commands/list.js +5 -3
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/refresh.js +78 -25
- package/dist/commands/registry.d.ts +6 -0
- package/dist/commands/registry.js +200 -92
- package/dist/commands/setup/github.d.ts +4 -0
- package/dist/commands/setup/github.js +91 -0
- 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 +65 -0
- package/dist/commands/update.js +6 -4
- package/dist/index.js +57 -9
- package/dist/registry.js +11 -3
- package/dist/types.d.ts +1 -0
- package/dist/types.js +2 -0
- package/package.json +2 -2
- package/skills/skillsmanager/SKILL.md +49 -4
|
@@ -1,55 +1,64 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import { readConfig, writeConfig, mergeRegistries, CONFIG_PATH } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
-
import { GDriveBackend } from "../backends/gdrive.js";
|
|
4
|
+
import { readConfig, writeConfig, mergeRegistries, CONFIG_PATH, CACHE_DIR } from "../config.js";
|
|
5
|
+
import { GithubBackend } from "../backends/github.js";
|
|
7
6
|
import { LocalBackend } from "../backends/local.js";
|
|
7
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
8
8
|
export async function registryCreateCommand(options) {
|
|
9
9
|
const backend = options.backend ?? "local";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
const supported = ["local", "gdrive", "github"];
|
|
11
|
+
if (!supported.includes(backend)) {
|
|
12
|
+
console.log(chalk.red(`Unknown backend "${backend}". Supported: ${supported.join(", ")}`));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (backend === "github" && !options.repo) {
|
|
16
|
+
console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
|
|
17
|
+
console.log(chalk.dim(" Example: skillsmanager registry create --backend github --repo owner/my-repo"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const label = backend === "local" ? "locally" : `in ${backend}`;
|
|
21
|
+
const spinner = ora(`Creating registry ${label}...`).start();
|
|
22
|
+
try {
|
|
23
|
+
let registry;
|
|
24
|
+
if (backend === "github") {
|
|
25
|
+
registry = await new GithubBackend().createRegistry(undefined, options.repo);
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
else {
|
|
28
|
+
registry = await (await resolveBackend(backend)).createRegistry();
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
spinner.succeed(`Registry created ${label}`);
|
|
31
|
+
let config = { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
|
|
32
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
33
|
+
try {
|
|
34
|
+
config = readConfig();
|
|
35
|
+
}
|
|
36
|
+
catch { /* use default */ }
|
|
37
|
+
}
|
|
38
|
+
config.registries.push(registry);
|
|
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();
|
|
39
44
|
try {
|
|
40
|
-
|
|
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
|
+
}
|
|
41
55
|
}
|
|
42
|
-
catch { /*
|
|
56
|
+
catch { /* local registry unreadable, skip hint */ }
|
|
43
57
|
}
|
|
44
|
-
config.registries.push(registry);
|
|
45
|
-
writeConfig(config);
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
spinner.fail(`Failed: ${err.message}`);
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
catch (err) {
|
|
61
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
export async function registryListCommand() {
|
|
@@ -69,9 +78,7 @@ export async function registryListCommand() {
|
|
|
69
78
|
for (const reg of config.registries) {
|
|
70
79
|
console.log(`\n${chalk.bold(reg.name)} ${chalk.dim(`(${reg.backend})`)}`);
|
|
71
80
|
try {
|
|
72
|
-
const backend = reg.backend
|
|
73
|
-
? new GDriveBackend(await ensureAuth())
|
|
74
|
-
: new LocalBackend();
|
|
81
|
+
const backend = await resolveBackend(reg.backend);
|
|
75
82
|
const data = await backend.readRegistry(reg);
|
|
76
83
|
if (data.collections.length === 0) {
|
|
77
84
|
console.log(chalk.dim(" No collections"));
|
|
@@ -92,9 +99,7 @@ export async function registryDiscoverCommand(options) {
|
|
|
92
99
|
const backendName = options.backend ?? "local";
|
|
93
100
|
const spinner = ora(`Discovering registries in ${backendName}...`).start();
|
|
94
101
|
try {
|
|
95
|
-
const backend = backendName
|
|
96
|
-
? new GDriveBackend(await ensureAuth())
|
|
97
|
-
: new LocalBackend();
|
|
102
|
+
const backend = await resolveBackend(backendName);
|
|
98
103
|
const fresh = await backend.discoverRegistries();
|
|
99
104
|
let config = { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
|
|
100
105
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
@@ -135,9 +140,7 @@ export async function registryAddCollectionCommand(collectionName, options) {
|
|
|
135
140
|
}
|
|
136
141
|
// Use first registry
|
|
137
142
|
const reg = config.registries[0];
|
|
138
|
-
const backend = reg.backend
|
|
139
|
-
? new GDriveBackend(await ensureAuth())
|
|
140
|
-
: new LocalBackend();
|
|
143
|
+
const backend = await resolveBackend(reg.backend);
|
|
141
144
|
const data = await backend.readRegistry(reg);
|
|
142
145
|
const existing = data.collections.find((c) => c.name === collectionName);
|
|
143
146
|
if (existing) {
|
|
@@ -152,10 +155,106 @@ export async function registryAddCollectionCommand(collectionName, options) {
|
|
|
152
155
|
await backend.writeRegistry(reg, data);
|
|
153
156
|
console.log(chalk.green(`Added "${collectionName}" to registry "${reg.name}".`));
|
|
154
157
|
}
|
|
158
|
+
export async function registryRemoveCollectionCommand(collectionName, options) {
|
|
159
|
+
let config;
|
|
160
|
+
try {
|
|
161
|
+
config = readConfig();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
console.log(chalk.red("No config found."));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (config.registries.length === 0) {
|
|
168
|
+
console.log(chalk.red("No registries configured."));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
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);
|
|
198
|
+
if (!ref) {
|
|
199
|
+
console.log(chalk.yellow(`Collection "${collectionName}" not found in any registry.`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// If --delete, delete the actual collection and skills from the backend
|
|
203
|
+
if (options.delete) {
|
|
204
|
+
const collectionInConfig = config.collections.find((c) => c.name === collectionName);
|
|
205
|
+
if (collectionInConfig) {
|
|
206
|
+
const collBackend = await resolveBackend(collectionInConfig.backend);
|
|
207
|
+
const spinner = ora(`Deleting collection "${collectionName}" from ${collectionInConfig.backend}...`).start();
|
|
208
|
+
try {
|
|
209
|
+
await collBackend.deleteCollection(collectionInConfig);
|
|
210
|
+
spinner.succeed(`Deleted collection "${collectionName}" from ${collectionInConfig.backend}`);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
spinner.fail(`Failed to delete: ${err.message}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Clean up local cache
|
|
218
|
+
if (collectionInConfig) {
|
|
219
|
+
const cachePath = path.join(CACHE_DIR, collectionInConfig.id);
|
|
220
|
+
if (fs.existsSync(cachePath)) {
|
|
221
|
+
fs.rmSync(cachePath, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Remove ref from registry
|
|
226
|
+
data.collections = data.collections.filter((c) => c.name !== collectionName);
|
|
227
|
+
await backend.writeRegistry(reg, data);
|
|
228
|
+
// Remove from local config — capture ID before removal for skills cleanup
|
|
229
|
+
const removedColId = config.collections.find((c) => c.name === collectionName)?.id;
|
|
230
|
+
config.collections = config.collections.filter((c) => c.name !== collectionName);
|
|
231
|
+
// Remove skills index entries for this collection
|
|
232
|
+
if (removedColId) {
|
|
233
|
+
for (const [skillName, locations] of Object.entries(config.skills)) {
|
|
234
|
+
config.skills[skillName] = locations.filter((l) => l.collectionId !== removedColId);
|
|
235
|
+
if (config.skills[skillName].length === 0)
|
|
236
|
+
delete config.skills[skillName];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
writeConfig(config);
|
|
240
|
+
if (options.delete) {
|
|
241
|
+
console.log(chalk.green(`Removed and deleted "${collectionName}" from registry "${reg.name}".`));
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.log(chalk.green(`Removed "${collectionName}" from registry "${reg.name}".`));
|
|
245
|
+
console.log(chalk.dim(` Collection data was kept. Use --delete to permanently remove it.`));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
155
248
|
export async function registryPushCommand(options) {
|
|
156
249
|
const targetBackend = options.backend ?? "gdrive";
|
|
157
|
-
|
|
158
|
-
|
|
250
|
+
const supportedPush = ["gdrive", "github"];
|
|
251
|
+
if (!supportedPush.includes(targetBackend)) {
|
|
252
|
+
console.log(chalk.red(`Push to "${targetBackend}" not yet supported. Use: --backend gdrive or --backend github`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (targetBackend === "github" && !options.repo) {
|
|
256
|
+
console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
|
|
257
|
+
console.log(chalk.dim(" Example: skillsmanager registry push --backend github --repo owner/my-repo"));
|
|
159
258
|
return;
|
|
160
259
|
}
|
|
161
260
|
let config;
|
|
@@ -166,7 +265,6 @@ export async function registryPushCommand(options) {
|
|
|
166
265
|
console.log(chalk.red("No config found."));
|
|
167
266
|
return;
|
|
168
267
|
}
|
|
169
|
-
// Find local registry
|
|
170
268
|
const localReg = config.registries.find((r) => r.backend === "local");
|
|
171
269
|
if (!localReg) {
|
|
172
270
|
console.log(chalk.yellow("No local registry to push."));
|
|
@@ -179,39 +277,57 @@ export async function registryPushCommand(options) {
|
|
|
179
277
|
console.log(chalk.yellow("No local collections to push."));
|
|
180
278
|
return;
|
|
181
279
|
}
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
280
|
+
const remote = await resolveBackend(targetBackend);
|
|
281
|
+
const spinner = ora(`Pushing collections to ${targetBackend}...`).start();
|
|
282
|
+
// Find or create target registry
|
|
283
|
+
let targetReg = config.registries.find((r) => r.backend === targetBackend);
|
|
284
|
+
if (!targetReg) {
|
|
285
|
+
spinner.text = `Creating registry in ${targetBackend}...`;
|
|
286
|
+
targetReg = await remote.createRegistry();
|
|
287
|
+
}
|
|
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
|
|
194
304
|
const pushed = [];
|
|
195
305
|
try {
|
|
196
|
-
for (const ref of
|
|
306
|
+
for (const ref of toUpload) {
|
|
197
307
|
spinner.text = `Uploading collection "${ref.name}"...`;
|
|
198
308
|
const collInfo = await local.resolveCollectionRef(ref);
|
|
199
|
-
if (!collInfo)
|
|
309
|
+
if (!collInfo)
|
|
200
310
|
throw new Error(`Collection "${ref.name}" not found locally`);
|
|
311
|
+
let remoteCol;
|
|
312
|
+
if (targetBackend === "gdrive") {
|
|
313
|
+
const gdrive = remote;
|
|
314
|
+
const folderName = `SKILLS_${ref.name.toUpperCase()}`;
|
|
315
|
+
remoteCol = await gdrive.createCollection(folderName);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const github = remote;
|
|
319
|
+
remoteCol = await github.createCollection(ref.name, options.repo);
|
|
201
320
|
}
|
|
202
|
-
const PREFIX = "SKILLS_";
|
|
203
|
-
const folderName = `${PREFIX}${ref.name.toUpperCase()}`;
|
|
204
|
-
const driveCol = await gdrive.createCollection(folderName);
|
|
205
321
|
const colData = await local.readCollection({ ...collInfo, id: "temp" });
|
|
206
322
|
for (const skill of colData.skills) {
|
|
207
323
|
const localSkillPath = path.join(collInfo.folderId, skill.name);
|
|
208
324
|
if (fs.existsSync(localSkillPath)) {
|
|
209
325
|
spinner.text = `Uploading ${ref.name}/${skill.name}...`;
|
|
210
|
-
await
|
|
326
|
+
await remote.uploadSkill(remoteCol, localSkillPath, skill.name);
|
|
211
327
|
}
|
|
212
328
|
}
|
|
213
|
-
await
|
|
214
|
-
pushed.push({ ref,
|
|
329
|
+
await remote.writeCollection(remoteCol, colData);
|
|
330
|
+
pushed.push({ ref, remoteCol });
|
|
215
331
|
}
|
|
216
332
|
}
|
|
217
333
|
catch (err) {
|
|
@@ -219,39 +335,31 @@ export async function registryPushCommand(options) {
|
|
|
219
335
|
console.log(chalk.dim(" No changes were committed. Local state is unchanged."));
|
|
220
336
|
return;
|
|
221
337
|
}
|
|
222
|
-
//
|
|
338
|
+
// Phase 2: Commit — update registry and config atomically
|
|
223
339
|
spinner.text = "Updating registry...";
|
|
224
340
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
gdriveData = { name: gdriveReg.name, owner: await gdrive.getOwner(), source: "gdrive", collections: [] };
|
|
232
|
-
}
|
|
233
|
-
// Add all pushed collections at once
|
|
234
|
-
for (const { ref, folderName } of pushed) {
|
|
235
|
-
gdriveData.collections.push({ name: ref.name, backend: "gdrive", ref: folderName });
|
|
341
|
+
for (const { ref, remoteCol } of pushed) {
|
|
342
|
+
if (!remoteData.collections.find((c) => c.name === ref.name)) {
|
|
343
|
+
remoteData.collections.push({ name: ref.name, backend: targetBackend, ref: remoteCol.folderId });
|
|
344
|
+
}
|
|
236
345
|
}
|
|
237
|
-
await
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
config.registries.push(gdriveReg);
|
|
346
|
+
await remote.writeRegistry(targetReg, remoteData);
|
|
347
|
+
if (!config.registries.find((r) => r.id === targetReg.id)) {
|
|
348
|
+
config.registries.push(targetReg);
|
|
241
349
|
}
|
|
242
|
-
for (const {
|
|
243
|
-
config.collections.push(
|
|
350
|
+
for (const { remoteCol } of pushed) {
|
|
351
|
+
config.collections.push(remoteCol);
|
|
244
352
|
}
|
|
245
353
|
writeConfig(config);
|
|
246
|
-
spinner.succeed(`Pushed ${pushed.length} collection(s) to
|
|
354
|
+
spinner.succeed(`Pushed ${pushed.length} collection(s) to ${targetBackend}`);
|
|
247
355
|
for (const { ref } of pushed) {
|
|
248
|
-
console.log(chalk.dim(` ${ref.name} →
|
|
356
|
+
console.log(chalk.dim(` ${ref.name} → ${targetBackend}`));
|
|
249
357
|
}
|
|
250
358
|
}
|
|
251
359
|
catch (err) {
|
|
252
360
|
spinner.fail(`Failed to update registry: ${err.message}`);
|
|
253
361
|
console.log(chalk.dim(" Collections were uploaded but the registry was not updated."));
|
|
254
|
-
console.log(chalk.dim(
|
|
362
|
+
console.log(chalk.dim(` Run 'skillsmanager registry push --backend ${targetBackend}' again to retry.`));
|
|
255
363
|
}
|
|
256
364
|
}
|
|
257
365
|
// Need path import for push command
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
4
|
+
export function ghInstalled() {
|
|
5
|
+
return spawnSync("gh", ["--version"], { stdio: "pipe" }).status === 0;
|
|
6
|
+
}
|
|
7
|
+
export function ghAuthed() {
|
|
8
|
+
return spawnSync("gh", ["auth", "status"], { stdio: "pipe" }).status === 0;
|
|
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
|
+
}
|
|
16
|
+
async function installGh() {
|
|
17
|
+
if (process.platform !== "darwin") {
|
|
18
|
+
console.log(chalk.yellow(" Auto-install is only supported on macOS."));
|
|
19
|
+
console.log(chalk.dim(" Install manually: https://cli.github.com/manual/installation"));
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const brewCheck = spawnSync("brew", ["--version"], { stdio: "pipe" });
|
|
23
|
+
if (brewCheck.status !== 0) {
|
|
24
|
+
console.log(chalk.yellow(" Homebrew not found. Install it from https://brew.sh then re-run."));
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
console.log(chalk.dim(" Installing gh via Homebrew..."));
|
|
28
|
+
const r = spawnSync("brew", ["install", "gh"], { stdio: "inherit" });
|
|
29
|
+
if (r.status !== 0) {
|
|
30
|
+
console.log(chalk.red(" Install failed. Try manually: brew install gh"));
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return ghInstalled();
|
|
34
|
+
}
|
|
35
|
+
// ── main command ──────────────────────────────────────────────────────────────
|
|
36
|
+
export async function setupGithubCommand() {
|
|
37
|
+
console.log(chalk.bold("\nSkills Manager — GitHub Setup\n"));
|
|
38
|
+
// Step 1: Check gh CLI
|
|
39
|
+
console.log(chalk.bold("Step 1 — gh CLI\n"));
|
|
40
|
+
if (!ghInstalled()) {
|
|
41
|
+
console.log(chalk.yellow(" gh CLI is not installed."));
|
|
42
|
+
const readline = await import("readline");
|
|
43
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
+
const ans = await new Promise((resolve) => {
|
|
45
|
+
rl.question(` Install it now via Homebrew? ${chalk.dim("[y/n]")} `, (a) => {
|
|
46
|
+
rl.close();
|
|
47
|
+
resolve(a.trim().toLowerCase());
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
if (!ans.startsWith("y")) {
|
|
51
|
+
console.log(chalk.dim(" Install manually: https://cli.github.com/manual/installation"));
|
|
52
|
+
console.log(chalk.dim(" Then re-run: skillsmanager setup github"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const ok = await installGh();
|
|
56
|
+
if (!ok)
|
|
57
|
+
return;
|
|
58
|
+
console.log(chalk.green(" ✓ gh installed"));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(chalk.green(" ✓ gh is installed"));
|
|
62
|
+
}
|
|
63
|
+
// Step 2: Check auth
|
|
64
|
+
console.log(chalk.bold("\nStep 2 — GitHub Authentication\n"));
|
|
65
|
+
if (ghAuthed()) {
|
|
66
|
+
const r = spawnSync("gh", ["api", "user", "--jq", ".login"], {
|
|
67
|
+
encoding: "utf-8", stdio: "pipe",
|
|
68
|
+
});
|
|
69
|
+
const login = r.stdout?.trim() ?? "";
|
|
70
|
+
console.log(chalk.green(` ✓ Already authenticated${login ? ` as ${chalk.white(login)}` : ""}`));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(chalk.dim(" Running gh auth login..."));
|
|
74
|
+
const r = spawnSync("gh", ["auth", "login"], { stdio: "inherit" });
|
|
75
|
+
if (r.status !== 0) {
|
|
76
|
+
console.log(chalk.red(" Authentication failed. Please try manually: gh auth login"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const r2 = spawnSync("gh", ["api", "user", "--jq", ".login"], {
|
|
80
|
+
encoding: "utf-8", stdio: "pipe",
|
|
81
|
+
});
|
|
82
|
+
const login = r2.stdout?.trim() ?? "";
|
|
83
|
+
if (!login) {
|
|
84
|
+
console.log(chalk.red(" Could not verify authentication."));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log(chalk.green(` ✓ Authenticated as ${chalk.white(login)}`));
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.green("\n ✓ GitHub setup complete!"));
|
|
90
|
+
console.log(`\nRun ${chalk.bold("skillsmanager collection create --backend github")} to create a collection.\n`);
|
|
91
|
+
}
|