skillstogether 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,2749 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command9 } from "commander";
5
+
6
+ // src/commands/add.ts
7
+ import * as p2 from "@clack/prompts";
8
+ import { Command } from "commander";
9
+ import { join as join5 } from "path";
10
+ import pc3 from "picocolors";
11
+
12
+ // src/lib/config.ts
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
+ import { homedir } from "os";
15
+ import { join } from "path";
16
+ var CONFIG_DIR = join(homedir(), ".skillstogether");
17
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
18
+ function ensureConfigDir() {
19
+ if (!existsSync(CONFIG_DIR)) {
20
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
21
+ }
22
+ }
23
+ function readConfig() {
24
+ try {
25
+ if (!existsSync(CONFIG_FILE)) {
26
+ return {};
27
+ }
28
+ const content = readFileSync(CONFIG_FILE, "utf-8");
29
+ const parsed = JSON.parse(content);
30
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+ function writeConfig(config) {
36
+ ensureConfigDir();
37
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
38
+ }
39
+ function getToken() {
40
+ return readConfig().token;
41
+ }
42
+ function setToken(token) {
43
+ const config = readConfig();
44
+ config.token = token;
45
+ writeConfig(config);
46
+ }
47
+ function removeToken() {
48
+ const config = readConfig();
49
+ delete config.token;
50
+ writeConfig(config);
51
+ }
52
+ function getApiUrl() {
53
+ return process.env.CLI_API_URL || "http://localhost:3000";
54
+ }
55
+ function isInsecureApiUrl() {
56
+ try {
57
+ const parsed = new URL(getApiUrl());
58
+ return parsed.protocol === "http:" && parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1";
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ // src/lib/constants.ts
65
+ var AUTH_CALLBACK_PORT = 9876;
66
+ var AUTH_CALLBACK_PORT_FALLBACKS = [9877, 9878, 9879];
67
+ var AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
68
+ var MAX_FILE_PATH_LENGTH = 255;
69
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
70
+ var DEFAULT_DOWNLOAD_MAX_BYTES = 10 * 1024 * 1024;
71
+
72
+ // src/lib/fetch.ts
73
+ async function safeFetch(url, options = {}) {
74
+ const {
75
+ timeout = DEFAULT_REQUEST_TIMEOUT_MS,
76
+ signal: userSignal,
77
+ ...init
78
+ } = options;
79
+ const controller = new AbortController();
80
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
81
+ try {
82
+ const response = await fetch(url, {
83
+ ...init,
84
+ signal: userSignal ?? controller.signal
85
+ });
86
+ clearTimeout(timeoutId);
87
+ return response;
88
+ } catch (err) {
89
+ clearTimeout(timeoutId);
90
+ if (err instanceof Error && err.name === "AbortError") {
91
+ throw new Error(`Request timed out after ${timeout}ms`);
92
+ }
93
+ throw err;
94
+ }
95
+ }
96
+ function isAllowedDownloadUrl(url, allowedHosts = []) {
97
+ try {
98
+ const parsed = new URL(url);
99
+ const protocol = parsed.protocol.toLowerCase();
100
+ const host = parsed.hostname.toLowerCase();
101
+ if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") {
102
+ return true;
103
+ }
104
+ if (protocol !== "https:" && protocol !== "http:") {
105
+ return false;
106
+ }
107
+ if (host !== "localhost" && host !== "127.0.0.1" && protocol === "http:") {
108
+ return false;
109
+ }
110
+ if (allowedHosts.length > 0) {
111
+ return allowedHosts.some(
112
+ (allowed) => host === allowed.toLowerCase() || host.endsWith(`.${allowed.toLowerCase()}`)
113
+ );
114
+ }
115
+ return protocol === "https:";
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+ async function fetchWithSizeLimit(url, options = {}) {
121
+ const {
122
+ maxBytes = DEFAULT_DOWNLOAD_MAX_BYTES,
123
+ timeout = DEFAULT_REQUEST_TIMEOUT_MS,
124
+ allowedHosts = []
125
+ } = options;
126
+ if (!isAllowedDownloadUrl(url, allowedHosts)) {
127
+ throw new Error("Download URL is not from an allowed origin");
128
+ }
129
+ const controller = new AbortController();
130
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
131
+ try {
132
+ const response = await fetch(url, {
133
+ signal: controller.signal
134
+ });
135
+ if (!response.ok) {
136
+ throw new Error(`Failed to download file: ${response.status}`);
137
+ }
138
+ const contentLength = response.headers.get("content-length");
139
+ if (contentLength) {
140
+ const length = parseInt(contentLength, 10);
141
+ if (!Number.isNaN(length) && length > maxBytes) {
142
+ throw new Error(
143
+ `File too large (${length} bytes, max ${maxBytes} bytes)`
144
+ );
145
+ }
146
+ }
147
+ const reader = response.body?.getReader();
148
+ if (!reader) {
149
+ throw new Error("Response has no body");
150
+ }
151
+ const chunks = [];
152
+ let totalBytes = 0;
153
+ while (true) {
154
+ const { done, value } = await reader.read();
155
+ if (done) break;
156
+ totalBytes += value.length;
157
+ if (totalBytes > maxBytes) {
158
+ reader.cancel();
159
+ throw new Error(
160
+ `File exceeds size limit (${totalBytes} bytes, max ${maxBytes} bytes)`
161
+ );
162
+ }
163
+ chunks.push(value);
164
+ }
165
+ clearTimeout(timeoutId);
166
+ return Buffer.concat(chunks);
167
+ } catch (err) {
168
+ clearTimeout(timeoutId);
169
+ if (err instanceof Error && err.name === "AbortError") {
170
+ throw new Error(`Download timed out after ${timeout}ms`);
171
+ }
172
+ throw err;
173
+ }
174
+ }
175
+
176
+ // src/lib/api.ts
177
+ var insecureUrlWarned = false;
178
+ function warnInsecureApiUrlOnce() {
179
+ if (!insecureUrlWarned && isInsecureApiUrl()) {
180
+ insecureUrlWarned = true;
181
+ console.warn("Warning: CLI_API_URL is using HTTP. Use HTTPS in production.");
182
+ }
183
+ }
184
+ function wrapFetchError(url, cause) {
185
+ const message = cause instanceof Error ? cause.message : "Unknown network error";
186
+ if (message === "fetch failed" || message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("network")) {
187
+ return new Error(
188
+ `Cannot reach API at ${url}. Is the server running? Set CLI_API_URL if using a different host.`,
189
+ { cause: cause instanceof Error ? cause : void 0 }
190
+ );
191
+ }
192
+ return cause instanceof Error ? cause : new Error(String(cause));
193
+ }
194
+ async function apiGet(path, queryParams) {
195
+ const token = getToken();
196
+ if (!token) {
197
+ throw new Error("Not authenticated. Run 'auth login' first.");
198
+ }
199
+ warnInsecureApiUrlOnce();
200
+ const apiUrl = getApiUrl();
201
+ const url = new URL(`${apiUrl}/api${path}`);
202
+ if (queryParams) {
203
+ for (const [key, value] of Object.entries(queryParams)) {
204
+ url.searchParams.set(key, value);
205
+ }
206
+ }
207
+ let response;
208
+ try {
209
+ response = await safeFetch(url.toString(), {
210
+ method: "GET",
211
+ headers: {
212
+ "x-api-key": token
213
+ },
214
+ timeout: DEFAULT_REQUEST_TIMEOUT_MS
215
+ });
216
+ } catch (err) {
217
+ throw wrapFetchError(url.toString(), err);
218
+ }
219
+ if (!response.ok) {
220
+ const errorData = await response.json().catch(() => ({
221
+ message: response.statusText
222
+ }));
223
+ const message = errorData?.message || errorData?.error?.message || `API error: ${response.status}`;
224
+ throw new Error(message);
225
+ }
226
+ return response.json();
227
+ }
228
+ async function apiPost(path, body) {
229
+ const token = getToken();
230
+ if (!token) {
231
+ throw new Error("Not authenticated. Run 'auth login' first.");
232
+ }
233
+ warnInsecureApiUrlOnce();
234
+ const apiUrl = getApiUrl();
235
+ const url = `${apiUrl}/api${path}`;
236
+ let response;
237
+ try {
238
+ response = await safeFetch(url, {
239
+ method: "POST",
240
+ headers: {
241
+ "Content-Type": "application/json",
242
+ "x-api-key": token
243
+ },
244
+ body: JSON.stringify(body),
245
+ timeout: DEFAULT_REQUEST_TIMEOUT_MS
246
+ });
247
+ } catch (err) {
248
+ throw wrapFetchError(url, err);
249
+ }
250
+ if (!response.ok) {
251
+ const errorData = await response.json().catch(() => ({
252
+ message: response.statusText
253
+ }));
254
+ const message = errorData?.message || errorData?.error?.message || `API error: ${response.status}`;
255
+ throw new Error(message);
256
+ }
257
+ return response.json();
258
+ }
259
+ async function fetchOrganizationSkills(organizationSlug) {
260
+ return apiGet("/cli/skills", { organizationSlug });
261
+ }
262
+ async function fetchSkill(organizationSlug, skillSlug) {
263
+ return apiGet(`/cli/skills/${encodeURIComponent(skillSlug)}`, {
264
+ organizationSlug
265
+ });
266
+ }
267
+ async function verifyToken() {
268
+ try {
269
+ const result = await apiGet("/cli/verify");
270
+ return { valid: true, user: result.user };
271
+ } catch {
272
+ return { valid: false };
273
+ }
274
+ }
275
+ async function checkForUpdates(organizationSlug, skills) {
276
+ return apiPost("/cli/skills/check-updates", { organizationSlug, skills });
277
+ }
278
+ function trackDownloads(events) {
279
+ if (events.length === 0) {
280
+ return;
281
+ }
282
+ const token = getToken();
283
+ if (!token) {
284
+ return;
285
+ }
286
+ const apiUrl = getApiUrl();
287
+ safeFetch(`${apiUrl}/api/cli/telemetry/track`, {
288
+ method: "POST",
289
+ headers: {
290
+ "Content-Type": "application/json",
291
+ "x-api-key": token
292
+ },
293
+ body: JSON.stringify({ downloads: events }),
294
+ timeout: DEFAULT_REQUEST_TIMEOUT_MS
295
+ }).catch(() => {
296
+ });
297
+ }
298
+
299
+ // src/lib/errors.ts
300
+ var DEFAULT_ERROR_MESSAGE = "Unknown error";
301
+ function formatErrorMessage(err, fallback = DEFAULT_ERROR_MESSAGE) {
302
+ if (err instanceof Error) {
303
+ return err.message || fallback;
304
+ }
305
+ if (typeof err === "string") {
306
+ return err;
307
+ }
308
+ return fallback;
309
+ }
310
+
311
+ // src/lib/installer.ts
312
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
313
+ import { dirname, join as join2 } from "path";
314
+
315
+ // src/lib/validation.ts
316
+ import { z } from "zod";
317
+ var slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
318
+ var organizationSlugSchema = z.string().min(1, "Organization slug is required").max(100).regex(
319
+ slugRegex,
320
+ "Organization slug must be lowercase letters, numbers, and hyphens only"
321
+ );
322
+ var skillSlugSchema = z.string().min(1, "Skill slug is required").max(100).regex(
323
+ slugRegex,
324
+ "Skill slug must be lowercase letters, numbers, and hyphens only"
325
+ );
326
+ var addTargetSchema = z.string().min(1, "Target is required").refine(
327
+ (val) => {
328
+ const parts = val.split("/");
329
+ return parts.length >= 1 && parts.length <= 2;
330
+ },
331
+ { message: "Target must be 'org-slug' or 'org-slug/skill-slug'" }
332
+ ).refine(
333
+ (val) => {
334
+ const parts = val.split("/");
335
+ return parts.every((part) => slugRegex.test(part));
336
+ },
337
+ { message: "Each part must be a valid slug (lowercase, numbers, hyphens)" }
338
+ );
339
+ var updateTargetSchema = z.string().max(200).optional().refine(
340
+ (val) => {
341
+ if (!val) return true;
342
+ const parts = val.split("/");
343
+ return parts.length >= 1 && parts.length <= 2 && parts.every((part) => slugRegex.test(part));
344
+ },
345
+ { message: "Target must be 'org-slug' or 'org-slug/skill-slug'" }
346
+ );
347
+ var uninstallTargetSchema = z.string().min(1, "Target is required").refine(
348
+ (val) => {
349
+ const parts = val.split("/");
350
+ return parts.length >= 1 && parts.length <= 2 && parts.every((part) => slugRegex.test(part));
351
+ },
352
+ { message: "Target must be 'org-slug' or 'org-slug/skill-slug'" }
353
+ );
354
+ var agentSchema = z.string().min(1).max(50);
355
+ var customDirSchema = z.string().min(1).max(4096);
356
+ function parseAddTarget(target) {
357
+ const parsed = addTargetSchema.parse(target);
358
+ const parts = parsed.split("/");
359
+ const organizationSlug = organizationSlugSchema.parse(parts[0]);
360
+ return {
361
+ organizationSlug,
362
+ skillSlug: parts.length === 2 ? parts[1] : void 0
363
+ };
364
+ }
365
+ function parseUpdateTarget(target) {
366
+ if (target === void 0 || target === "") return {};
367
+ const parsed = updateTargetSchema.parse(target);
368
+ if (!parsed || typeof parsed !== "string") return {};
369
+ const parts = parsed.split("/");
370
+ return {
371
+ organizationSlug: parts[0],
372
+ skillSlug: parts.length === 2 ? parts[1] : void 0
373
+ };
374
+ }
375
+ function parseUninstallTarget(target) {
376
+ const parsed = uninstallTargetSchema.parse(target);
377
+ const parts = parsed.split("/");
378
+ return {
379
+ organizationSlug: parts[0],
380
+ skillSlug: parts.length === 2 ? parts[1] : void 0
381
+ };
382
+ }
383
+ function isValidSlug(slug) {
384
+ return slugRegex.test(slug);
385
+ }
386
+
387
+ // src/lib/yaml.ts
388
+ function escapeYaml(value) {
389
+ if (/[:\n\r#'"{}[\]|>&*!%@`]/.test(value) || value.trim() !== value) {
390
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
391
+ }
392
+ return value;
393
+ }
394
+
395
+ // src/lib/installer.ts
396
+ function isValidFilePath(path) {
397
+ return path.length > 0 && path.length <= MAX_FILE_PATH_LENGTH && !path.startsWith("/") && !path.includes("..") && /^[a-zA-Z0-9_\-/.]+$/.test(path);
398
+ }
399
+ function generateFilesChecksum(files) {
400
+ if (files.length === 0) return "";
401
+ const data = files.map((f) => `${f.path}:${f.size}`).sort().join("|");
402
+ let hash = 0;
403
+ for (let i = 0; i < data.length; i++) {
404
+ const char = data.charCodeAt(i);
405
+ hash = (hash << 5) - hash + char;
406
+ hash = hash & hash;
407
+ }
408
+ return Math.abs(hash).toString(36);
409
+ }
410
+ function generateSkillContent(skill, organizationSlug, filesChecksum) {
411
+ const frontmatter = [
412
+ "---",
413
+ `name: ${escapeYaml(skill.name)}`,
414
+ `slug: ${escapeYaml(skill.slug)}`,
415
+ `organization: ${escapeYaml(organizationSlug)}`,
416
+ skill.description ? `description: ${escapeYaml(skill.description)}` : null,
417
+ `createdBy: ${escapeYaml(skill.createdBy.name)}`,
418
+ `installedAt: ${(/* @__PURE__ */ new Date()).toISOString()}`,
419
+ filesChecksum ? `filesChecksum: ${filesChecksum}` : null,
420
+ "---",
421
+ ""
422
+ ].filter(Boolean).join("\n");
423
+ return frontmatter + (skill.content || "");
424
+ }
425
+ async function downloadFile(url) {
426
+ return fetchWithSizeLimit(url, {
427
+ maxBytes: DEFAULT_DOWNLOAD_MAX_BYTES,
428
+ timeout: DEFAULT_REQUEST_TIMEOUT_MS
429
+ });
430
+ }
431
+ var SKILL_FOLDER_PATTERN = (organizationSlug, skillSlug) => `${organizationSlug}-${skillSlug}`;
432
+ var SKILL_FILENAME = "SKILL.md";
433
+ async function installSkill(skill, organizationSlug, options) {
434
+ if (!isValidSlug(organizationSlug)) {
435
+ return {
436
+ success: false,
437
+ path: "",
438
+ skipped: false,
439
+ error: "Invalid organization slug format"
440
+ };
441
+ }
442
+ if (!isValidSlug(skill.slug)) {
443
+ return {
444
+ success: false,
445
+ path: "",
446
+ skipped: false,
447
+ error: "Invalid skill slug format"
448
+ };
449
+ }
450
+ const skillFolderName = SKILL_FOLDER_PATTERN(organizationSlug, skill.slug);
451
+ const skillDir = join2(options.dir, skillFolderName);
452
+ const filePath = join2(skillDir, SKILL_FILENAME);
453
+ try {
454
+ if (existsSync2(filePath) && !options.force) {
455
+ return {
456
+ success: true,
457
+ path: filePath,
458
+ skipped: true
459
+ };
460
+ }
461
+ if (!existsSync2(skillDir)) {
462
+ mkdirSync2(skillDir, { recursive: true });
463
+ }
464
+ const files = skill.files || [];
465
+ const filesChecksum = generateFilesChecksum(files);
466
+ const content = generateSkillContent(skill, organizationSlug, filesChecksum);
467
+ writeFileSync2(filePath, content, "utf-8");
468
+ let filesInstalled = 0;
469
+ for (const file of files) {
470
+ if (!isValidFilePath(file.path)) {
471
+ console.warn(`Skipping invalid file path: ${file.path}`);
472
+ continue;
473
+ }
474
+ const targetPath = join2(skillDir, file.path);
475
+ const targetDir = dirname(targetPath);
476
+ if (!existsSync2(targetDir)) {
477
+ mkdirSync2(targetDir, { recursive: true });
478
+ }
479
+ try {
480
+ const fileContent = await downloadFile(file.blobUrl);
481
+ writeFileSync2(targetPath, fileContent);
482
+ filesInstalled++;
483
+ } catch (err) {
484
+ console.warn(
485
+ `Failed to download file ${file.path}: ${formatErrorMessage(err)}`
486
+ );
487
+ }
488
+ }
489
+ return {
490
+ success: true,
491
+ path: filePath,
492
+ skipped: false,
493
+ filesInstalled
494
+ };
495
+ } catch (error) {
496
+ return {
497
+ success: false,
498
+ path: filePath,
499
+ skipped: false,
500
+ error: formatErrorMessage(error)
501
+ };
502
+ }
503
+ }
504
+ async function installSkills(skills, organizationSlug, options) {
505
+ const results = [];
506
+ for (const skill of skills) {
507
+ const result = await installSkill(skill, organizationSlug, options);
508
+ results.push(result);
509
+ }
510
+ return results;
511
+ }
512
+
513
+ // src/lib/scanner.ts
514
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync } from "fs";
515
+ import { homedir as homedir2 } from "os";
516
+ import { basename, join as join3 } from "path";
517
+
518
+ // src/lib/agents.ts
519
+ var AGENTS = [
520
+ { id: "cursor", name: "Cursor", dir: ".cursor/skills" },
521
+ { id: "claude-code", name: "Claude Code", dir: ".claude/skills" },
522
+ { id: "codex", name: "Codex", dir: ".codex/skills" },
523
+ { id: "cline", name: "Cline", dir: ".cline/skills" },
524
+ { id: "amp", name: "Amp", dir: ".agents/skills" },
525
+ { id: "antigravity", name: "Antigravity", dir: ".agent/skills" },
526
+ { id: "augment", name: "Augment", dir: ".augment/rules" },
527
+ { id: "openclaw", name: "OpenClaw", dir: "skills" },
528
+ { id: "codebuddy", name: "CodeBuddy", dir: ".codebuddy/skills" },
529
+ { id: "windsurf", name: "Windsurf", dir: ".windsurf/skills" },
530
+ { id: "goose", name: "Goose", dir: ".goose/skills" },
531
+ {
532
+ id: "github-copilot",
533
+ name: "GitHub Copilot",
534
+ dir: ".github/copilot/skills"
535
+ }
536
+ ];
537
+
538
+ // src/lib/frontmatter.ts
539
+ function parseFrontmatter(content) {
540
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
541
+ const match = content.match(frontmatterRegex);
542
+ if (!match) {
543
+ return { data: {}, content };
544
+ }
545
+ const frontmatterStr = match[1];
546
+ const remainingContent = content.slice(match[0].length);
547
+ const data = {};
548
+ const lines = frontmatterStr.split(/\r?\n/);
549
+ for (const line of lines) {
550
+ const colonIndex = line.indexOf(":");
551
+ if (colonIndex === -1) continue;
552
+ const key = line.slice(0, colonIndex).trim();
553
+ let value = line.slice(colonIndex + 1).trim();
554
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
555
+ value = value.slice(1, -1);
556
+ value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
557
+ }
558
+ if (key === "version") {
559
+ const numValue = parseInt(value, 10);
560
+ data[key] = isNaN(numValue) ? void 0 : numValue;
561
+ } else {
562
+ data[key] = value;
563
+ }
564
+ }
565
+ return {
566
+ data,
567
+ content: remainingContent
568
+ };
569
+ }
570
+ function generateFrontmatter(data) {
571
+ const lines = ["---"];
572
+ if (data.name) lines.push(`name: ${escapeYaml(data.name)}`);
573
+ if (data.slug) lines.push(`slug: ${escapeYaml(data.slug)}`);
574
+ if (data.organization)
575
+ lines.push(`organization: ${escapeYaml(data.organization)}`);
576
+ if (data.description)
577
+ lines.push(`description: ${escapeYaml(data.description)}`);
578
+ if (data.createdBy) lines.push(`createdBy: ${escapeYaml(data.createdBy)}`);
579
+ if (data.installedAt) lines.push(`installedAt: ${data.installedAt}`);
580
+ if (data.version !== void 0) lines.push(`version: ${data.version}`);
581
+ if (data.filesChecksum) lines.push(`filesChecksum: ${data.filesChecksum}`);
582
+ lines.push("---");
583
+ return lines.join("\n");
584
+ }
585
+
586
+ // src/lib/scanner.ts
587
+ function scanSkillFiles(skillDir) {
588
+ const files = [];
589
+ if (!existsSync3(skillDir)) {
590
+ return files;
591
+ }
592
+ try {
593
+ const entries = readdirSync(skillDir, { withFileTypes: true });
594
+ for (const entry of entries) {
595
+ if (entry.name === SKILL_FILENAME) continue;
596
+ const entryPath = join3(skillDir, entry.name);
597
+ if (entry.isFile()) {
598
+ files.push({ path: entry.name, folder: null });
599
+ } else if (entry.isDirectory()) {
600
+ try {
601
+ const subEntries = readdirSync(entryPath, { withFileTypes: true });
602
+ for (const subEntry of subEntries) {
603
+ if (subEntry.isFile()) {
604
+ files.push({
605
+ path: `${entry.name}/${subEntry.name}`,
606
+ folder: entry.name
607
+ });
608
+ }
609
+ }
610
+ } catch {
611
+ }
612
+ }
613
+ }
614
+ } catch {
615
+ }
616
+ return files;
617
+ }
618
+ function scanAgentDirectory(agentDir, agent, scope) {
619
+ const skills = [];
620
+ if (!existsSync3(agentDir)) {
621
+ return skills;
622
+ }
623
+ try {
624
+ const entries = readdirSync(agentDir, { withFileTypes: true });
625
+ for (const entry of entries) {
626
+ if (!entry.isDirectory()) continue;
627
+ const dirPath = join3(agentDir, entry.name);
628
+ const skillFilePath = join3(dirPath, SKILL_FILENAME);
629
+ if (existsSync3(skillFilePath)) {
630
+ try {
631
+ const content = readFileSync2(skillFilePath, "utf-8");
632
+ const { data } = parseFrontmatter(content);
633
+ const skillSlug = data.slug ?? "";
634
+ const organization = data.organization ?? "";
635
+ if (skillSlug && organization) {
636
+ const files = scanSkillFiles(dirPath);
637
+ skills.push({
638
+ slug: skillSlug,
639
+ name: data.name || skillSlug,
640
+ organization,
641
+ description: data.description,
642
+ installedAt: data.installedAt || "",
643
+ version: data.version,
644
+ filesChecksum: data.filesChecksum,
645
+ path: skillFilePath,
646
+ agent,
647
+ scope,
648
+ files
649
+ });
650
+ }
651
+ } catch {
652
+ }
653
+ continue;
654
+ }
655
+ const skillFiles = readdirSync(dirPath, { withFileTypes: true });
656
+ const organizationSlug = entry.name;
657
+ for (const skillFile of skillFiles) {
658
+ if (!skillFile.isFile() || !skillFile.name.endsWith(".md")) continue;
659
+ const skillPath = join3(dirPath, skillFile.name);
660
+ try {
661
+ const content = readFileSync2(skillPath, "utf-8");
662
+ const { data } = parseFrontmatter(content);
663
+ const skillSlug = data.slug || basename(skillFile.name, ".md");
664
+ const skillName = data.name || skillSlug;
665
+ const organization = data.organization || organizationSlug;
666
+ skills.push({
667
+ slug: skillSlug,
668
+ name: skillName,
669
+ organization,
670
+ description: data.description,
671
+ installedAt: data.installedAt || "",
672
+ version: data.version,
673
+ path: skillPath,
674
+ agent,
675
+ scope,
676
+ files: []
677
+ // Legacy format doesn't support files
678
+ });
679
+ } catch {
680
+ }
681
+ }
682
+ }
683
+ } catch {
684
+ }
685
+ return skills;
686
+ }
687
+ function scanInstalledSkills(options = {}) {
688
+ const skills = [];
689
+ const projectDir = process.cwd();
690
+ const globalDir = homedir2();
691
+ const agentsToScan = options.agents ? AGENTS.filter((a) => options.agents.includes(a.id)) : AGENTS;
692
+ for (const agent of agentsToScan) {
693
+ if (!options.scope || options.scope === "project") {
694
+ const projectAgentDir = join3(projectDir, agent.dir);
695
+ const projectSkills = scanAgentDirectory(
696
+ projectAgentDir,
697
+ agent,
698
+ "project"
699
+ );
700
+ skills.push(...projectSkills);
701
+ }
702
+ if (!options.scope || options.scope === "global") {
703
+ const globalAgentDir = join3(globalDir, agent.dir);
704
+ const globalSkills = scanAgentDirectory(globalAgentDir, agent, "global");
705
+ skills.push(...globalSkills);
706
+ }
707
+ }
708
+ if (options.organization) {
709
+ return skills.filter((s) => s.organization === options.organization);
710
+ }
711
+ return skills;
712
+ }
713
+ function groupSkillsByOrganization(skills) {
714
+ const groups = /* @__PURE__ */ new Map();
715
+ for (const skill of skills) {
716
+ const existing = groups.get(skill.organization) || [];
717
+ existing.push(skill);
718
+ groups.set(skill.organization, existing);
719
+ }
720
+ return groups;
721
+ }
722
+ function groupSkillsByAgent(skills) {
723
+ const groups = /* @__PURE__ */ new Map();
724
+ for (const skill of skills) {
725
+ const key = skill.agent.id;
726
+ const existing = groups.get(key) || [];
727
+ existing.push(skill);
728
+ groups.set(key, existing);
729
+ }
730
+ return groups;
731
+ }
732
+ function findSkillInstances(organizationSlug, skillSlug, options = {}) {
733
+ const skills = scanInstalledSkills({
734
+ ...options,
735
+ organization: organizationSlug
736
+ });
737
+ return skills.filter((s) => s.slug === skillSlug);
738
+ }
739
+ function deduplicateSkills(skills) {
740
+ const skillMap = /* @__PURE__ */ new Map();
741
+ for (const skill of skills) {
742
+ const key = `${skill.organization}:${skill.slug}`;
743
+ const existing = skillMap.get(key);
744
+ if (existing) {
745
+ if (!existing.agents.some((a) => a.id === skill.agent.id)) {
746
+ existing.agents.push(skill.agent);
747
+ }
748
+ existing.paths.push(skill.path);
749
+ if (skill.installedAt && skill.installedAt > existing.installedAt) {
750
+ existing.installedAt = skill.installedAt;
751
+ existing.files = skill.files;
752
+ existing.filesChecksum = skill.filesChecksum;
753
+ }
754
+ if (skill.scope === "project") {
755
+ existing.scope = "project";
756
+ }
757
+ if (skill.version && (!existing.version || skill.version > existing.version)) {
758
+ existing.version = skill.version;
759
+ }
760
+ } else {
761
+ skillMap.set(key, {
762
+ slug: skill.slug,
763
+ name: skill.name,
764
+ organization: skill.organization,
765
+ description: skill.description,
766
+ installedAt: skill.installedAt,
767
+ version: skill.version,
768
+ filesChecksum: skill.filesChecksum,
769
+ agents: [skill.agent],
770
+ paths: [skill.path],
771
+ scope: skill.scope,
772
+ files: skill.files
773
+ });
774
+ }
775
+ }
776
+ return Array.from(skillMap.values());
777
+ }
778
+ function scanUniqueSkills(options = {}) {
779
+ const allSkills = scanInstalledSkills(options);
780
+ return deduplicateSkills(allSkills);
781
+ }
782
+
783
+ // src/utils/banner.ts
784
+ import pc from "picocolors";
785
+ function printBanner() {
786
+ const banner = `
787
+ ${pc.cyan("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
788
+ ${pc.cyan("\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D")}
789
+ ${pc.cyan("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
790
+ ${pc.cyan("\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551")}
791
+ ${pc.cyan("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551")}
792
+ ${pc.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
793
+ ${pc.cyan("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
794
+ ${pc.cyan("\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}
795
+ ${pc.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
796
+ ${pc.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}
797
+ ${pc.cyan(" \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551")}
798
+ ${pc.cyan(" \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D")}
799
+ `;
800
+ console.log(banner);
801
+ }
802
+ function printCompactBanner() {
803
+ console.log();
804
+ console.log(pc.bold(pc.cyan(" SkillsTogether")));
805
+ console.log(pc.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
806
+ console.log();
807
+ }
808
+
809
+ // src/utils/paths.ts
810
+ import { homedir as homedir3 } from "os";
811
+ function shortenPath(fullPath) {
812
+ const home = homedir3();
813
+ if (fullPath.startsWith(home)) {
814
+ return fullPath.replace(home, "~");
815
+ }
816
+ return fullPath;
817
+ }
818
+
819
+ // src/utils/prompts.ts
820
+ import * as p from "@clack/prompts";
821
+ import { homedir as homedir4 } from "os";
822
+ import { join as join4 } from "path";
823
+ var SCOPE_CONFIGS = {
824
+ project: {
825
+ label: "Project",
826
+ hint: "Install in current directory (committed with your project)",
827
+ getBaseDir: () => process.cwd()
828
+ },
829
+ global: {
830
+ label: "Global",
831
+ hint: "Install in home directory (available across all projects)",
832
+ getBaseDir: () => homedir4()
833
+ }
834
+ };
835
+ async function selectAgents(skipPrompts) {
836
+ if (skipPrompts) {
837
+ return AGENTS.filter((a) => a.id === "cursor");
838
+ }
839
+ const selected = await p.multiselect({
840
+ message: "Which agents do you want to install to?",
841
+ options: AGENTS.map((agent) => ({
842
+ value: agent.id,
843
+ label: agent.name,
844
+ hint: agent.dir
845
+ })),
846
+ required: true
847
+ });
848
+ if (p.isCancel(selected)) {
849
+ p.cancel("Installation cancelled");
850
+ process.exit(0);
851
+ }
852
+ const selectedIds = selected;
853
+ return AGENTS.filter((a) => selectedIds.includes(a.id));
854
+ }
855
+ async function selectInstallationScope(skipPrompts, globalFlag, customDir) {
856
+ if (customDir) {
857
+ customDirSchema.parse(customDir);
858
+ const fullPath = customDir.startsWith("/") ? customDir : join4(process.cwd(), customDir);
859
+ return { baseDir: fullPath, scopeLabel: "Custom" };
860
+ }
861
+ if (globalFlag) {
862
+ return {
863
+ baseDir: SCOPE_CONFIGS.global.getBaseDir(),
864
+ scopeLabel: SCOPE_CONFIGS.global.label
865
+ };
866
+ }
867
+ if (skipPrompts) {
868
+ return {
869
+ baseDir: SCOPE_CONFIGS.project.getBaseDir(),
870
+ scopeLabel: SCOPE_CONFIGS.project.label
871
+ };
872
+ }
873
+ const scope = await p.select({
874
+ message: "Installation scope",
875
+ options: [
876
+ {
877
+ value: "project",
878
+ label: SCOPE_CONFIGS.project.label,
879
+ hint: SCOPE_CONFIGS.project.hint
880
+ },
881
+ {
882
+ value: "global",
883
+ label: SCOPE_CONFIGS.global.label,
884
+ hint: SCOPE_CONFIGS.global.hint
885
+ }
886
+ ]
887
+ });
888
+ if (p.isCancel(scope)) {
889
+ p.cancel("Installation cancelled");
890
+ process.exit(0);
891
+ }
892
+ const selectedScope = scope;
893
+ return {
894
+ baseDir: SCOPE_CONFIGS[selectedScope].getBaseDir(),
895
+ scopeLabel: SCOPE_CONFIGS[selectedScope].label
896
+ };
897
+ }
898
+
899
+ // src/utils/ui.ts
900
+ import pc2 from "picocolors";
901
+ function stripAnsi(str) {
902
+ return str.replace(
903
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
904
+ ""
905
+ );
906
+ }
907
+ function createBox(title, lines) {
908
+ const maxLineLength = Math.max(
909
+ title.length,
910
+ ...lines.map((l) => stripAnsi(l).length)
911
+ );
912
+ const boxWidth = Math.max(maxLineLength + 4, 60);
913
+ const horizontalLine = "\u2500".repeat(boxWidth - 2);
914
+ const padLine = (line) => {
915
+ const visibleLength = stripAnsi(line).length;
916
+ const padding = boxWidth - 4 - visibleLength;
917
+ return `\u2502 ${line}${" ".repeat(Math.max(0, padding))} \u2502`;
918
+ };
919
+ const output = [
920
+ `\u256D\u2500${title}${"\u2500".repeat(boxWidth - 3 - title.length)}\u256E`,
921
+ "\u2502" + " ".repeat(boxWidth - 2) + "\u2502",
922
+ ...lines.map(padLine),
923
+ "\u2502" + " ".repeat(boxWidth - 2) + "\u2502",
924
+ `\u251C${horizontalLine}\u256F`
925
+ ];
926
+ return output.join("\n");
927
+ }
928
+ function showFinalSummaryMultiAgent(allResults, scopeLabel) {
929
+ const summaryLines = [];
930
+ let totalSuccessful = 0;
931
+ let totalSkipped = 0;
932
+ for (const { agent, results } of allResults) {
933
+ const successful = results.filter((r) => r.success && !r.skipped);
934
+ const skipped = results.filter((r) => r.success && r.skipped);
935
+ const failed = results.filter((r) => !r.success);
936
+ totalSuccessful += successful.length;
937
+ totalSkipped += skipped.length;
938
+ summaryLines.push(`${pc2.cyan(agent.name)}`);
939
+ for (const result of successful) {
940
+ summaryLines.push(` ${pc2.green("\u2713")} ${shortenPath(result.path)}`);
941
+ }
942
+ for (const result of skipped) {
943
+ summaryLines.push(
944
+ ` ${pc2.yellow("\u25CB")} ${shortenPath(result.path)} ${pc2.dim("(skipped)")}`
945
+ );
946
+ }
947
+ for (const result of failed) {
948
+ summaryLines.push(
949
+ ` ${pc2.red("\u2717")} ${shortenPath(result.path)} ${pc2.dim(`(${result.error})`)}`
950
+ );
951
+ }
952
+ }
953
+ if (summaryLines.length > 0) {
954
+ summaryLines.push("");
955
+ summaryLines.push(`Scope: ${pc2.cyan(scopeLabel)}`);
956
+ }
957
+ const agentCount = allResults.length;
958
+ const title = totalSuccessful > 0 ? ` Installed ${totalSuccessful} skill${totalSuccessful !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""} ` : totalSkipped > 0 ? ` Skipped ${totalSkipped} skill${totalSkipped !== 1 ? "s" : ""} ` : ` Failed to install `;
959
+ const summaryBox = createBox(title, summaryLines);
960
+ console.log(pc2.cyan("\u25C7") + summaryBox.slice(1));
961
+ console.log("\u2502");
962
+ }
963
+
964
+ // src/commands/add.ts
965
+ function mapScopeLabel(scopeLabel) {
966
+ if (scopeLabel === "Global") return "global";
967
+ return "project";
968
+ }
969
+ function buildDownloadEvents(allResults, skills, scopeLabel) {
970
+ const events = [];
971
+ const scope = mapScopeLabel(scopeLabel);
972
+ for (const { agent, results } of allResults) {
973
+ for (let i = 0; i < results.length && i < skills.length; i++) {
974
+ const result = results[i];
975
+ const skill = skills[i];
976
+ if (result.success && !result.skipped) {
977
+ events.push({
978
+ skillId: skill.id,
979
+ agent: agent.id,
980
+ scope
981
+ });
982
+ }
983
+ }
984
+ }
985
+ return events;
986
+ }
987
+ var addCommand = new Command("add").description("Install skills from an organization").argument("<target>", "Organization slug or organization/skill slug").option("-d, --dir <path>", "Custom installation directory").option("-f, --force", "Overwrite existing skill files", false).option("-y, --yes", "Skip interactive prompts and install all skills", false).option("--skill <slug>", "Install a specific skill by slug").option("--global", "Install globally (in home directory)", false).action(
988
+ async (target, options) => {
989
+ printBanner();
990
+ p2.intro(pc3.bgCyan(pc3.black(" npx skillstogether ")));
991
+ const token = getToken();
992
+ if (!token) {
993
+ p2.log.error(
994
+ `Not authenticated. Run ${pc3.cyan("npx skillstogether auth login")} first.`
995
+ );
996
+ p2.outro(pc3.red("Authentication required"));
997
+ process.exit(1);
998
+ }
999
+ let parsed;
1000
+ try {
1001
+ parsed = parseAddTarget(target);
1002
+ } catch (err) {
1003
+ const msg = err instanceof Error && "message" in err ? err.message : "Invalid target format";
1004
+ p2.log.error(msg);
1005
+ p2.outro(pc3.red("Invalid format"));
1006
+ process.exit(1);
1007
+ }
1008
+ if (parsed.skillSlug !== void 0) {
1009
+ await installSingleSkill(
1010
+ parsed.organizationSlug,
1011
+ parsed.skillSlug,
1012
+ options
1013
+ );
1014
+ } else if (options.skill) {
1015
+ const skillResult = skillSlugSchema.safeParse(options.skill);
1016
+ if (!skillResult.success) {
1017
+ const msg = skillResult.error.errors?.[0]?.message ?? skillResult.error.message ?? "Invalid skill slug";
1018
+ p2.log.error(msg);
1019
+ p2.outro(pc3.red("Invalid format"));
1020
+ process.exit(1);
1021
+ }
1022
+ await installSingleSkill(
1023
+ parsed.organizationSlug,
1024
+ skillResult.data,
1025
+ options
1026
+ );
1027
+ } else {
1028
+ await installOrganizationSkills(parsed.organizationSlug, options);
1029
+ }
1030
+ }
1031
+ );
1032
+ async function installOrganizationSkills(organizationSlug, options) {
1033
+ const s = p2.spinner();
1034
+ s.start(`Fetching skills from ${pc3.cyan(organizationSlug)}`);
1035
+ let skills;
1036
+ try {
1037
+ skills = await fetchOrganizationSkills(organizationSlug);
1038
+ } catch (error) {
1039
+ s.stop("Failed to fetch skills");
1040
+ p2.log.error(pc3.red(formatErrorMessage(error)));
1041
+ p2.outro(pc3.red("Failed"));
1042
+ process.exit(1);
1043
+ }
1044
+ if (skills.length === 0) {
1045
+ s.stop(`No skills found in organization ${pc3.cyan(organizationSlug)}`);
1046
+ p2.outro(pc3.yellow("No skills available"));
1047
+ return;
1048
+ }
1049
+ s.stop(
1050
+ `Found ${pc3.green(skills.length.toString())} skill${skills.length > 1 ? "s" : ""}`
1051
+ );
1052
+ const installedSkills = scanInstalledSkills({
1053
+ organization: organizationSlug
1054
+ });
1055
+ const installedSlugs = new Set(installedSkills.map((s2) => s2.slug));
1056
+ let updatesAvailable = /* @__PURE__ */ new Set();
1057
+ if (installedSkills.length > 0) {
1058
+ try {
1059
+ const installedInfo = installedSkills.map((s2) => ({
1060
+ slug: s2.slug,
1061
+ installedAt: s2.installedAt
1062
+ }));
1063
+ const updateResult = await checkForUpdates(
1064
+ organizationSlug,
1065
+ installedInfo
1066
+ );
1067
+ updatesAvailable = new Set(
1068
+ updateResult.updates.filter((u) => u.hasUpdate).map((u) => u.slug)
1069
+ );
1070
+ } catch {
1071
+ }
1072
+ }
1073
+ const installedCount = skills.filter((s2) => installedSlugs.has(s2.slug)).length;
1074
+ const newCount = skills.length - installedCount;
1075
+ const updatesCount = updatesAvailable.size;
1076
+ p2.log.info(`Organization: ${pc3.cyan(organizationSlug)}`);
1077
+ if (installedCount > 0 || updatesCount > 0) {
1078
+ const parts = [];
1079
+ if (installedCount > 0) {
1080
+ parts.push(`${pc3.dim(`${installedCount} already installed`)}`);
1081
+ }
1082
+ if (updatesCount > 0) {
1083
+ parts.push(`${pc3.yellow(`${updatesCount} with updates available`)}`);
1084
+ }
1085
+ if (newCount > 0) {
1086
+ parts.push(`${pc3.green(`${newCount} new`)}`);
1087
+ }
1088
+ p2.log.info(` ${parts.join(", ")}`);
1089
+ }
1090
+ let selectedSkills;
1091
+ if (options.yes) {
1092
+ selectedSkills = skills;
1093
+ p2.log.step(
1094
+ `Selected ${pc3.green("all")} ${skills.length} skill${skills.length > 1 ? "s" : ""}`
1095
+ );
1096
+ } else {
1097
+ const installChoice = await p2.select({
1098
+ message: "Which skills do you want to install?",
1099
+ options: [
1100
+ {
1101
+ value: "all",
1102
+ label: "All skills",
1103
+ hint: `Install all ${skills.length} skills`
1104
+ },
1105
+ {
1106
+ value: "select",
1107
+ label: "Select specific skills",
1108
+ hint: "Choose which skills to install"
1109
+ }
1110
+ ]
1111
+ });
1112
+ if (p2.isCancel(installChoice)) {
1113
+ p2.cancel("Installation cancelled");
1114
+ process.exit(0);
1115
+ }
1116
+ if (installChoice === "all") {
1117
+ selectedSkills = skills;
1118
+ p2.log.step(
1119
+ `Selected ${pc3.green("all")} ${skills.length} skill${skills.length > 1 ? "s" : ""}`
1120
+ );
1121
+ } else {
1122
+ const selected = await p2.multiselect({
1123
+ message: "Select skills to install:",
1124
+ options: skills.map((skill) => {
1125
+ const isInstalled = installedSlugs.has(skill.slug);
1126
+ const hasUpdate = updatesAvailable.has(skill.slug);
1127
+ let hint = skill.description || void 0;
1128
+ if (isInstalled && hasUpdate) {
1129
+ hint = `${pc3.yellow("update available")}${hint ? ` - ${hint}` : ""}`;
1130
+ } else if (isInstalled) {
1131
+ hint = `${pc3.dim("installed")}${hint ? ` - ${hint}` : ""}`;
1132
+ }
1133
+ return {
1134
+ value: skill.slug,
1135
+ label: skill.name,
1136
+ hint
1137
+ };
1138
+ }),
1139
+ required: true
1140
+ });
1141
+ if (p2.isCancel(selected)) {
1142
+ p2.cancel("Installation cancelled");
1143
+ process.exit(0);
1144
+ }
1145
+ selectedSkills = skills.filter(
1146
+ (sk) => selected.includes(sk.slug)
1147
+ );
1148
+ p2.log.step(
1149
+ `Selected ${pc3.green(selectedSkills.length.toString())} skill${selectedSkills.length > 1 ? "s" : ""}`
1150
+ );
1151
+ }
1152
+ }
1153
+ const selectedAgents = await selectAgents(options.yes);
1154
+ if (!options.yes) {
1155
+ p2.log.step(
1156
+ `Selected ${pc3.green(selectedAgents.length.toString())} agent${selectedAgents.length > 1 ? "s" : ""}: ${selectedAgents.map((a) => a.name).join(", ")}`
1157
+ );
1158
+ }
1159
+ const { baseDir, scopeLabel } = await selectInstallationScope(
1160
+ options.yes,
1161
+ options.global,
1162
+ options.dir
1163
+ );
1164
+ if (!options.yes) {
1165
+ console.log();
1166
+ const summaryLines = [];
1167
+ for (const agent of selectedAgents) {
1168
+ const agentDir = join5(baseDir, agent.dir);
1169
+ summaryLines.push(`${pc3.cyan(agent.name)} (${agent.dir})`);
1170
+ for (const skill of selectedSkills) {
1171
+ const skillPath = shortenPath(
1172
+ join5(
1173
+ agentDir,
1174
+ SKILL_FOLDER_PATTERN(organizationSlug, skill.slug),
1175
+ SKILL_FILENAME
1176
+ )
1177
+ );
1178
+ summaryLines.push(` ${skillPath}`);
1179
+ }
1180
+ }
1181
+ summaryLines.push("");
1182
+ summaryLines.push(`Scope: ${pc3.cyan(scopeLabel)}`);
1183
+ const summaryBox = createBox(` Installation Summary `, summaryLines);
1184
+ console.log(pc3.cyan("\u25C7") + summaryBox.slice(1));
1185
+ console.log("\u2502");
1186
+ const confirmed = await p2.select({
1187
+ message: "Proceed with installation?",
1188
+ options: [
1189
+ { value: true, label: "Yes" },
1190
+ { value: false, label: "No" }
1191
+ ]
1192
+ });
1193
+ if (p2.isCancel(confirmed) || !confirmed) {
1194
+ p2.cancel("Installation cancelled");
1195
+ process.exit(0);
1196
+ }
1197
+ }
1198
+ const installSpinner = p2.spinner();
1199
+ installSpinner.start(
1200
+ `Installing ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""} to ${selectedAgents.length} agent${selectedAgents.length > 1 ? "s" : ""}...`
1201
+ );
1202
+ const allResults = [];
1203
+ for (const agent of selectedAgents) {
1204
+ const agentDir = join5(baseDir, agent.dir);
1205
+ const installOptions = {
1206
+ dir: agentDir,
1207
+ force: options.force
1208
+ };
1209
+ const results = await installSkills(
1210
+ selectedSkills,
1211
+ organizationSlug,
1212
+ installOptions
1213
+ );
1214
+ allResults.push({ agent, results });
1215
+ }
1216
+ installSpinner.stop("Installation complete");
1217
+ const downloadEvents = buildDownloadEvents(
1218
+ allResults,
1219
+ selectedSkills,
1220
+ scopeLabel
1221
+ );
1222
+ trackDownloads(downloadEvents);
1223
+ console.log();
1224
+ showFinalSummaryMultiAgent(allResults, scopeLabel);
1225
+ p2.outro(pc3.dim("Done!"));
1226
+ }
1227
+ async function installSingleSkill(organizationSlug, skillSlug, options) {
1228
+ const s = p2.spinner();
1229
+ s.start(`Fetching skill ${pc3.cyan(`${organizationSlug}/${skillSlug}`)}`);
1230
+ let skill;
1231
+ try {
1232
+ skill = await fetchSkill(organizationSlug, skillSlug);
1233
+ } catch (error) {
1234
+ s.stop("Failed to fetch skill");
1235
+ p2.log.error(pc3.red(formatErrorMessage(error)));
1236
+ p2.outro(pc3.red("Failed"));
1237
+ process.exit(1);
1238
+ }
1239
+ s.stop(`Found skill: ${pc3.cyan(skill.name)}`);
1240
+ p2.log.info(`Organization: ${pc3.cyan(organizationSlug)}`);
1241
+ p2.log.info(`Skill: ${pc3.cyan(skill.name)}`);
1242
+ if (skill.description) {
1243
+ p2.log.info(`Description: ${pc3.dim(skill.description)}`);
1244
+ }
1245
+ const selectedAgents = await selectAgents(options.yes);
1246
+ if (!options.yes) {
1247
+ p2.log.step(
1248
+ `Selected ${pc3.green(selectedAgents.length.toString())} agent${selectedAgents.length > 1 ? "s" : ""}: ${selectedAgents.map((a) => a.name).join(", ")}`
1249
+ );
1250
+ }
1251
+ const { baseDir, scopeLabel } = await selectInstallationScope(
1252
+ options.yes,
1253
+ options.global,
1254
+ options.dir
1255
+ );
1256
+ if (!options.yes) {
1257
+ console.log();
1258
+ const summaryLines = [];
1259
+ for (const agent of selectedAgents) {
1260
+ const agentDir = join5(baseDir, agent.dir);
1261
+ const skillPath = shortenPath(
1262
+ join5(
1263
+ agentDir,
1264
+ SKILL_FOLDER_PATTERN(organizationSlug, skill.slug),
1265
+ SKILL_FILENAME
1266
+ )
1267
+ );
1268
+ summaryLines.push(`${pc3.cyan(agent.name)}: ${skillPath}`);
1269
+ }
1270
+ summaryLines.push("");
1271
+ summaryLines.push(`Scope: ${pc3.cyan(scopeLabel)}`);
1272
+ const summaryBox = createBox(` Installation Summary `, summaryLines);
1273
+ console.log(pc3.cyan("\u25C7") + summaryBox.slice(1));
1274
+ console.log("\u2502");
1275
+ const confirmed = await p2.select({
1276
+ message: "Proceed with installation?",
1277
+ options: [
1278
+ { value: true, label: "Yes" },
1279
+ { value: false, label: "No" }
1280
+ ]
1281
+ });
1282
+ if (p2.isCancel(confirmed) || !confirmed) {
1283
+ p2.cancel("Installation cancelled");
1284
+ process.exit(0);
1285
+ }
1286
+ }
1287
+ const installSpinner = p2.spinner();
1288
+ installSpinner.start(
1289
+ `Installing ${pc3.cyan(skill.name)} to ${selectedAgents.length} agent${selectedAgents.length > 1 ? "s" : ""}...`
1290
+ );
1291
+ const allResults = [];
1292
+ for (const agent of selectedAgents) {
1293
+ const agentDir = join5(baseDir, agent.dir);
1294
+ const installOptions = {
1295
+ dir: agentDir,
1296
+ force: options.force
1297
+ };
1298
+ const result = await installSkill(skill, organizationSlug, installOptions);
1299
+ allResults.push({ agent, results: [result] });
1300
+ }
1301
+ installSpinner.stop("Installation complete");
1302
+ const downloadEvents = buildDownloadEvents(allResults, [skill], scopeLabel);
1303
+ trackDownloads(downloadEvents);
1304
+ console.log();
1305
+ showFinalSummaryMultiAgent(allResults, scopeLabel);
1306
+ p2.outro(pc3.dim("Done!"));
1307
+ }
1308
+
1309
+ // src/commands/auth.ts
1310
+ import * as p3 from "@clack/prompts";
1311
+ import { Command as Command2 } from "commander";
1312
+ import pc4 from "picocolors";
1313
+
1314
+ // src/utils/browser.ts
1315
+ import { randomBytes } from "crypto";
1316
+ import {
1317
+ createServer
1318
+ } from "http";
1319
+ import open from "open";
1320
+ var AUTH_LISTEN_HOST = "0.0.0.0";
1321
+ var PORTS_TO_TRY = [AUTH_CALLBACK_PORT, ...AUTH_CALLBACK_PORT_FALLBACKS];
1322
+ function escapeHtml(unsafe) {
1323
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1324
+ }
1325
+ async function browserAuth() {
1326
+ return new Promise((resolve, reject) => {
1327
+ const state = randomBytes(16).toString("hex");
1328
+ let boundPort = null;
1329
+ let timeoutId = null;
1330
+ let resolved = false;
1331
+ const cleanup = (result) => {
1332
+ if (resolved) return;
1333
+ resolved = true;
1334
+ if (timeoutId) clearTimeout(timeoutId);
1335
+ setTimeout(() => {
1336
+ server.close();
1337
+ server.closeAllConnections();
1338
+ resolve(result);
1339
+ }, 1e3);
1340
+ };
1341
+ const server = createServer((req, res) => {
1342
+ const port = boundPort ?? AUTH_CALLBACK_PORT;
1343
+ const url = new URL(req.url || "/", `http://${AUTH_LISTEN_HOST}:${port}`);
1344
+ if (url.pathname === "/callback") {
1345
+ const token = url.searchParams.get("token");
1346
+ const error = url.searchParams.get("error");
1347
+ const returnedState = url.searchParams.get("state");
1348
+ if (returnedState !== state) {
1349
+ res.writeHead(400, { "Content-Type": "text/html" });
1350
+ res.end(
1351
+ getErrorHtml("Invalid state parameter. Please try again."),
1352
+ () => {
1353
+ cleanup({ success: false, error: "Invalid state parameter" });
1354
+ }
1355
+ );
1356
+ return;
1357
+ }
1358
+ if (error) {
1359
+ res.writeHead(400, { "Content-Type": "text/html" });
1360
+ res.end(getErrorHtml(error), () => {
1361
+ cleanup({ success: false, error });
1362
+ });
1363
+ return;
1364
+ }
1365
+ if (token) {
1366
+ res.writeHead(200, { "Content-Type": "text/html" });
1367
+ res.end(getSuccessHtml(), () => {
1368
+ cleanup({ success: true, token });
1369
+ });
1370
+ return;
1371
+ }
1372
+ res.writeHead(400, { "Content-Type": "text/html" });
1373
+ res.end(getErrorHtml("No token received"), () => {
1374
+ cleanup({ success: false, error: "No token received" });
1375
+ });
1376
+ } else {
1377
+ res.writeHead(404);
1378
+ res.end("Not found");
1379
+ }
1380
+ });
1381
+ function tryListen(portIndex) {
1382
+ if (portIndex >= PORTS_TO_TRY.length) {
1383
+ reject(new Error("No available port for auth callback"));
1384
+ return;
1385
+ }
1386
+ const port = PORTS_TO_TRY[portIndex];
1387
+ server.listen(port, AUTH_LISTEN_HOST, () => {
1388
+ boundPort = port;
1389
+ const apiUrl = getApiUrl();
1390
+ const authUrl = `${apiUrl}/api/cli/auth?state=${state}&port=${port}`;
1391
+ open(authUrl);
1392
+ });
1393
+ server.once("error", (err) => {
1394
+ if (err.code === "EADDRINUSE" || err.code === "EACCES") {
1395
+ tryListen(portIndex + 1);
1396
+ } else {
1397
+ reject(err);
1398
+ }
1399
+ });
1400
+ }
1401
+ tryListen(0);
1402
+ timeoutId = setTimeout(() => {
1403
+ cleanup({ success: false, error: "Authentication timed out" });
1404
+ }, AUTH_TIMEOUT_MS);
1405
+ });
1406
+ }
1407
+ function getSuccessHtml() {
1408
+ return `
1409
+ <!DOCTYPE html>
1410
+ <html lang="en">
1411
+ <head>
1412
+ <meta charset="utf-8" />
1413
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1414
+ <title>Authentication Successful</title>
1415
+ <style>
1416
+ /* Align with app/globals.css design tokens */
1417
+ :root {
1418
+ --background: oklch(1 0 0);
1419
+ --foreground: oklch(0.145 0 0);
1420
+ --card: oklch(1 0 0);
1421
+ --card-foreground: oklch(0.145 0 0);
1422
+ --primary: oklch(0.205 0 0);
1423
+ --primary-foreground: oklch(0.985 0 0);
1424
+ --muted: oklch(0.97 0 0);
1425
+ --muted-foreground: oklch(0.556 0 0);
1426
+ --border: oklch(0.922 0 0);
1427
+ --radius: 0.625rem;
1428
+ }
1429
+ @media (prefers-color-scheme: dark) {
1430
+ :root {
1431
+ --background: oklch(0.145 0 0);
1432
+ --foreground: oklch(0.985 0 0);
1433
+ --card: oklch(0.205 0 0);
1434
+ --card-foreground: oklch(0.985 0 0);
1435
+ --primary: oklch(0.922 0 0);
1436
+ --primary-foreground: oklch(0.205 0 0);
1437
+ --muted: oklch(0.269 0 0);
1438
+ --muted-foreground: oklch(0.708 0 0);
1439
+ --border: oklch(1 0 0 / 10%);
1440
+ }
1441
+ }
1442
+ * { box-sizing: border-box; }
1443
+ body {
1444
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1445
+ display: flex;
1446
+ justify-content: center;
1447
+ align-items: center;
1448
+ min-height: 100vh;
1449
+ margin: 0;
1450
+ padding: 1rem;
1451
+ background: var(--background);
1452
+ color: var(--foreground);
1453
+ -webkit-font-smoothing: antialiased;
1454
+ }
1455
+ .container {
1456
+ text-align: center;
1457
+ padding: 2rem;
1458
+ max-width: 24rem;
1459
+ background: var(--card);
1460
+ color: var(--card-foreground);
1461
+ border: 1px solid var(--border);
1462
+ border-radius: var(--radius);
1463
+ box-shadow: 0 1px 3px oklch(0 0 0 / 0.08);
1464
+ }
1465
+ h1 {
1466
+ margin: 0 0 0.5rem;
1467
+ font-size: 1.25rem;
1468
+ font-weight: 600;
1469
+ color: var(--foreground);
1470
+ }
1471
+ p {
1472
+ margin: 0;
1473
+ font-size: 0.875rem;
1474
+ color: var(--muted-foreground);
1475
+ line-height: 1.5;
1476
+ }
1477
+ .icon {
1478
+ display: inline-flex;
1479
+ align-items: center;
1480
+ justify-content: center;
1481
+ width: 3rem;
1482
+ height: 3rem;
1483
+ margin: 0 auto 1rem;
1484
+ background: var(--primary);
1485
+ color: var(--primary-foreground);
1486
+ border-radius: calc(var(--radius) + 2px);
1487
+ font-size: 1.5rem;
1488
+ font-weight: 600;
1489
+ }
1490
+ </style>
1491
+ </head>
1492
+ <body>
1493
+ <div class="container">
1494
+ <div class="icon" aria-hidden="true">\u2713</div>
1495
+ <h1>Authentication successful</h1>
1496
+ <p>You can close this window and return to the terminal.</p>
1497
+ </div>
1498
+ </body>
1499
+ </html>
1500
+ `;
1501
+ }
1502
+ function getErrorHtml(error) {
1503
+ return `
1504
+ <!DOCTYPE html>
1505
+ <html lang="en">
1506
+ <head>
1507
+ <meta charset="utf-8" />
1508
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1509
+ <title>Authentication Failed</title>
1510
+ <style>
1511
+ /* Align with app/globals.css design tokens */
1512
+ :root {
1513
+ --background: oklch(1 0 0);
1514
+ --foreground: oklch(0.145 0 0);
1515
+ --card: oklch(1 0 0);
1516
+ --card-foreground: oklch(0.145 0 0);
1517
+ --muted: oklch(0.97 0 0);
1518
+ --muted-foreground: oklch(0.556 0 0);
1519
+ --destructive: oklch(0.577 0.245 27.325);
1520
+ --destructive-foreground: oklch(0.985 0 0);
1521
+ --border: oklch(0.922 0 0);
1522
+ --radius: 0.625rem;
1523
+ }
1524
+ @media (prefers-color-scheme: dark) {
1525
+ :root {
1526
+ --background: oklch(0.145 0 0);
1527
+ --foreground: oklch(0.985 0 0);
1528
+ --card: oklch(0.205 0 0);
1529
+ --card-foreground: oklch(0.985 0 0);
1530
+ --muted: oklch(0.269 0 0);
1531
+ --muted-foreground: oklch(0.708 0 0);
1532
+ --destructive: oklch(0.704 0.191 22.216);
1533
+ --border: oklch(1 0 0 / 10%);
1534
+ }
1535
+ }
1536
+ * { box-sizing: border-box; }
1537
+ body {
1538
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1539
+ display: flex;
1540
+ justify-content: center;
1541
+ align-items: center;
1542
+ min-height: 100vh;
1543
+ margin: 0;
1544
+ padding: 1rem;
1545
+ background: var(--background);
1546
+ color: var(--foreground);
1547
+ -webkit-font-smoothing: antialiased;
1548
+ }
1549
+ .container {
1550
+ text-align: center;
1551
+ padding: 2rem;
1552
+ max-width: 24rem;
1553
+ background: var(--card);
1554
+ color: var(--card-foreground);
1555
+ border: 1px solid var(--border);
1556
+ border-radius: var(--radius);
1557
+ box-shadow: 0 1px 3px oklch(0 0 0 / 0.08);
1558
+ }
1559
+ h1 {
1560
+ margin: 0 0 0.5rem;
1561
+ font-size: 1.25rem;
1562
+ font-weight: 600;
1563
+ color: var(--foreground);
1564
+ }
1565
+ p {
1566
+ margin: 0;
1567
+ font-size: 0.875rem;
1568
+ color: var(--muted-foreground);
1569
+ line-height: 1.5;
1570
+ }
1571
+ .icon {
1572
+ display: inline-flex;
1573
+ align-items: center;
1574
+ justify-content: center;
1575
+ width: 3rem;
1576
+ height: 3rem;
1577
+ margin: 0 auto 1rem;
1578
+ background: var(--destructive);
1579
+ color: var(--destructive-foreground);
1580
+ border-radius: calc(var(--radius) + 2px);
1581
+ font-size: 1.5rem;
1582
+ font-weight: 600;
1583
+ }
1584
+ </style>
1585
+ </head>
1586
+ <body>
1587
+ <div class="container">
1588
+ <div class="icon" aria-hidden="true">\u2717</div>
1589
+ <h1>Authentication failed</h1>
1590
+ <p>${escapeHtml(error)}</p>
1591
+ </div>
1592
+ </body>
1593
+ </html>
1594
+ `;
1595
+ }
1596
+
1597
+ // src/commands/auth.ts
1598
+ var authCommand = new Command2("auth").description(
1599
+ "Manage authentication"
1600
+ );
1601
+ authCommand.command("login").description("Log in via browser").action(async () => {
1602
+ printCompactBanner();
1603
+ p3.intro(pc4.bgCyan(pc4.black(" Authentication ")));
1604
+ const existingToken = getToken();
1605
+ if (existingToken) {
1606
+ const s = p3.spinner();
1607
+ s.start("Checking existing session");
1608
+ const { valid, user } = await verifyToken();
1609
+ if (valid && user) {
1610
+ s.stop(`Already logged in as ${pc4.cyan(user.name)} (${user.email})`);
1611
+ p3.log.info(
1612
+ pc4.dim("Use 'auth logout' to log out and log in as a different user.")
1613
+ );
1614
+ p3.outro(pc4.green("\u2713 Authenticated"));
1615
+ return;
1616
+ }
1617
+ s.stop("Existing session expired");
1618
+ p3.log.info("Logging in again...");
1619
+ }
1620
+ p3.log.step("Opening browser for authentication...");
1621
+ const result = await browserAuth();
1622
+ if (result.success && result.token) {
1623
+ setToken(result.token);
1624
+ const s = p3.spinner();
1625
+ s.start("Verifying authentication");
1626
+ const { valid, user } = await verifyToken();
1627
+ if (valid && user) {
1628
+ s.stop(`Logged in as ${pc4.cyan(user.name)} (${user.email})`);
1629
+ p3.outro(pc4.green("\u2713 Authentication successful!"));
1630
+ } else {
1631
+ s.stop("Authentication completed");
1632
+ p3.outro(pc4.green("\u2713 Authentication successful!"));
1633
+ }
1634
+ } else {
1635
+ p3.log.error(`Authentication failed: ${result.error}`);
1636
+ p3.outro(pc4.red("\u2717 Failed"));
1637
+ process.exit(1);
1638
+ }
1639
+ });
1640
+ authCommand.command("logout").description("Log out and remove stored credentials").action(() => {
1641
+ printCompactBanner();
1642
+ p3.intro(pc4.bgCyan(pc4.black(" Logout ")));
1643
+ const token = getToken();
1644
+ if (!token) {
1645
+ p3.log.warn("Not logged in.");
1646
+ p3.outro(pc4.dim("Nothing to do"));
1647
+ return;
1648
+ }
1649
+ removeToken();
1650
+ p3.log.success("Credentials removed");
1651
+ p3.outro(pc4.green("\u2713 Logged out successfully!"));
1652
+ });
1653
+ authCommand.command("status").description("Check authentication status").action(async () => {
1654
+ printCompactBanner();
1655
+ p3.intro(pc4.bgCyan(pc4.black(" Auth Status ")));
1656
+ const token = getToken();
1657
+ if (!token) {
1658
+ p3.log.warn("Not logged in.");
1659
+ p3.log.info(pc4.dim("Run 'auth login' to authenticate."));
1660
+ p3.outro(pc4.yellow("Not authenticated"));
1661
+ return;
1662
+ }
1663
+ const s = p3.spinner();
1664
+ s.start("Checking authentication status");
1665
+ const { valid, user } = await verifyToken();
1666
+ if (valid && user) {
1667
+ s.stop(`Logged in as ${pc4.cyan(user.name)} (${user.email})`);
1668
+ p3.outro(pc4.green("\u2713 Authenticated"));
1669
+ } else {
1670
+ s.stop("Session expired or invalid");
1671
+ p3.log.info(pc4.dim("Run 'auth login' to authenticate again."));
1672
+ p3.outro(pc4.red("\u2717 Not authenticated"));
1673
+ }
1674
+ });
1675
+
1676
+ // src/commands/doctor.ts
1677
+ import * as p4 from "@clack/prompts";
1678
+ import { Command as Command3 } from "commander";
1679
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1680
+ import { homedir as homedir5 } from "os";
1681
+ import { join as join6 } from "path";
1682
+ import pc5 from "picocolors";
1683
+ var doctorCommand = new Command3("doctor").description("Diagnose and fix common issues").option("--verbose", "Show detailed output").action(async (options) => {
1684
+ printCompactBanner();
1685
+ p4.intro(pc5.bgCyan(pc5.black(" Doctor ")));
1686
+ const checks = [];
1687
+ const configPath = join6(homedir5(), ".skillstogether", "config.json");
1688
+ if (existsSync4(configPath)) {
1689
+ try {
1690
+ const content = readFileSync3(configPath, "utf-8");
1691
+ JSON.parse(content);
1692
+ checks.push({
1693
+ name: "Config file",
1694
+ status: "pass",
1695
+ message: "Config file exists and is valid JSON"
1696
+ });
1697
+ } catch {
1698
+ checks.push({
1699
+ name: "Config file",
1700
+ status: "fail",
1701
+ message: "Config file exists but is not valid JSON",
1702
+ fix: "Delete ~/.skillstogether/config.json and run 'auth login' again"
1703
+ });
1704
+ }
1705
+ } else {
1706
+ checks.push({
1707
+ name: "Config file",
1708
+ status: "warn",
1709
+ message: "Config file does not exist",
1710
+ fix: "Run 'npx skillstogether auth login' to create it"
1711
+ });
1712
+ }
1713
+ const token = getToken();
1714
+ if (token) {
1715
+ const s = p4.spinner();
1716
+ s.start("Verifying authentication token...");
1717
+ const { valid, user } = await verifyToken();
1718
+ if (valid && user) {
1719
+ s.stop("Token verified");
1720
+ checks.push({
1721
+ name: "Authentication",
1722
+ status: "pass",
1723
+ message: `Logged in as ${user.name} (${user.email})`
1724
+ });
1725
+ } else {
1726
+ s.stop("Token verification failed");
1727
+ checks.push({
1728
+ name: "Authentication",
1729
+ status: "fail",
1730
+ message: "Token is expired or invalid",
1731
+ fix: "Run 'npx skillstogether auth login' to re-authenticate"
1732
+ });
1733
+ }
1734
+ } else {
1735
+ checks.push({
1736
+ name: "Authentication",
1737
+ status: "warn",
1738
+ message: "Not logged in",
1739
+ fix: "Run 'npx skillstogether auth login' to authenticate"
1740
+ });
1741
+ }
1742
+ const apiUrl = getApiUrl();
1743
+ checks.push({
1744
+ name: "API URL",
1745
+ status: "pass",
1746
+ message: `Configured: ${apiUrl}`
1747
+ });
1748
+ const projectDir = process.cwd();
1749
+ const globalDir = homedir5();
1750
+ let agentDirsFound = 0;
1751
+ const agentDirDetails = [];
1752
+ for (const agent of AGENTS) {
1753
+ const projectPath = join6(projectDir, agent.dir);
1754
+ const globalPath = join6(globalDir, agent.dir);
1755
+ if (existsSync4(projectPath)) {
1756
+ agentDirsFound++;
1757
+ agentDirDetails.push(`${agent.name} (project): ${projectPath}`);
1758
+ }
1759
+ if (existsSync4(globalPath)) {
1760
+ agentDirsFound++;
1761
+ agentDirDetails.push(`${agent.name} (global): ${globalPath}`);
1762
+ }
1763
+ }
1764
+ if (agentDirsFound > 0) {
1765
+ checks.push({
1766
+ name: "Agent directories",
1767
+ status: "pass",
1768
+ message: `Found ${agentDirsFound} agent director${agentDirsFound !== 1 ? "ies" : "y"}`
1769
+ });
1770
+ } else {
1771
+ checks.push({
1772
+ name: "Agent directories",
1773
+ status: "warn",
1774
+ message: "No agent directories found",
1775
+ fix: "Install skills with 'npx skillstogether add <org-slug>'"
1776
+ });
1777
+ }
1778
+ const skills = scanInstalledSkills({});
1779
+ const skillsWithIssues = [];
1780
+ for (const skill of skills) {
1781
+ try {
1782
+ const content = readFileSync3(skill.path, "utf-8");
1783
+ const { data } = parseFrontmatter(content);
1784
+ if (!data.slug) {
1785
+ skillsWithIssues.push({
1786
+ path: skill.path,
1787
+ issue: "Missing slug in frontmatter"
1788
+ });
1789
+ }
1790
+ if (!data.organization) {
1791
+ skillsWithIssues.push({
1792
+ path: skill.path,
1793
+ issue: "Missing organization in frontmatter"
1794
+ });
1795
+ }
1796
+ if (!data.installedAt) {
1797
+ skillsWithIssues.push({
1798
+ path: skill.path,
1799
+ issue: "Missing installedAt in frontmatter"
1800
+ });
1801
+ }
1802
+ } catch (error) {
1803
+ skillsWithIssues.push({
1804
+ path: skill.path,
1805
+ issue: `Cannot read: ${formatErrorMessage(error)}`
1806
+ });
1807
+ }
1808
+ }
1809
+ if (skills.length === 0) {
1810
+ checks.push({
1811
+ name: "Installed skills",
1812
+ status: "warn",
1813
+ message: "No skills installed",
1814
+ fix: "Install skills with 'npx skillstogether add <org-slug>'"
1815
+ });
1816
+ } else if (skillsWithIssues.length > 0) {
1817
+ checks.push({
1818
+ name: "Installed skills",
1819
+ status: "warn",
1820
+ message: `Found ${skills.length} skills, ${skillsWithIssues.length} with issues`
1821
+ });
1822
+ } else {
1823
+ checks.push({
1824
+ name: "Installed skills",
1825
+ status: "pass",
1826
+ message: `Found ${skills.length} skill${skills.length !== 1 ? "s" : ""}, all valid`
1827
+ });
1828
+ }
1829
+ console.log();
1830
+ p4.log.info("Diagnostic Results:");
1831
+ console.log();
1832
+ const passCount = checks.filter((c) => c.status === "pass").length;
1833
+ const warnCount = checks.filter((c) => c.status === "warn").length;
1834
+ const failCount = checks.filter((c) => c.status === "fail").length;
1835
+ for (const check of checks) {
1836
+ const icon = check.status === "pass" ? pc5.green("\u2713") : check.status === "warn" ? pc5.yellow("!") : pc5.red("\u2717");
1837
+ const statusColor = check.status === "pass" ? pc5.green : check.status === "warn" ? pc5.yellow : pc5.red;
1838
+ console.log(` ${icon} ${pc5.white(check.name)}`);
1839
+ console.log(` ${statusColor(check.message)}`);
1840
+ if (check.fix) {
1841
+ console.log(` ${pc5.dim(`Fix: ${check.fix}`)}`);
1842
+ }
1843
+ console.log();
1844
+ }
1845
+ if (options.verbose) {
1846
+ if (agentDirDetails.length > 0) {
1847
+ console.log(pc5.dim("Agent directories found:"));
1848
+ for (const detail of agentDirDetails) {
1849
+ console.log(pc5.dim(` \u2022 ${detail}`));
1850
+ }
1851
+ console.log();
1852
+ }
1853
+ if (skillsWithIssues.length > 0) {
1854
+ console.log(pc5.dim("Skills with issues:"));
1855
+ for (const { path, issue } of skillsWithIssues) {
1856
+ console.log(pc5.dim(` \u2022 ${path}`));
1857
+ console.log(pc5.dim(` ${issue}`));
1858
+ }
1859
+ console.log();
1860
+ }
1861
+ }
1862
+ const summaryParts = [];
1863
+ if (passCount > 0) summaryParts.push(pc5.green(`${passCount} passed`));
1864
+ if (warnCount > 0) summaryParts.push(pc5.yellow(`${warnCount} warnings`));
1865
+ if (failCount > 0) summaryParts.push(pc5.red(`${failCount} failed`));
1866
+ p4.log.info(`Summary: ${summaryParts.join(", ")}`);
1867
+ if (failCount > 0) {
1868
+ p4.outro(pc5.red("Issues found - see fixes above"));
1869
+ process.exit(1);
1870
+ } else if (warnCount > 0) {
1871
+ p4.outro(pc5.yellow("Some warnings - see suggestions above"));
1872
+ } else {
1873
+ p4.outro(pc5.green("All checks passed!"));
1874
+ }
1875
+ });
1876
+
1877
+ // src/commands/list.ts
1878
+ import * as p5 from "@clack/prompts";
1879
+ import { Command as Command4 } from "commander";
1880
+ import pc6 from "picocolors";
1881
+ function displaySkillsTable(skills) {
1882
+ if (skills.length === 0) {
1883
+ p5.log.info(pc6.dim("No skills installed"));
1884
+ return;
1885
+ }
1886
+ const byOrg = groupSkillsByOrganization(skills);
1887
+ for (const [org, orgSkills] of byOrg) {
1888
+ console.log();
1889
+ console.log(pc6.cyan(` ${org}/`));
1890
+ for (const skill of orgSkills) {
1891
+ const agentBadge = pc6.dim(`[${skill.agent.name}]`);
1892
+ const scopeBadge = skill.scope === "global" ? pc6.yellow("(global)") : pc6.dim("(project)");
1893
+ const versionStr = skill.version ? pc6.dim(`v${skill.version}`) : "";
1894
+ console.log(
1895
+ ` ${pc6.white(skill.name)} ${versionStr} ${agentBadge} ${scopeBadge}`
1896
+ );
1897
+ console.log(pc6.dim(` ${shortenPath(skill.path)}`));
1898
+ }
1899
+ }
1900
+ }
1901
+ function displaySkillsByAgent(skills) {
1902
+ if (skills.length === 0) {
1903
+ p5.log.info(pc6.dim("No skills installed"));
1904
+ return;
1905
+ }
1906
+ const byAgent = groupSkillsByAgent(skills);
1907
+ for (const [agent, agentSkills] of byAgent) {
1908
+ const agentConfig = AGENTS.find((a) => a.id === agent);
1909
+ const agentName = agentConfig?.name || agent;
1910
+ console.log();
1911
+ console.log(pc6.cyan(` ${agentName}`));
1912
+ for (const skill of agentSkills) {
1913
+ const scopeBadge = skill.scope === "global" ? pc6.yellow("(global)") : pc6.dim("(project)");
1914
+ console.log(
1915
+ ` ${pc6.white(`${skill.organization}/${skill.slug}`)} ${scopeBadge}`
1916
+ );
1917
+ }
1918
+ }
1919
+ }
1920
+ var listCommand = new Command4("list").description("List installed skills").option("--global", "Show only globally installed skills").option("--project", "Show only project-installed skills").option("--agent <id>", "Show only skills for a specific agent").option("--by-agent", "Group output by agent instead of organization").option("--json", "Output as JSON").action(
1921
+ async (options) => {
1922
+ printCompactBanner();
1923
+ p5.intro(pc6.bgCyan(pc6.black(" Installed Skills ")));
1924
+ let scope;
1925
+ if (options.global && !options.project) {
1926
+ scope = "global";
1927
+ } else if (options.project && !options.global) {
1928
+ scope = "project";
1929
+ }
1930
+ const s = p5.spinner();
1931
+ s.start("Scanning for installed skills...");
1932
+ const skills = scanInstalledSkills({
1933
+ scope,
1934
+ agents: options.agent ? [options.agent] : void 0
1935
+ });
1936
+ const uniqueSkills = deduplicateSkills(skills);
1937
+ const uniqueCount = uniqueSkills.length;
1938
+ const totalInstances = skills.length;
1939
+ const countDisplay = uniqueCount === totalInstances ? `${pc6.green(uniqueCount.toString())} skill${uniqueCount !== 1 ? "s" : ""}` : `${pc6.green(uniqueCount.toString())} unique skill${uniqueCount !== 1 ? "s" : ""} (${totalInstances} installations)`;
1940
+ s.stop(`Found ${countDisplay}`);
1941
+ if (skills.length === 0) {
1942
+ p5.log.info(
1943
+ pc6.dim(
1944
+ "No skills found. Install skills with: npx skillstogether add <org-slug>"
1945
+ )
1946
+ );
1947
+ p5.outro(pc6.dim("Done"));
1948
+ return;
1949
+ }
1950
+ if (options.json) {
1951
+ console.log(
1952
+ JSON.stringify(
1953
+ skills.map((s2) => ({
1954
+ slug: s2.slug,
1955
+ name: s2.name,
1956
+ organization: s2.organization,
1957
+ description: s2.description,
1958
+ installedAt: s2.installedAt,
1959
+ version: s2.version,
1960
+ path: s2.path,
1961
+ agent: s2.agent.id,
1962
+ scope: s2.scope
1963
+ })),
1964
+ null,
1965
+ 2
1966
+ )
1967
+ );
1968
+ return;
1969
+ }
1970
+ if (options.byAgent) {
1971
+ displaySkillsByAgent(skills);
1972
+ } else {
1973
+ displaySkillsTable(skills);
1974
+ }
1975
+ console.log();
1976
+ const globalCount = uniqueSkills.filter((s2) => s2.scope === "global").length;
1977
+ const projectCount = uniqueSkills.filter(
1978
+ (s2) => s2.scope === "project"
1979
+ ).length;
1980
+ const orgCount = new Set(uniqueSkills.map((s2) => s2.organization)).size;
1981
+ const agentCount = new Set(skills.map((s2) => s2.agent.id)).size;
1982
+ p5.log.info(
1983
+ pc6.dim(
1984
+ `${uniqueCount} unique skill${uniqueCount !== 1 ? "s" : ""}, ${orgCount} organization${orgCount !== 1 ? "s" : ""}, ${agentCount} agent${agentCount !== 1 ? "s" : ""}, ${globalCount} global, ${projectCount} project`
1985
+ )
1986
+ );
1987
+ p5.outro(pc6.dim("Done"));
1988
+ }
1989
+ );
1990
+
1991
+ // src/commands/status.ts
1992
+ import * as p6 from "@clack/prompts";
1993
+ import { Command as Command5 } from "commander";
1994
+ import pc7 from "picocolors";
1995
+ function formatDate(dateStr) {
1996
+ if (!dateStr) return pc7.dim("unknown");
1997
+ try {
1998
+ const date = new Date(dateStr);
1999
+ return date.toLocaleDateString("en-US", {
2000
+ year: "numeric",
2001
+ month: "short",
2002
+ day: "numeric",
2003
+ hour: "2-digit",
2004
+ minute: "2-digit"
2005
+ });
2006
+ } catch {
2007
+ return pc7.dim("invalid");
2008
+ }
2009
+ }
2010
+ function groupByOrganization(skills) {
2011
+ const groups = /* @__PURE__ */ new Map();
2012
+ for (const skill of skills) {
2013
+ const existing = groups.get(skill.organization) || [];
2014
+ existing.push(skill);
2015
+ groups.set(skill.organization, existing);
2016
+ }
2017
+ return groups;
2018
+ }
2019
+ var statusCommand = new Command5("status").description("Check for skill updates").argument("[org-slug]", "Organization to check (optional)").option("--global", "Check only globally installed skills").option("--project", "Check only project-installed skills").option("--json", "Output as JSON").action(
2020
+ async (organizationSlug, options) => {
2021
+ printCompactBanner();
2022
+ p6.intro(pc7.bgCyan(pc7.black(" Check for Updates ")));
2023
+ const token = getToken();
2024
+ if (!token) {
2025
+ p6.log.error(
2026
+ `Not authenticated. Run ${pc7.cyan("npx skillstogether auth login")} first.`
2027
+ );
2028
+ p6.outro(pc7.red("Authentication required"));
2029
+ process.exit(1);
2030
+ }
2031
+ let scope;
2032
+ if (options.global && !options.project) {
2033
+ scope = "global";
2034
+ } else if (options.project && !options.global) {
2035
+ scope = "project";
2036
+ }
2037
+ const s = p6.spinner();
2038
+ s.start("Scanning installed skills...");
2039
+ const installedSkills = scanUniqueSkills({
2040
+ scope,
2041
+ organization: organizationSlug
2042
+ });
2043
+ if (installedSkills.length === 0) {
2044
+ s.stop("No installed skills found");
2045
+ p6.log.info(
2046
+ pc7.dim("Install skills with: npx skillstogether add <org-slug>")
2047
+ );
2048
+ p6.outro(pc7.dim("Done"));
2049
+ return;
2050
+ }
2051
+ s.stop(
2052
+ `Found ${pc7.green(installedSkills.length.toString())} installed skill${installedSkills.length !== 1 ? "s" : ""}`
2053
+ );
2054
+ const byOrg = groupByOrganization(installedSkills);
2055
+ const checkSpinner = p6.spinner();
2056
+ checkSpinner.start("Checking for updates...");
2057
+ const allUpdates = [];
2058
+ const errors = [];
2059
+ for (const [org, skills] of byOrg) {
2060
+ try {
2061
+ const skillsInfo = skills.map((s2) => ({
2062
+ slug: s2.slug,
2063
+ installedAt: s2.installedAt
2064
+ }));
2065
+ const result = await checkForUpdates(org, skillsInfo);
2066
+ allUpdates.push({ org, updates: result.updates });
2067
+ } catch (error) {
2068
+ errors.push(`${org}: ${formatErrorMessage(error)}`);
2069
+ }
2070
+ }
2071
+ checkSpinner.stop("Update check complete");
2072
+ const outdatedSkills = allUpdates.flatMap(
2073
+ (o) => o.updates.filter((u) => u.hasUpdate)
2074
+ );
2075
+ const upToDateSkills = allUpdates.flatMap(
2076
+ (o) => o.updates.filter((u) => !u.hasUpdate)
2077
+ );
2078
+ if (options.json) {
2079
+ console.log(
2080
+ JSON.stringify(
2081
+ {
2082
+ outdated: outdatedSkills,
2083
+ upToDate: upToDateSkills,
2084
+ errors
2085
+ },
2086
+ null,
2087
+ 2
2088
+ )
2089
+ );
2090
+ return;
2091
+ }
2092
+ if (outdatedSkills.length > 0) {
2093
+ console.log();
2094
+ p6.log.warn(
2095
+ `${pc7.yellow(outdatedSkills.length.toString())} skill${outdatedSkills.length !== 1 ? "s" : ""} ${outdatedSkills.length !== 1 ? "have" : "has"} updates available:`
2096
+ );
2097
+ for (const { org, updates } of allUpdates) {
2098
+ const orgOutdated = updates.filter((u) => u.hasUpdate);
2099
+ if (orgOutdated.length === 0) continue;
2100
+ console.log();
2101
+ console.log(pc7.cyan(` ${org}/`));
2102
+ for (const update of orgOutdated) {
2103
+ console.log(` ${pc7.yellow("\u2191")} ${pc7.white(update.name)}`);
2104
+ console.log(
2105
+ ` Installed: ${pc7.dim(formatDate(update.installedAt))}`
2106
+ );
2107
+ console.log(
2108
+ ` Available: ${pc7.green(formatDate(update.updatedAt))} ${pc7.dim(`(v${update.currentVersion})`)}`
2109
+ );
2110
+ }
2111
+ }
2112
+ console.log();
2113
+ p6.log.info(
2114
+ `Run ${pc7.cyan("npx skillstogether update")} to update all skills`
2115
+ );
2116
+ if (organizationSlug) {
2117
+ p6.log.info(
2118
+ `Or ${pc7.cyan(`npx skillstogether update ${organizationSlug}`)} to update this organization only`
2119
+ );
2120
+ }
2121
+ } else {
2122
+ p6.log.success(
2123
+ `All ${pc7.green(upToDateSkills.length.toString())} skill${upToDateSkills.length !== 1 ? "s are" : " is"} up to date!`
2124
+ );
2125
+ }
2126
+ if (errors.length > 0) {
2127
+ console.log();
2128
+ p6.log.error(
2129
+ `Failed to check ${errors.length} organization${errors.length !== 1 ? "s" : ""}:`
2130
+ );
2131
+ for (const error of errors) {
2132
+ console.log(` ${pc7.red("\u2022")} ${error}`);
2133
+ }
2134
+ }
2135
+ p6.outro(pc7.dim("Done"));
2136
+ }
2137
+ );
2138
+
2139
+ // src/commands/sync.ts
2140
+ import * as p7 from "@clack/prompts";
2141
+ import { Command as Command6 } from "commander";
2142
+ import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
2143
+ import { homedir as homedir6 } from "os";
2144
+ import { dirname as dirname2, join as join7 } from "path";
2145
+ import pc8 from "picocolors";
2146
+ function getDestinationPath(skill, targetAgent, scope) {
2147
+ const baseDir = scope === "global" ? homedir6() : process.cwd();
2148
+ const folderName = SKILL_FOLDER_PATTERN(skill.organization, skill.slug);
2149
+ return join7(baseDir, targetAgent.dir, folderName, SKILL_FILENAME);
2150
+ }
2151
+ function copySkill(sourcePath, destPath) {
2152
+ try {
2153
+ const destDir = dirname2(destPath);
2154
+ if (!existsSync5(destDir)) {
2155
+ mkdirSync3(destDir, { recursive: true });
2156
+ }
2157
+ copyFileSync(sourcePath, destPath);
2158
+ return { success: true };
2159
+ } catch (error) {
2160
+ return {
2161
+ success: false,
2162
+ error: formatErrorMessage(error)
2163
+ };
2164
+ }
2165
+ }
2166
+ var syncCommand = new Command6("sync").description("Sync skills between agents").option("--from <agent>", "Source agent to sync from").option("--to <agent>", "Target agent to sync to (can be repeated)", []).option("--global", "Sync globally installed skills").option("--project", "Sync project-installed skills").option("-y, --yes", "Skip confirmation prompts").option("--dry-run", "Show what would be synced without making changes").action(
2167
+ async (options) => {
2168
+ printCompactBanner();
2169
+ p7.intro(pc8.bgCyan(pc8.black(" Sync Skills ")));
2170
+ let scope;
2171
+ if (options.global && !options.project) {
2172
+ scope = "global";
2173
+ } else if (options.project && !options.global) {
2174
+ scope = "project";
2175
+ }
2176
+ let sourceAgentId = options.from;
2177
+ if (!sourceAgentId) {
2178
+ const selected = await p7.select({
2179
+ message: "Select source agent to sync from:",
2180
+ options: AGENTS.map((agent) => ({
2181
+ value: agent.id,
2182
+ label: agent.name,
2183
+ hint: agent.dir
2184
+ }))
2185
+ });
2186
+ if (p7.isCancel(selected)) {
2187
+ p7.cancel("Sync cancelled");
2188
+ process.exit(0);
2189
+ }
2190
+ sourceAgentId = selected;
2191
+ }
2192
+ agentSchema.parse(sourceAgentId);
2193
+ const sourceAgent = AGENTS.find((a) => a.id === sourceAgentId);
2194
+ if (!sourceAgent) {
2195
+ p7.log.error(`Unknown agent: ${sourceAgentId}`);
2196
+ p7.log.info(`Available agents: ${AGENTS.map((a) => a.id).join(", ")}`);
2197
+ p7.outro(pc8.red("Invalid agent"));
2198
+ process.exit(1);
2199
+ }
2200
+ let targetAgentIds = options.to || [];
2201
+ if (targetAgentIds.length === 0) {
2202
+ const selected = await p7.multiselect({
2203
+ message: "Select target agents to sync to:",
2204
+ options: AGENTS.filter((a) => a.id !== sourceAgentId).map((agent) => ({
2205
+ value: agent.id,
2206
+ label: agent.name,
2207
+ hint: agent.dir
2208
+ })),
2209
+ required: true
2210
+ });
2211
+ if (p7.isCancel(selected)) {
2212
+ p7.cancel("Sync cancelled");
2213
+ process.exit(0);
2214
+ }
2215
+ targetAgentIds = selected;
2216
+ }
2217
+ const targetAgents = [];
2218
+ for (const id of targetAgentIds) {
2219
+ agentSchema.parse(id);
2220
+ const agent = AGENTS.find((a) => a.id === id);
2221
+ if (!agent) {
2222
+ p7.log.error(`Unknown agent: ${id}`);
2223
+ p7.log.info(`Available agents: ${AGENTS.map((a) => a.id).join(", ")}`);
2224
+ p7.outro(pc8.red("Invalid agent"));
2225
+ process.exit(1);
2226
+ }
2227
+ if (agent.id === sourceAgentId) {
2228
+ p7.log.warn(`Skipping source agent as target: ${id}`);
2229
+ continue;
2230
+ }
2231
+ targetAgents.push(agent);
2232
+ }
2233
+ if (targetAgents.length === 0) {
2234
+ p7.log.error("No valid target agents selected");
2235
+ p7.outro(pc8.red("No targets"));
2236
+ process.exit(1);
2237
+ }
2238
+ const s = p7.spinner();
2239
+ s.start(`Scanning skills from ${pc8.cyan(sourceAgent.name)}...`);
2240
+ const skills = scanInstalledSkills({
2241
+ agents: [sourceAgentId],
2242
+ scope
2243
+ });
2244
+ if (skills.length === 0) {
2245
+ s.stop(`No skills found in ${pc8.cyan(sourceAgent.name)}`);
2246
+ p7.outro(pc8.yellow("Nothing to sync"));
2247
+ return;
2248
+ }
2249
+ s.stop(
2250
+ `Found ${pc8.green(skills.length.toString())} skill${skills.length !== 1 ? "s" : ""} in ${pc8.cyan(sourceAgent.name)}`
2251
+ );
2252
+ const operations = [];
2253
+ for (const skill of skills) {
2254
+ for (const target of targetAgents) {
2255
+ const destPath = getDestinationPath(skill, target, skill.scope);
2256
+ const exists = existsSync5(destPath);
2257
+ operations.push({
2258
+ skill,
2259
+ target,
2260
+ destPath,
2261
+ exists
2262
+ });
2263
+ }
2264
+ }
2265
+ console.log();
2266
+ p7.log.info(
2267
+ `Will sync ${pc8.green(skills.length.toString())} skill${skills.length !== 1 ? "s" : ""} to ${pc8.cyan(targetAgents.length.toString())} agent${targetAgents.length !== 1 ? "s" : ""}:`
2268
+ );
2269
+ for (const target of targetAgents) {
2270
+ const targetOps = operations.filter((o) => o.target.id === target.id);
2271
+ const targetNew = targetOps.filter((o) => !o.exists).length;
2272
+ const targetUpdate = targetOps.filter((o) => o.exists).length;
2273
+ console.log(` ${pc8.cyan(target.name)}`);
2274
+ if (targetNew > 0) {
2275
+ console.log(` ${pc8.green(`+ ${targetNew} new`)}`);
2276
+ }
2277
+ if (targetUpdate > 0) {
2278
+ console.log(
2279
+ ` ${pc8.yellow(`\u2191 ${targetUpdate} existing (will overwrite)`)}`
2280
+ );
2281
+ }
2282
+ }
2283
+ console.log();
2284
+ if (options.dryRun) {
2285
+ p7.log.info(pc8.dim("Dry run - no changes made"));
2286
+ if (options.yes || await confirmVerbose()) {
2287
+ for (const op of operations) {
2288
+ const icon = op.exists ? pc8.yellow("\u2191") : pc8.green("+");
2289
+ console.log(
2290
+ ` ${icon} ${op.skill.organization}/${op.skill.slug} \u2192 ${op.target.name}`
2291
+ );
2292
+ console.log(` ${pc8.dim(shortenPath(op.destPath))}`);
2293
+ }
2294
+ }
2295
+ p7.outro(pc8.dim("Done"));
2296
+ return;
2297
+ }
2298
+ if (!options.yes) {
2299
+ const confirmed = await p7.confirm({
2300
+ message: `Sync ${skills.length} skill${skills.length !== 1 ? "s" : ""} to ${targetAgents.length} agent${targetAgents.length !== 1 ? "s" : ""}?`,
2301
+ initialValue: true
2302
+ });
2303
+ if (p7.isCancel(confirmed) || !confirmed) {
2304
+ p7.cancel("Sync cancelled");
2305
+ process.exit(0);
2306
+ }
2307
+ }
2308
+ const syncSpinner = p7.spinner();
2309
+ syncSpinner.start("Syncing skills...");
2310
+ let synced = 0;
2311
+ let failed = 0;
2312
+ const errors = [];
2313
+ for (const op of operations) {
2314
+ const result = copySkill(op.skill.path, op.destPath);
2315
+ if (result.success) {
2316
+ synced++;
2317
+ } else {
2318
+ failed++;
2319
+ errors.push(
2320
+ `${op.skill.organization}/${op.skill.slug} \u2192 ${op.target.name}: ${result.error}`
2321
+ );
2322
+ }
2323
+ }
2324
+ syncSpinner.stop("Sync complete");
2325
+ if (synced > 0) {
2326
+ p7.log.success(
2327
+ `Synced ${pc8.green(synced.toString())} skill${synced !== 1 ? "s" : ""}`
2328
+ );
2329
+ }
2330
+ if (failed > 0) {
2331
+ p7.log.error(
2332
+ `Failed to sync ${pc8.red(failed.toString())} skill${failed !== 1 ? "s" : ""}`
2333
+ );
2334
+ for (const error of errors) {
2335
+ console.log(` ${pc8.red("\u2022")} ${error}`);
2336
+ }
2337
+ }
2338
+ p7.outro(pc8.dim("Done"));
2339
+ }
2340
+ );
2341
+ async function confirmVerbose() {
2342
+ const showDetails = await p7.confirm({
2343
+ message: "Show detailed operations?",
2344
+ initialValue: false
2345
+ });
2346
+ return !p7.isCancel(showDetails) && showDetails;
2347
+ }
2348
+
2349
+ // src/commands/uninstall.ts
2350
+ import * as p8 from "@clack/prompts";
2351
+ import { Command as Command7 } from "commander";
2352
+ import { readdirSync as readdirSync2, rmSync, rmdirSync } from "fs";
2353
+ import { dirname as dirname3 } from "path";
2354
+ import pc9 from "picocolors";
2355
+ function removeSkill(skill) {
2356
+ try {
2357
+ rmSync(skill.path);
2358
+ const orgDir = dirname3(skill.path);
2359
+ try {
2360
+ const remaining = readdirSync2(orgDir);
2361
+ if (remaining.length === 0) {
2362
+ rmdirSync(orgDir);
2363
+ }
2364
+ } catch {
2365
+ }
2366
+ return { success: true };
2367
+ } catch (error) {
2368
+ return {
2369
+ success: false,
2370
+ error: formatErrorMessage(error)
2371
+ };
2372
+ }
2373
+ }
2374
+ var uninstallCommand = new Command7("uninstall").description("Remove installed skills").argument("<target>", "Organization slug or organization/skill slug").option("--all", "Remove all skills from the organization").option("--global", "Remove from global installation only").option("--project", "Remove from project installation only").option("-y, --yes", "Skip confirmation prompts").action(
2375
+ async (target, options) => {
2376
+ printCompactBanner();
2377
+ p8.intro(pc9.bgCyan(pc9.black(" Uninstall Skills ")));
2378
+ let scope;
2379
+ if (options.global && !options.project) {
2380
+ scope = "global";
2381
+ } else if (options.project && !options.global) {
2382
+ scope = "project";
2383
+ }
2384
+ let parsed;
2385
+ try {
2386
+ parsed = parseUninstallTarget(target);
2387
+ } catch (err) {
2388
+ const msg = err instanceof Error && "message" in err ? err.message : "Invalid target format";
2389
+ p8.log.error(msg);
2390
+ p8.outro(pc9.red("Invalid format"));
2391
+ process.exit(1);
2392
+ }
2393
+ if (parsed.skillSlug === void 0) {
2394
+ if (!options.all) {
2395
+ p8.log.error(
2396
+ `To uninstall all skills from ${pc9.cyan(parsed.organizationSlug)}, use the ${pc9.cyan("--all")} flag`
2397
+ );
2398
+ p8.log.info(
2399
+ `Or specify a skill: ${pc9.cyan(`npx skillstogether uninstall ${parsed.organizationSlug}/<skill-slug>`)}`
2400
+ );
2401
+ p8.outro(pc9.red("Aborted"));
2402
+ process.exit(1);
2403
+ }
2404
+ await uninstallOrganization(parsed.organizationSlug, scope, options.yes);
2405
+ } else {
2406
+ await uninstallSkill(
2407
+ parsed.organizationSlug,
2408
+ parsed.skillSlug,
2409
+ scope,
2410
+ options.yes
2411
+ );
2412
+ }
2413
+ }
2414
+ );
2415
+ async function uninstallOrganization(organizationSlug, scope, skipConfirm) {
2416
+ const s = p8.spinner();
2417
+ s.start(`Finding skills from ${pc9.cyan(organizationSlug)}...`);
2418
+ const skills = scanInstalledSkills({ organization: organizationSlug, scope });
2419
+ if (skills.length === 0) {
2420
+ s.stop(`No skills found from ${pc9.cyan(organizationSlug)}`);
2421
+ p8.outro(pc9.yellow("Nothing to uninstall"));
2422
+ return;
2423
+ }
2424
+ s.stop(
2425
+ `Found ${pc9.green(skills.length.toString())} skill${skills.length !== 1 ? "s" : ""} from ${pc9.cyan(organizationSlug)}`
2426
+ );
2427
+ console.log();
2428
+ p8.log.info("Skills to remove:");
2429
+ for (const skill of skills) {
2430
+ const scopeBadge = skill.scope === "global" ? pc9.yellow("(global)") : pc9.dim("(project)");
2431
+ console.log(
2432
+ ` ${pc9.red("\u2022")} ${skill.name} ${pc9.dim(`[${skill.agent.name}]`)} ${scopeBadge}`
2433
+ );
2434
+ }
2435
+ console.log();
2436
+ if (!skipConfirm) {
2437
+ const confirmed = await p8.confirm({
2438
+ message: `Remove all ${skills.length} skill${skills.length !== 1 ? "s" : ""} from ${organizationSlug}?`,
2439
+ initialValue: false
2440
+ });
2441
+ if (p8.isCancel(confirmed) || !confirmed) {
2442
+ p8.cancel("Uninstall cancelled");
2443
+ process.exit(0);
2444
+ }
2445
+ }
2446
+ const removeSpinner = p8.spinner();
2447
+ removeSpinner.start("Removing skills...");
2448
+ let removed = 0;
2449
+ let failed = 0;
2450
+ const errors = [];
2451
+ for (const skill of skills) {
2452
+ const result = removeSkill(skill);
2453
+ if (result.success) {
2454
+ removed++;
2455
+ } else {
2456
+ failed++;
2457
+ errors.push(`${skill.organization}/${skill.slug}: ${result.error}`);
2458
+ }
2459
+ }
2460
+ removeSpinner.stop("Removal complete");
2461
+ if (removed > 0) {
2462
+ p8.log.success(
2463
+ `Removed ${pc9.green(removed.toString())} skill${removed !== 1 ? "s" : ""}`
2464
+ );
2465
+ }
2466
+ if (failed > 0) {
2467
+ p8.log.error(
2468
+ `Failed to remove ${pc9.red(failed.toString())} skill${failed !== 1 ? "s" : ""}`
2469
+ );
2470
+ for (const error of errors) {
2471
+ p8.log.error(pc9.dim(` ${error}`));
2472
+ }
2473
+ }
2474
+ p8.outro(pc9.dim("Done"));
2475
+ }
2476
+ async function uninstallSkill(organizationSlug, skillSlug, scope, skipConfirm) {
2477
+ const s = p8.spinner();
2478
+ s.start(`Finding ${pc9.cyan(`${organizationSlug}/${skillSlug}`)}...`);
2479
+ const instances = findSkillInstances(organizationSlug, skillSlug, { scope });
2480
+ if (instances.length === 0) {
2481
+ s.stop(`Skill ${pc9.cyan(`${organizationSlug}/${skillSlug}`)} not found`);
2482
+ p8.outro(pc9.yellow("Nothing to uninstall"));
2483
+ return;
2484
+ }
2485
+ s.stop(
2486
+ `Found ${pc9.green(instances.length.toString())} instance${instances.length !== 1 ? "s" : ""} of ${pc9.cyan(`${organizationSlug}/${skillSlug}`)}`
2487
+ );
2488
+ console.log();
2489
+ p8.log.info("Instances to remove:");
2490
+ for (const skill of instances) {
2491
+ const scopeBadge = skill.scope === "global" ? pc9.yellow("(global)") : pc9.dim("(project)");
2492
+ console.log(
2493
+ ` ${pc9.red("\u2022")} ${pc9.dim(`[${skill.agent.name}]`)} ${scopeBadge}`
2494
+ );
2495
+ console.log(` ${pc9.dim(shortenPath(skill.path))}`);
2496
+ }
2497
+ console.log();
2498
+ if (!skipConfirm) {
2499
+ const confirmed = await p8.confirm({
2500
+ message: `Remove ${instances.length} instance${instances.length !== 1 ? "s" : ""} of ${organizationSlug}/${skillSlug}?`,
2501
+ initialValue: false
2502
+ });
2503
+ if (p8.isCancel(confirmed) || !confirmed) {
2504
+ p8.cancel("Uninstall cancelled");
2505
+ process.exit(0);
2506
+ }
2507
+ }
2508
+ const removeSpinner = p8.spinner();
2509
+ removeSpinner.start("Removing skill...");
2510
+ let removed = 0;
2511
+ let failed = 0;
2512
+ const errors = [];
2513
+ for (const skill of instances) {
2514
+ const result = removeSkill(skill);
2515
+ if (result.success) {
2516
+ removed++;
2517
+ } else {
2518
+ failed++;
2519
+ errors.push(`${skill.agent.name}: ${result.error}`);
2520
+ }
2521
+ }
2522
+ removeSpinner.stop("Removal complete");
2523
+ if (removed > 0) {
2524
+ p8.log.success(
2525
+ `Removed ${pc9.green(removed.toString())} instance${removed !== 1 ? "s" : ""}`
2526
+ );
2527
+ }
2528
+ if (failed > 0) {
2529
+ p8.log.error(
2530
+ `Failed to remove ${pc9.red(failed.toString())} instance${failed !== 1 ? "s" : ""}`
2531
+ );
2532
+ for (const error of errors) {
2533
+ p8.log.error(pc9.dim(` ${error}`));
2534
+ }
2535
+ }
2536
+ p8.outro(pc9.dim("Done"));
2537
+ }
2538
+
2539
+ // src/commands/update.ts
2540
+ import * as p9 from "@clack/prompts";
2541
+ import { Command as Command8 } from "commander";
2542
+ import { writeFileSync as writeFileSync3 } from "fs";
2543
+ import pc10 from "picocolors";
2544
+ function generateUpdatedContent(skill, organizationSlug) {
2545
+ const frontmatter = generateFrontmatter({
2546
+ name: skill.name,
2547
+ slug: skill.slug,
2548
+ organization: organizationSlug,
2549
+ description: skill.description || void 0,
2550
+ createdBy: skill.createdBy.name,
2551
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2552
+ });
2553
+ return frontmatter + "\n" + (skill.content || "");
2554
+ }
2555
+ var updateCommand = new Command8("update").description("Update installed skills to latest version").argument("[target]", "Organization slug or organization/skill slug").option("--global", "Update only globally installed skills").option("--project", "Update only project-installed skills").option("-y, --yes", "Skip confirmation prompts").option("--dry-run", "Show what would be updated without making changes").action(
2556
+ async (target, options) => {
2557
+ printCompactBanner();
2558
+ p9.intro(pc10.bgCyan(pc10.black(" Update Skills ")));
2559
+ const token = getToken();
2560
+ if (!token) {
2561
+ p9.log.error(
2562
+ `Not authenticated. Run ${pc10.cyan("npx skillstogether auth login")} first.`
2563
+ );
2564
+ p9.outro(pc10.red("Authentication required"));
2565
+ process.exit(1);
2566
+ }
2567
+ let scope;
2568
+ if (options.global && !options.project) {
2569
+ scope = "global";
2570
+ } else if (options.project && !options.global) {
2571
+ scope = "project";
2572
+ }
2573
+ let organizationSlug;
2574
+ let skillSlug;
2575
+ if (target) {
2576
+ try {
2577
+ const parsed = parseUpdateTarget(target);
2578
+ organizationSlug = parsed.organizationSlug;
2579
+ skillSlug = parsed.skillSlug;
2580
+ } catch (err) {
2581
+ const msg = err instanceof Error && "message" in err ? err.message : "Invalid target format";
2582
+ p9.log.error(msg);
2583
+ p9.outro(pc10.red("Invalid format"));
2584
+ process.exit(1);
2585
+ }
2586
+ }
2587
+ const s = p9.spinner();
2588
+ s.start("Scanning installed skills...");
2589
+ const allInstalledSkills = scanInstalledSkills({
2590
+ scope,
2591
+ organization: organizationSlug
2592
+ });
2593
+ let skillsToCheck = allInstalledSkills;
2594
+ if (skillSlug) {
2595
+ skillsToCheck = allInstalledSkills.filter((s2) => s2.slug === skillSlug);
2596
+ }
2597
+ if (skillsToCheck.length === 0) {
2598
+ s.stop("No installed skills found");
2599
+ p9.log.info(
2600
+ pc10.dim("Install skills with: npx skillstogether add <org-slug>")
2601
+ );
2602
+ p9.outro(pc10.dim("Done"));
2603
+ return;
2604
+ }
2605
+ const uniqueSkills = deduplicateSkills(skillsToCheck);
2606
+ s.stop(
2607
+ `Found ${pc10.green(uniqueSkills.length.toString())} installed skill${uniqueSkills.length !== 1 ? "s" : ""}`
2608
+ );
2609
+ const byOrg = /* @__PURE__ */ new Map();
2610
+ for (const skill of uniqueSkills) {
2611
+ const existing = byOrg.get(skill.organization) || [];
2612
+ existing.push(skill);
2613
+ byOrg.set(skill.organization, existing);
2614
+ }
2615
+ const checkSpinner = p9.spinner();
2616
+ checkSpinner.start("Checking for updates...");
2617
+ const skillsToUpdate = [];
2618
+ const errors = [];
2619
+ for (const [org, skills] of byOrg) {
2620
+ try {
2621
+ const skillsInfo = skills.map((s2) => ({
2622
+ slug: s2.slug,
2623
+ installedAt: s2.installedAt
2624
+ }));
2625
+ const result = await checkForUpdates(org, skillsInfo);
2626
+ for (const update of result.updates) {
2627
+ if (update.hasUpdate) {
2628
+ const unique = skills.find((s2) => s2.slug === update.slug);
2629
+ if (unique) {
2630
+ skillsToUpdate.push({
2631
+ unique,
2632
+ remoteVersion: update.currentVersion,
2633
+ updatedAt: update.updatedAt
2634
+ });
2635
+ }
2636
+ }
2637
+ }
2638
+ } catch (error) {
2639
+ errors.push(`${org}: ${formatErrorMessage(error)}`);
2640
+ }
2641
+ }
2642
+ checkSpinner.stop("Update check complete");
2643
+ if (skillsToUpdate.length === 0) {
2644
+ p9.log.success("All skills are up to date!");
2645
+ if (errors.length > 0) {
2646
+ console.log();
2647
+ p9.log.error(
2648
+ `Failed to check ${errors.length} organization${errors.length !== 1 ? "s" : ""}:`
2649
+ );
2650
+ for (const error of errors) {
2651
+ console.log(` ${pc10.red("\u2022")} ${error}`);
2652
+ }
2653
+ }
2654
+ p9.outro(pc10.dim("Done"));
2655
+ return;
2656
+ }
2657
+ const totalInstances = skillsToUpdate.reduce(
2658
+ (sum, s2) => sum + s2.unique.paths.length,
2659
+ 0
2660
+ );
2661
+ console.log();
2662
+ const updateMsg = skillsToUpdate.length === totalInstances ? `${pc10.yellow(skillsToUpdate.length.toString())} skill${skillsToUpdate.length !== 1 ? "s" : ""} will be updated:` : `${pc10.yellow(skillsToUpdate.length.toString())} skill${skillsToUpdate.length !== 1 ? "s" : ""} (${totalInstances} installations) will be updated:`;
2663
+ p9.log.info(updateMsg);
2664
+ for (const { unique, remoteVersion } of skillsToUpdate) {
2665
+ const agentNames = unique.agents.map((a) => a.name).join(", ");
2666
+ const scopeBadge = unique.scope === "global" ? pc10.yellow("(global)") : pc10.dim("(project)");
2667
+ console.log(
2668
+ ` ${pc10.yellow("\u2191")} ${pc10.white(`${unique.organization}/${unique.slug}`)} ${pc10.dim(`v${unique.version || "?"}`)} \u2192 ${pc10.green(`v${remoteVersion}`)} ${pc10.dim(`[${agentNames}]`)} ${scopeBadge}`
2669
+ );
2670
+ }
2671
+ console.log();
2672
+ if (options.dryRun) {
2673
+ p9.log.info(pc10.dim("Dry run - no changes made"));
2674
+ p9.outro(pc10.dim("Done"));
2675
+ return;
2676
+ }
2677
+ if (!options.yes) {
2678
+ const confirmMsg = skillsToUpdate.length === totalInstances ? `Update ${skillsToUpdate.length} skill${skillsToUpdate.length !== 1 ? "s" : ""}?` : `Update ${skillsToUpdate.length} skill${skillsToUpdate.length !== 1 ? "s" : ""} (${totalInstances} installations)?`;
2679
+ const confirmed = await p9.confirm({
2680
+ message: confirmMsg,
2681
+ initialValue: true
2682
+ });
2683
+ if (p9.isCancel(confirmed) || !confirmed) {
2684
+ p9.cancel("Update cancelled");
2685
+ process.exit(0);
2686
+ }
2687
+ }
2688
+ const updateSpinner = p9.spinner();
2689
+ updateSpinner.start("Updating skills...");
2690
+ let updatedSkills = 0;
2691
+ let updatedInstances = 0;
2692
+ let failed = 0;
2693
+ const updateErrors = [];
2694
+ for (const { unique } of skillsToUpdate) {
2695
+ try {
2696
+ const skill = await fetchSkill(unique.organization, unique.slug);
2697
+ const content = generateUpdatedContent(skill, unique.organization);
2698
+ for (const path of unique.paths) {
2699
+ writeFileSync3(path, content, "utf-8");
2700
+ updatedInstances++;
2701
+ }
2702
+ updatedSkills++;
2703
+ } catch (error) {
2704
+ failed++;
2705
+ updateErrors.push(
2706
+ `${unique.organization}/${unique.slug}: ${formatErrorMessage(error)}`
2707
+ );
2708
+ }
2709
+ }
2710
+ updateSpinner.stop("Update complete");
2711
+ if (updatedSkills > 0) {
2712
+ const summaryMsg = updatedSkills === updatedInstances ? `Updated ${pc10.green(updatedSkills.toString())} skill${updatedSkills !== 1 ? "s" : ""}` : `Updated ${pc10.green(updatedSkills.toString())} skill${updatedSkills !== 1 ? "s" : ""} (${updatedInstances} installations)`;
2713
+ p9.log.success(summaryMsg);
2714
+ }
2715
+ if (failed > 0) {
2716
+ p9.log.error(
2717
+ `Failed to update ${pc10.red(failed.toString())} skill${failed !== 1 ? "s" : ""}`
2718
+ );
2719
+ for (const error of updateErrors) {
2720
+ console.log(` ${pc10.red("\u2022")} ${error}`);
2721
+ }
2722
+ }
2723
+ p9.outro(pc10.dim("Done"));
2724
+ }
2725
+ );
2726
+
2727
+ // src/index.ts
2728
+ var program = new Command9();
2729
+ program.name("skillstogether").description("CLI tool to install organization skills").version("0.1.0").hook("preAction", (thisCommand) => {
2730
+ const commandName = thisCommand.name();
2731
+ if (commandName === "skillstogether") {
2732
+ }
2733
+ }).configureHelp({
2734
+ showGlobalOptions: true
2735
+ });
2736
+ var originalHelp = program.helpInformation.bind(program);
2737
+ program.helpInformation = function() {
2738
+ printBanner();
2739
+ return originalHelp();
2740
+ };
2741
+ program.addCommand(authCommand);
2742
+ program.addCommand(addCommand);
2743
+ program.addCommand(doctorCommand);
2744
+ program.addCommand(listCommand);
2745
+ program.addCommand(statusCommand);
2746
+ program.addCommand(syncCommand);
2747
+ program.addCommand(uninstallCommand);
2748
+ program.addCommand(updateCommand);
2749
+ program.parse();