@localskills/cli 0.11.0 → 0.11.2

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.
Files changed (3) hide show
  1. package/README.md +35 -6
  2. package/dist/index.js +799 -188
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,13 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __require = /* @__PURE__ */ ((x3) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x3, {
9
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
- }) : x3)(function(x3) {
11
- if (typeof require !== "undefined") return require.apply(this, arguments);
12
- throw Error('Dynamic require of "' + x3 + '" is not supported');
13
- });
14
- var __commonJS = (cb, mod) => function __require2() {
8
+ var __commonJS = (cb, mod) => function __require() {
15
9
  try {
16
10
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
11
  } catch (e2) {
@@ -1400,6 +1394,9 @@ var ApiClient = class {
1400
1394
  }
1401
1395
  return h;
1402
1396
  }
1397
+ authHeader() {
1398
+ return this.token ? { Authorization: `Bearer ${this.token}` } : {};
1399
+ }
1403
1400
  async handleResponse(res) {
1404
1401
  if (res.status === 401) {
1405
1402
  return {
@@ -1409,19 +1406,45 @@ var ApiClient = class {
1409
1406
  }
1410
1407
  return res.json();
1411
1408
  }
1409
+ /** Turn a transport-level rejection (offline, DNS, TLS, reset, bad body) into a failure response. */
1410
+ transportError(err) {
1411
+ const detail = err instanceof Error ? err.message : String(err);
1412
+ return { success: false, error: `Network error: ${detail}`, networkError: true };
1413
+ }
1412
1414
  async get(path) {
1413
- const res = await fetch(`${this.baseUrl}${path}`, {
1414
- headers: this.headers()
1415
- });
1416
- return this.handleResponse(res);
1415
+ try {
1416
+ const res = await fetch(`${this.baseUrl}${path}`, {
1417
+ headers: this.headers()
1418
+ });
1419
+ return await this.handleResponse(res);
1420
+ } catch (err) {
1421
+ return this.transportError(err);
1422
+ }
1417
1423
  }
1418
1424
  async post(path, body) {
1419
- const res = await fetch(`${this.baseUrl}${path}`, {
1420
- method: "POST",
1421
- headers: this.headers(),
1422
- body: body ? JSON.stringify(body) : void 0
1423
- });
1424
- return this.handleResponse(res);
1425
+ try {
1426
+ const res = await fetch(`${this.baseUrl}${path}`, {
1427
+ method: "POST",
1428
+ headers: this.headers(),
1429
+ body: body ? JSON.stringify(body) : void 0
1430
+ });
1431
+ return await this.handleResponse(res);
1432
+ } catch (err) {
1433
+ return this.transportError(err);
1434
+ }
1435
+ }
1436
+ async postForm(path, form) {
1437
+ try {
1438
+ const res = await fetch(`${this.baseUrl}${path}`, {
1439
+ method: "POST",
1440
+ // No Content-Type — fetch sets the multipart/form-data boundary.
1441
+ headers: this.authHeader(),
1442
+ body: form
1443
+ });
1444
+ return await this.handleResponse(res);
1445
+ } catch (err) {
1446
+ return this.transportError(err);
1447
+ }
1425
1448
  }
1426
1449
  async fetchBinary(url) {
1427
1450
  const headers = {};
@@ -1587,7 +1610,8 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
1587
1610
 
1588
1611
  // src/commands/install.ts
1589
1612
  import { Command as Command2 } from "commander";
1590
- import { mkdirSync as mkdirSync7 } from "fs";
1613
+ import { mkdirSync as mkdirSync7, rmSync as rmSync4, cpSync } from "fs";
1614
+ import { dirname as dirname5 } from "path";
1591
1615
 
1592
1616
  // ../../packages/shared/dist/utils/semver.js
1593
1617
  var SEMVER_RE = /^\d+\.\d+\.\d+$/;
@@ -1624,6 +1648,10 @@ function isGreaterThan(next, prev) {
1624
1648
  }
1625
1649
 
1626
1650
  // ../../packages/shared/dist/utils/index.js
1651
+ var PACKAGE_MAX_COMPRESSED_BYTES = 1 * 1024 * 1024;
1652
+ var PACKAGE_MAX_DECOMPRESSED_BYTES = 1 * 1024 * 1024;
1653
+ var PACKAGE_MAX_FILE_COUNT = 500;
1654
+ var PACKAGE_PREVIEW_BYTES = 2048;
1627
1655
  function titleFromSlug(slug) {
1628
1656
  return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1629
1657
  }
@@ -1660,13 +1688,13 @@ function cancelGuard(value) {
1660
1688
  // src/lib/cache.ts
1661
1689
  import {
1662
1690
  existsSync as existsSync13,
1663
- mkdirSync as mkdirSync5,
1691
+ mkdirSync as mkdirSync6,
1664
1692
  readFileSync as readFileSync4,
1665
- readdirSync,
1666
- writeFileSync as writeFileSync5,
1693
+ readdirSync as readdirSync2,
1694
+ writeFileSync as writeFileSync6,
1667
1695
  rmSync as rmSync3
1668
1696
  } from "fs";
1669
- import { join as join12, resolve as resolve2 } from "path";
1697
+ import { join as join13, resolve as resolve3 } from "path";
1670
1698
  import { homedir as homedir8 } from "os";
1671
1699
 
1672
1700
  // src/lib/installers/cursor.ts
@@ -1722,9 +1750,10 @@ import {
1722
1750
  lstatSync,
1723
1751
  existsSync as existsSync2,
1724
1752
  mkdirSync as mkdirSync2,
1725
- rmSync
1753
+ rmSync,
1754
+ readlinkSync
1726
1755
  } from "fs";
1727
- import { dirname, resolve } from "path";
1756
+ import { dirname, isAbsolute, relative, resolve, sep } from "path";
1728
1757
  function createSymlink(targetPath, linkPath) {
1729
1758
  const absTarget = resolve(targetPath);
1730
1759
  const absLink = resolve(linkPath);
@@ -1753,6 +1782,17 @@ function isSymlink(path) {
1753
1782
  return false;
1754
1783
  }
1755
1784
  }
1785
+ function isSymlinkInto(linkPath, dir) {
1786
+ try {
1787
+ if (!lstatSync(linkPath).isSymbolicLink()) return false;
1788
+ const target = resolve(dirname(linkPath), readlinkSync(linkPath));
1789
+ const base = resolve(dir);
1790
+ const rel = relative(base, target);
1791
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
1792
+ } catch {
1793
+ return false;
1794
+ }
1795
+ }
1756
1796
 
1757
1797
  // src/lib/installers/common.ts
1758
1798
  function safeSlugName(slug) {
@@ -1828,7 +1868,7 @@ var cursorAdapter = {
1828
1868
  };
1829
1869
 
1830
1870
  // src/lib/installers/claude.ts
1831
- import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
1871
+ import { existsSync as existsSync5, rmSync as rmSync2, statSync, readdirSync } from "fs";
1832
1872
  import { join as join4 } from "path";
1833
1873
  import { homedir as homedir3 } from "os";
1834
1874
  var descriptor2 = {
@@ -1847,14 +1887,20 @@ function detect2(projectDir) {
1847
1887
  project: existsSync5(join4(cwd, ".claude"))
1848
1888
  };
1849
1889
  }
1890
+ function claudeBase(scope, projectDir) {
1891
+ return scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
1892
+ }
1850
1893
  function resolvePath2(slug, scope, projectDir, contentType) {
1851
1894
  const safeName = safeSlugName(slug);
1852
- const base = scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
1895
+ const base = claudeBase(scope, projectDir);
1853
1896
  if (contentType === "rule") {
1854
1897
  return join4(base, "rules", `${safeName}.md`);
1855
1898
  }
1856
1899
  return join4(base, "skills", safeName, "SKILL.md");
1857
1900
  }
1901
+ function resolvePackageDir(slug, scope, projectDir, _contentType) {
1902
+ return join4(claudeBase(scope, projectDir), "skills", safeSlugName(slug));
1903
+ }
1858
1904
  function transformContent2(content, skill) {
1859
1905
  if (skill.type === "rule") {
1860
1906
  return toPlainMD(content);
@@ -1866,11 +1912,19 @@ function install2(opts) {
1866
1912
  return installFileOrSymlink(opts, targetPath);
1867
1913
  }
1868
1914
  function uninstall2(installation, _slug) {
1869
- uninstallFile(installation);
1870
- const parentDir = join4(installation.path, "..");
1915
+ const target = installation.path;
1916
+ if (isSymlink(target)) {
1917
+ removeSymlink(target);
1918
+ } else if (existsSync5(target)) {
1919
+ if (statSync(target).isDirectory()) {
1920
+ rmSync2(target, { recursive: true, force: true });
1921
+ } else {
1922
+ rmSync2(target, { force: true });
1923
+ }
1924
+ }
1925
+ const parentDir = join4(target, "..");
1871
1926
  try {
1872
- const { readdirSync: readdirSync3 } = __require("fs");
1873
- if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
1927
+ if (existsSync5(parentDir) && readdirSync(parentDir).length === 0) {
1874
1928
  rmSync2(parentDir, { recursive: true });
1875
1929
  }
1876
1930
  } catch {
@@ -1883,6 +1937,7 @@ var claudeAdapter = {
1883
1937
  descriptor: descriptor2,
1884
1938
  detect: detect2,
1885
1939
  resolvePath: resolvePath2,
1940
+ resolvePackageDir,
1886
1941
  transformContent: transformContent2,
1887
1942
  install: install2,
1888
1943
  uninstall: uninstall2,
@@ -2334,8 +2389,32 @@ function getAllAdapters() {
2334
2389
  return [...adapters.values()];
2335
2390
  }
2336
2391
 
2392
+ // src/lib/extract.ts
2393
+ import { unzipSync } from "fflate";
2394
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2395
+ import { join as join12, dirname as dirname3, resolve as resolve2 } from "path";
2396
+ function extractPackage(zipBuffer, targetDir) {
2397
+ const resolvedTarget = resolve2(targetDir);
2398
+ const extracted = unzipSync(new Uint8Array(zipBuffer));
2399
+ const writtenFiles = [];
2400
+ for (const [path, data] of Object.entries(extracted)) {
2401
+ if (path.endsWith("/")) continue;
2402
+ if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
2403
+ continue;
2404
+ }
2405
+ const fullPath = resolve2(join12(targetDir, path));
2406
+ if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
2407
+ continue;
2408
+ }
2409
+ mkdirSync5(dirname3(fullPath), { recursive: true });
2410
+ writeFileSync5(fullPath, Buffer.from(data));
2411
+ writtenFiles.push(path);
2412
+ }
2413
+ return writtenFiles;
2414
+ }
2415
+
2337
2416
  // src/lib/cache.ts
2338
- var CACHE_DIR = join12(homedir8(), ".localskills", "cache");
2417
+ var CACHE_DIR = join13(homedir8(), ".localskills", "cache");
2339
2418
  function slugToDir(slug) {
2340
2419
  if (slug.includes("..") || slug.includes("\0")) {
2341
2420
  throw new Error("Invalid slug: contains forbidden characters");
@@ -2343,16 +2422,16 @@ function slugToDir(slug) {
2343
2422
  return slug.replace(/\//g, "--");
2344
2423
  }
2345
2424
  function getCacheDir(slug) {
2346
- const dir = resolve2(join12(CACHE_DIR, slugToDir(slug)));
2347
- if (!dir.startsWith(resolve2(CACHE_DIR) + "/") && dir !== resolve2(CACHE_DIR)) {
2425
+ const dir = resolve3(join13(CACHE_DIR, slugToDir(slug)));
2426
+ if (!dir.startsWith(resolve3(CACHE_DIR) + "/") && dir !== resolve3(CACHE_DIR)) {
2348
2427
  throw new Error("Invalid slug: path traversal detected");
2349
2428
  }
2350
2429
  return dir;
2351
2430
  }
2352
2431
  function store(slug, content, skill, version2) {
2353
2432
  const dir = getCacheDir(slug);
2354
- mkdirSync5(dir, { recursive: true });
2355
- writeFileSync5(join12(dir, "raw.md"), content);
2433
+ mkdirSync6(dir, { recursive: true });
2434
+ writeFileSync6(join13(dir, "raw.md"), content);
2356
2435
  const meta = {
2357
2436
  hash: skill.contentHash,
2358
2437
  version: version2,
@@ -2362,7 +2441,7 @@ function store(slug, content, skill, version2) {
2362
2441
  type: skill.type ?? "skill",
2363
2442
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2364
2443
  };
2365
- writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2444
+ writeFileSync6(join13(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2366
2445
  clearPlatformFiles(slug);
2367
2446
  }
2368
2447
  function getPlatformFile(slug, platform, skill) {
@@ -2373,25 +2452,25 @@ function getPlatformFile(slug, platform, skill) {
2373
2452
  const transformed = adapter.transformContent(raw, skill);
2374
2453
  if (platform === "claude") {
2375
2454
  if (skill.type === "rule") {
2376
- const claudeRuleDir = join12(dir, "claude-rule");
2377
- mkdirSync5(claudeRuleDir, { recursive: true });
2378
- const filePath3 = join12(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
2379
- writeFileSync5(filePath3, transformed);
2455
+ const claudeRuleDir = join13(dir, "claude-rule");
2456
+ mkdirSync6(claudeRuleDir, { recursive: true });
2457
+ const filePath3 = join13(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
2458
+ writeFileSync6(filePath3, transformed);
2380
2459
  return filePath3;
2381
2460
  }
2382
- const claudeDir = join12(dir, "claude");
2383
- mkdirSync5(claudeDir, { recursive: true });
2384
- const filePath2 = join12(claudeDir, "SKILL.md");
2385
- writeFileSync5(filePath2, transformed);
2461
+ const claudeDir = join13(dir, "claude");
2462
+ mkdirSync6(claudeDir, { recursive: true });
2463
+ const filePath2 = join13(claudeDir, "SKILL.md");
2464
+ writeFileSync6(filePath2, transformed);
2386
2465
  return filePath2;
2387
2466
  }
2388
2467
  const ext = adapter.descriptor.fileExtension;
2389
- const filePath = join12(dir, `${platform}${ext}`);
2390
- writeFileSync5(filePath, transformed);
2468
+ const filePath = join13(dir, `${platform}${ext}`);
2469
+ writeFileSync6(filePath, transformed);
2391
2470
  return filePath;
2392
2471
  }
2393
2472
  function getRawContent(slug) {
2394
- const filePath = join12(getCacheDir(slug), "raw.md");
2473
+ const filePath = join13(getCacheDir(slug), "raw.md");
2395
2474
  if (!existsSync13(filePath)) return null;
2396
2475
  return readFileSync4(filePath, "utf-8");
2397
2476
  }
@@ -2401,11 +2480,18 @@ function purge(slug) {
2401
2480
  rmSync3(dir, { recursive: true, force: true });
2402
2481
  }
2403
2482
  }
2483
+ function getPackageDir(slug) {
2484
+ return join13(getCacheDir(slug), "pkg");
2485
+ }
2404
2486
  function storePackage(slug, zipBuffer, manifest, skill, version2) {
2405
2487
  const dir = getCacheDir(slug);
2406
- mkdirSync5(dir, { recursive: true });
2407
- writeFileSync5(join12(dir, "package.zip"), zipBuffer);
2408
- writeFileSync5(join12(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
2488
+ mkdirSync6(dir, { recursive: true });
2489
+ const pkgDir = join13(dir, "pkg");
2490
+ rmSync3(pkgDir, { recursive: true, force: true });
2491
+ mkdirSync6(pkgDir, { recursive: true });
2492
+ extractPackage(zipBuffer, pkgDir);
2493
+ rmSync3(join13(dir, "package.zip"), { force: true });
2494
+ writeFileSync6(join13(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
2409
2495
  const meta = {
2410
2496
  hash: skill.contentHash,
2411
2497
  version: version2,
@@ -2416,41 +2502,44 @@ function storePackage(slug, zipBuffer, manifest, skill, version2) {
2416
2502
  format: "package",
2417
2503
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2418
2504
  };
2419
- writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2505
+ writeFileSync6(join13(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2420
2506
  }
2421
2507
  function clearPlatformFiles(slug) {
2422
2508
  const dir = getCacheDir(slug);
2423
2509
  if (!existsSync13(dir)) return;
2424
- const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "package.zip", "manifest.json"]);
2425
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
2510
+ const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "manifest.json", "pkg"]);
2511
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2426
2512
  if (!keep.has(entry.name)) {
2427
- rmSync3(join12(dir, entry.name), { recursive: true, force: true });
2513
+ rmSync3(join13(dir, entry.name), { recursive: true, force: true });
2428
2514
  }
2429
2515
  }
2430
2516
  }
2431
2517
 
2432
- // src/lib/extract.ts
2433
- import { unzipSync } from "fflate";
2434
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
2435
- import { join as join13, dirname as dirname3, resolve as resolve3 } from "path";
2436
- function extractPackage(zipBuffer, targetDir) {
2437
- const resolvedTarget = resolve3(targetDir);
2438
- const extracted = unzipSync(new Uint8Array(zipBuffer));
2439
- const writtenFiles = [];
2440
- for (const [path, data] of Object.entries(extracted)) {
2441
- if (path.endsWith("/")) continue;
2442
- if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
2443
- continue;
2444
- }
2445
- const fullPath = resolve3(join13(targetDir, path));
2446
- if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
2447
- continue;
2448
- }
2449
- mkdirSync6(dirname3(fullPath), { recursive: true });
2450
- writeFileSync6(fullPath, Buffer.from(data));
2451
- writtenFiles.push(path);
2518
+ // src/lib/package-target.ts
2519
+ import { lstatSync as lstatSync2, readdirSync as readdirSync3 } from "fs";
2520
+ import { basename, dirname as dirname4, join as join14 } from "path";
2521
+ import { homedir as homedir9 } from "os";
2522
+ function packageTargetConflict(targetDir, ownedByConfig) {
2523
+ try {
2524
+ lstatSync2(targetDir);
2525
+ } catch {
2526
+ return null;
2452
2527
  }
2453
- return writtenFiles;
2528
+ if (ownedByConfig) return null;
2529
+ if (isSymlinkInto(targetDir, join14(homedir9(), ".localskills", "cache"))) {
2530
+ return null;
2531
+ }
2532
+ return `${targetDir} already exists and isn't managed by localskills \u2014 move or remove it first`;
2533
+ }
2534
+ function soleTrackedOccupant(targetDir, installedPath) {
2535
+ if (dirname4(installedPath) !== targetDir) return false;
2536
+ let entries;
2537
+ try {
2538
+ entries = readdirSync3(targetDir);
2539
+ } catch {
2540
+ return false;
2541
+ }
2542
+ return entries.length === 1 && entries[0] === basename(installedPath);
2454
2543
  }
2455
2544
 
2456
2545
  // src/lib/interactive.ts
@@ -2570,6 +2659,32 @@ function buildSkillRecord(cacheKey, skill, version2, resolvedSemver, requestedRa
2570
2659
  installations: existingInstallations ? [...existingInstallations, ...newInstallations] : newInstallations
2571
2660
  };
2572
2661
  }
2662
+ function reconcilePlatformInstalls(existing, slug, platformId, scope, projectDir, newPath, newFormat) {
2663
+ const remaining = [];
2664
+ const stale = [];
2665
+ let ownedTarget = false;
2666
+ for (const inst of existing ?? []) {
2667
+ const sameTarget = inst.platform === platformId && inst.scope === scope && (inst.projectDir ?? null) === (projectDir ?? null);
2668
+ if (!sameTarget) {
2669
+ remaining.push(inst);
2670
+ continue;
2671
+ }
2672
+ if (inst.path === newPath && (inst.format ?? "text") === newFormat) {
2673
+ ownedTarget = true;
2674
+ continue;
2675
+ }
2676
+ stale.push(inst);
2677
+ }
2678
+ return { remaining, stale, ownedTarget };
2679
+ }
2680
+ function removeStaleInstalls(stale, slug) {
2681
+ for (const inst of stale) {
2682
+ try {
2683
+ getAdapter(inst.platform).uninstall(inst, slug);
2684
+ } catch {
2685
+ }
2686
+ }
2687
+ }
2573
2688
  var installCommand = new Command2("install").description("Install a skill locally").argument("[slug]", "Skill slug (omit for interactive search)").option("-t, --target <targets...>", "Target platforms (cursor, claude, codex, ...)").option("-g, --global", "Install globally").option("-p, --project [dir]", "Install in project directory").option("--symlink", "Use symlink (default)").option("--copy", "Copy instead of symlink").action(
2574
2689
  async (slugArg, opts) => {
2575
2690
  const client = new ApiClient();
@@ -2652,11 +2767,20 @@ var installCommand = new Command2("install").description("Install a skill locall
2652
2767
  spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version3)} (package, ${manifest.files.length} files)`);
2653
2768
  const dlSpinner = bt2();
2654
2769
  dlSpinner.start("Downloading package...");
2655
- const zipBuffer = await client.fetchBinary(downloadUrl);
2770
+ let zipBuffer;
2771
+ try {
2772
+ zipBuffer = await client.fetchBinary(downloadUrl);
2773
+ } catch (err) {
2774
+ dlSpinner.stop(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
2775
+ process.exit(1);
2776
+ return;
2777
+ }
2656
2778
  dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
2657
2779
  storePackage(cacheKey, zipBuffer, manifest, skill2, version3);
2780
+ const pkgDir = getPackageDir(cacheKey);
2658
2781
  const installations2 = [];
2659
2782
  const results2 = [];
2783
+ let existingInstallations2 = config.installed_skills[cacheKey]?.installations;
2660
2784
  for (const platformId of platforms) {
2661
2785
  const adapter = getAdapter(platformId);
2662
2786
  const desc = adapter.descriptor;
@@ -2668,19 +2792,55 @@ var installCommand = new Command2("install").description("Install a skill locall
2668
2792
  R2.warn(`${desc.name} does not support project \u2014 skipping.`);
2669
2793
  continue;
2670
2794
  }
2671
- const targetPath = adapter.resolvePath(cacheKey, scope, projectDir, skill2.type ?? "skill");
2672
- mkdirSync7(targetPath, { recursive: true });
2673
- const written = extractPackage(zipBuffer, targetPath);
2795
+ if (!adapter.resolvePackageDir) {
2796
+ R2.warn(`${desc.name} does not support multi-file (folder) skills \u2014 skipping.`);
2797
+ continue;
2798
+ }
2799
+ const targetDir = adapter.resolvePackageDir(cacheKey, scope, projectDir, skill2.type ?? "skill");
2800
+ const reconciled = reconcilePlatformInstalls(
2801
+ existingInstallations2,
2802
+ cacheKey,
2803
+ platformId,
2804
+ scope,
2805
+ projectDir,
2806
+ targetDir,
2807
+ "package"
2808
+ );
2809
+ const owned = reconciled.ownedTarget || reconciled.stale.some((inst) => soleTrackedOccupant(targetDir, inst.path));
2810
+ const conflict = packageTargetConflict(targetDir, owned);
2811
+ if (conflict) {
2812
+ R2.warn(`${desc.name} \u2014 ${conflict}. Skipping.`);
2813
+ existingInstallations2 = [...reconciled.remaining, ...reconciled.stale];
2814
+ continue;
2815
+ }
2816
+ removeStaleInstalls(reconciled.stale, cacheKey);
2817
+ existingInstallations2 = reconciled.remaining;
2818
+ const actualMethod = method === "copy" ? "copy" : "symlink";
2819
+ if (actualMethod === "symlink") {
2820
+ createSymlink(pkgDir, targetDir);
2821
+ } else {
2822
+ rmSync4(targetDir, { recursive: true, force: true });
2823
+ mkdirSync7(dirname5(targetDir), { recursive: true });
2824
+ cpSync(pkgDir, targetDir, { recursive: true });
2825
+ }
2674
2826
  const installation = {
2675
2827
  platform: platformId,
2676
2828
  scope,
2677
- method: "copy",
2678
- path: targetPath,
2829
+ method: actualMethod,
2830
+ path: targetDir,
2679
2831
  projectDir,
2680
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
2832
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2833
+ format: "package"
2681
2834
  };
2682
2835
  installations2.push(installation);
2683
- results2.push(`${desc.name} \u2192 ${targetPath} (${written.length} files extracted)`);
2836
+ const methodLabel = actualMethod === "symlink" ? "symlinked" : "copied";
2837
+ results2.push(`${desc.name} \u2192 ${targetDir} (${methodLabel}, ${manifest.files.length} files)`);
2838
+ }
2839
+ if (installations2.length === 0) {
2840
+ R2.error("No compatible targets for this folder skill.");
2841
+ R2.info("Folder (package) skills currently install to Claude Code only.");
2842
+ process.exit(1);
2843
+ return;
2684
2844
  }
2685
2845
  config.installed_skills[cacheKey] = buildSkillRecord(
2686
2846
  cacheKey,
@@ -2688,7 +2848,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2688
2848
  version3,
2689
2849
  resolvedSemver2,
2690
2850
  requestedRange,
2691
- config.installed_skills[cacheKey]?.installations,
2851
+ existingInstallations2,
2692
2852
  installations2
2693
2853
  );
2694
2854
  saveConfig(config);
@@ -2704,6 +2864,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2704
2864
  const contentType = skill.type ?? "skill";
2705
2865
  const installations = [];
2706
2866
  const results = [];
2867
+ let existingInstallations = config.installed_skills[cacheKey]?.installations;
2707
2868
  for (const platformId of platforms) {
2708
2869
  const adapter = getAdapter(platformId);
2709
2870
  const desc = adapter.descriptor;
@@ -2716,6 +2877,18 @@ var installCommand = new Command2("install").description("Install a skill locall
2716
2877
  continue;
2717
2878
  }
2718
2879
  const actualMethod = adapter.defaultMethod(scope) === "section" ? "section" : method;
2880
+ const newPath = adapter.resolvePath(cacheKey, scope, projectDir, contentType);
2881
+ const reconciledText = reconcilePlatformInstalls(
2882
+ existingInstallations,
2883
+ cacheKey,
2884
+ platformId,
2885
+ scope,
2886
+ projectDir,
2887
+ newPath,
2888
+ "text"
2889
+ );
2890
+ removeStaleInstalls(reconciledText.stale, cacheKey);
2891
+ existingInstallations = reconciledText.remaining;
2719
2892
  const cachePath = getPlatformFile(cacheKey, platformId, skill);
2720
2893
  const transformed = adapter.transformContent(content, skill);
2721
2894
  const installedPath = adapter.install({
@@ -2733,7 +2906,8 @@ var installCommand = new Command2("install").description("Install a skill locall
2733
2906
  method: actualMethod,
2734
2907
  path: installedPath,
2735
2908
  projectDir,
2736
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
2909
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2910
+ format: "text"
2737
2911
  };
2738
2912
  installations.push(installation);
2739
2913
  const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
@@ -2745,7 +2919,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2745
2919
  version2,
2746
2920
  resolvedSemver,
2747
2921
  requestedRange,
2748
- config.installed_skills[cacheKey]?.installations,
2922
+ existingInstallations,
2749
2923
  installations
2750
2924
  );
2751
2925
  saveConfig(config);
@@ -2861,8 +3035,21 @@ ${res.data.length} skill(s) found.`);
2861
3035
  });
2862
3036
 
2863
3037
  // src/commands/pull.ts
2864
- import { mkdirSync as mkdirSync8 } from "fs";
3038
+ import { mkdirSync as mkdirSync8, rmSync as rmSync5, cpSync as cpSync2, statSync as statSync2 } from "fs";
3039
+ import { dirname as dirname6 } from "path";
2865
3040
  import { Command as Command5 } from "commander";
3041
+ function errMsg(err) {
3042
+ return err instanceof Error ? err.message : String(err);
3043
+ }
3044
+ function installedAsPackage(inst) {
3045
+ if (inst.format === "package") return true;
3046
+ if (inst.format === "text") return false;
3047
+ try {
3048
+ return statSync2(inst.path).isDirectory();
3049
+ } catch {
3050
+ return false;
3051
+ }
3052
+ }
2866
3053
  var pullCommand = new Command5("pull").description("Pull latest versions of all installed skills").argument("[slug]", "Pull a specific skill (omit for all)").action(async (slugArg) => {
2867
3054
  const config = loadConfig();
2868
3055
  const slugs = slugArg ? [slugArg] : Object.keys(config.installed_skills);
@@ -2898,53 +3085,129 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
2898
3085
  skipped++;
2899
3086
  continue;
2900
3087
  }
3088
+ let allHandled = true;
2901
3089
  if (format === "package") {
2902
3090
  const { downloadUrl, manifest } = resData;
2903
- const zipBuffer = await client.fetchBinary(downloadUrl);
3091
+ let zipBuffer;
3092
+ try {
3093
+ zipBuffer = await client.fetchBinary(downloadUrl);
3094
+ } catch (err) {
3095
+ spinner.stop(`${slug} \u2014 download failed: ${errMsg(err)}`);
3096
+ continue;
3097
+ }
2904
3098
  storePackage(slug, zipBuffer, manifest, skill, version2);
3099
+ const pkgDir = getPackageDir(slug);
3100
+ const kept = [];
2905
3101
  for (const installation of installed.installations) {
2906
3102
  const adapter = getAdapter(installation.platform);
2907
- const targetPath = adapter.resolvePath(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
2908
- mkdirSync8(targetPath, { recursive: true });
2909
- extractPackage(zipBuffer, targetPath);
3103
+ const wasPackage = installedAsPackage(installation);
3104
+ if (!adapter.resolvePackageDir) {
3105
+ R2.warn(
3106
+ `${adapter.descriptor.name} can't hold a folder skill \u2014 keeping the existing install; it won't receive this update.`
3107
+ );
3108
+ kept.push(installation);
3109
+ allHandled = false;
3110
+ continue;
3111
+ }
3112
+ try {
3113
+ const targetDir = adapter.resolvePackageDir(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
3114
+ const owned = wasPackage && installation.path === targetDir || !wasPackage && soleTrackedOccupant(targetDir, installation.path);
3115
+ const conflict = packageTargetConflict(targetDir, owned);
3116
+ if (conflict) {
3117
+ R2.warn(
3118
+ `${adapter.descriptor.name} \u2014 ${conflict}. Skipping; reinstall with \`localskills install ${slug}\`.`
3119
+ );
3120
+ kept.push(installation);
3121
+ allHandled = false;
3122
+ continue;
3123
+ }
3124
+ if (!wasPackage) {
3125
+ adapter.uninstall(installation, slug);
3126
+ }
3127
+ const useMethod = installation.method === "copy" ? "copy" : "symlink";
3128
+ if (useMethod === "symlink") {
3129
+ createSymlink(pkgDir, targetDir);
3130
+ } else {
3131
+ rmSync5(targetDir, { recursive: true, force: true });
3132
+ mkdirSync8(dirname6(targetDir), { recursive: true });
3133
+ cpSync2(pkgDir, targetDir, { recursive: true });
3134
+ }
3135
+ kept.push({ ...installation, method: useMethod, path: targetDir, format: "package" });
3136
+ } catch (err) {
3137
+ R2.warn(`${adapter.descriptor.name} \u2014 failed to update: ${errMsg(err)}`);
3138
+ kept.push(installation);
3139
+ allHandled = false;
3140
+ }
2910
3141
  }
3142
+ installed.installations = kept;
2911
3143
  } else {
2912
3144
  const { content } = resData;
2913
3145
  store(slug, content, skill, version2);
2914
3146
  for (const installation of installed.installations) {
2915
- if (installation.method === "symlink") {
2916
- getPlatformFile(slug, installation.platform, skill);
2917
- continue;
2918
- }
2919
3147
  const adapter = getAdapter(installation.platform);
2920
- const transformed = adapter.transformContent(content, skill);
2921
- if (installation.method === "section") {
2922
- upsertSection(
2923
- installation.path,
2924
- slug,
2925
- `## ${slug}
3148
+ try {
3149
+ const transformed = adapter.transformContent(content, skill);
3150
+ if (installedAsPackage(installation)) {
3151
+ adapter.uninstall(installation, slug);
3152
+ const method = installation.method === "section" ? "copy" : installation.method;
3153
+ const cachePath = getPlatformFile(slug, installation.platform, skill);
3154
+ const installedPath = adapter.install({
3155
+ slug,
3156
+ content: transformed,
3157
+ scope: installation.scope,
3158
+ method,
3159
+ cachePath,
3160
+ projectDir: installation.projectDir,
3161
+ contentType: skill.type
3162
+ });
3163
+ installation.path = installedPath;
3164
+ installation.method = method;
3165
+ installation.format = "text";
3166
+ continue;
3167
+ }
3168
+ if (installation.method === "symlink") {
3169
+ getPlatformFile(slug, installation.platform, skill);
3170
+ installation.format = "text";
3171
+ continue;
3172
+ }
3173
+ if (installation.method === "section") {
3174
+ upsertSection(
3175
+ installation.path,
3176
+ slug,
3177
+ `## ${slug}
2926
3178
 
2927
3179
  ${transformed}`
2928
- );
2929
- } else {
2930
- const cachePath = getPlatformFile(slug, installation.platform, skill);
2931
- adapter.install({
2932
- slug,
2933
- content: transformed,
2934
- scope: installation.scope,
2935
- method: "copy",
2936
- cachePath,
2937
- projectDir: installation.projectDir
2938
- });
3180
+ );
3181
+ } else {
3182
+ const cachePath = getPlatformFile(slug, installation.platform, skill);
3183
+ adapter.install({
3184
+ slug,
3185
+ content: transformed,
3186
+ scope: installation.scope,
3187
+ method: "copy",
3188
+ cachePath,
3189
+ projectDir: installation.projectDir,
3190
+ contentType: skill.type
3191
+ });
3192
+ }
3193
+ installation.format = "text";
3194
+ } catch (err) {
3195
+ R2.warn(`${adapter.descriptor.name} \u2014 failed to update: ${errMsg(err)}`);
3196
+ allHandled = false;
2939
3197
  }
2940
3198
  }
2941
3199
  }
2942
- installed.hash = skill.contentHash;
2943
- installed.version = version2;
2944
- installed.semver = resData.semver ?? null;
2945
- installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
2946
- updated++;
2947
- spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version2)}`);
3200
+ if (allHandled) {
3201
+ installed.hash = skill.contentHash;
3202
+ installed.version = version2;
3203
+ installed.semver = resData.semver ?? null;
3204
+ installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
3205
+ updated++;
3206
+ spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version2)}`);
3207
+ } else {
3208
+ updated++;
3209
+ spinner.stop(`${slug} \u2014 partially updated to ${formatVersionLabel(res.data.semver, version2)} (see warnings above)`);
3210
+ }
2948
3211
  }
2949
3212
  saveConfig(config);
2950
3213
  Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
@@ -2952,44 +3215,43 @@ ${transformed}`
2952
3215
 
2953
3216
  // src/commands/publish.ts
2954
3217
  import { Command as Command6 } from "commander";
2955
- import { readFileSync as readFileSync6, existsSync as existsSync15 } from "fs";
2956
- import { resolve as resolve4, basename as basename2, extname as extname2 } from "path";
2957
- import { homedir as homedir10 } from "os";
3218
+ import { readFileSync as readFileSync7, existsSync as existsSync15, statSync as statSync4 } from "fs";
3219
+ import { resolve as resolve4, basename as basename3, extname as extname2 } from "path";
3220
+ import { homedir as homedir11 } from "os";
2958
3221
 
2959
3222
  // src/lib/scanner.ts
2960
- import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
2961
- import { join as join14, basename, extname } from "path";
2962
- import { homedir as homedir9 } from "os";
2963
- import { readlinkSync, lstatSync as lstatSync2 } from "fs";
3223
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "fs";
3224
+ import { join as join15, basename as basename2, extname, dirname as dirname7 } from "path";
3225
+ import { homedir as homedir10 } from "os";
2964
3226
  function scanForSkills(projectDir) {
2965
- const home = homedir9();
3227
+ const home = homedir10();
2966
3228
  const cwd = projectDir || process.cwd();
2967
3229
  const results = [];
2968
- scanDirectory(join14(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
2969
- scanDirectory(join14(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
2970
- scanClaudeSkills(join14(home, ".claude", "skills"), "global", results);
2971
- scanClaudeSkills(join14(cwd, ".claude", "skills"), "project", results);
2972
- scanDirectory(join14(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
2973
- scanDirectory(join14(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
2974
- scanSingleFile(join14(home, ".codex", "AGENTS.md"), "codex", "global", results);
2975
- scanSingleFile(join14(cwd, "AGENTS.md"), "codex", "project", results);
3230
+ scanDirectory(join15(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
3231
+ scanDirectory(join15(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
3232
+ scanClaudeSkills(join15(home, ".claude", "skills"), "global", results);
3233
+ scanClaudeSkills(join15(cwd, ".claude", "skills"), "project", results);
3234
+ scanDirectory(join15(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
3235
+ scanDirectory(join15(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
3236
+ scanSingleFile(join15(home, ".codex", "AGENTS.md"), "codex", "global", results);
3237
+ scanSingleFile(join15(cwd, "AGENTS.md"), "codex", "project", results);
2976
3238
  scanSingleFile(
2977
- join14(home, ".codeium", "windsurf", "memories", "global_rules.md"),
3239
+ join15(home, ".codeium", "windsurf", "memories", "global_rules.md"),
2978
3240
  "windsurf",
2979
3241
  "global",
2980
3242
  results
2981
3243
  );
2982
- scanDirectory(join14(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
2983
- scanDirectory(join14(cwd, ".clinerules"), ".md", "cline", "project", results);
3244
+ scanDirectory(join15(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
3245
+ scanDirectory(join15(cwd, ".clinerules"), ".md", "cline", "project", results);
2984
3246
  scanSingleFile(
2985
- join14(cwd, ".github", "copilot-instructions.md"),
3247
+ join15(cwd, ".github", "copilot-instructions.md"),
2986
3248
  "copilot",
2987
3249
  "project",
2988
3250
  results
2989
3251
  );
2990
- scanDirectory(join14(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
2991
- scanDirectory(join14(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
2992
- scanDirectory(join14(cwd, ".aider", "skills"), ".md", "aider", "project", results);
3252
+ scanDirectory(join15(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
3253
+ scanDirectory(join15(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
3254
+ scanDirectory(join15(cwd, ".aider", "skills"), ".md", "aider", "project", results);
2993
3255
  return results;
2994
3256
  }
2995
3257
  function filterTracked(detected, config) {
@@ -2999,35 +3261,32 @@ function filterTracked(detected, config) {
2999
3261
  trackedPaths.add(inst.path);
3000
3262
  }
3001
3263
  }
3002
- const cacheDir = join14(homedir9(), ".localskills", "cache");
3264
+ const cacheDir = join15(homedir10(), ".localskills", "cache");
3003
3265
  return detected.filter((skill) => {
3004
- if (trackedPaths.has(skill.filePath)) return false;
3005
- try {
3006
- const stat = lstatSync2(skill.filePath);
3007
- if (stat.isSymbolicLink()) {
3008
- const target = readlinkSync(skill.filePath);
3009
- if (target.startsWith(cacheDir)) return false;
3010
- }
3011
- } catch {
3266
+ const candidates = [skill.filePath, dirname7(skill.filePath)];
3267
+ if (skill.dir) candidates.push(skill.dir);
3268
+ for (const path of candidates) {
3269
+ if (trackedPaths.has(path)) return false;
3270
+ if (isSymlinkInto(path, cacheDir)) return false;
3012
3271
  }
3013
3272
  return true;
3014
3273
  });
3015
3274
  }
3016
3275
  function slugFromFilename(filename) {
3017
- return basename(filename, extname(filename));
3276
+ return basename2(filename, extname(filename));
3018
3277
  }
3019
3278
  var nameFromSlug = titleFromSlug;
3020
3279
  function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
3021
3280
  if (!existsSync14(dir)) return;
3022
3281
  let entries;
3023
3282
  try {
3024
- entries = readdirSync2(dir);
3283
+ entries = readdirSync4(dir);
3025
3284
  } catch {
3026
3285
  return;
3027
3286
  }
3028
3287
  for (const entry of entries) {
3029
3288
  if (!entry.endsWith(ext)) continue;
3030
- const filePath = join14(dir, entry);
3289
+ const filePath = join15(dir, entry);
3031
3290
  try {
3032
3291
  const raw = readFileSync5(filePath, "utf-8");
3033
3292
  const content = stripFrontmatter(raw).trim();
@@ -3035,6 +3294,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
3035
3294
  const slug = slugFromFilename(entry);
3036
3295
  results.push({
3037
3296
  filePath,
3297
+ format: "text",
3038
3298
  platform,
3039
3299
  scope,
3040
3300
  suggestedName: nameFromSlug(slug),
@@ -3046,28 +3306,56 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
3046
3306
  }
3047
3307
  }
3048
3308
  }
3309
+ var SCAN_JUNK = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".git", "node_modules", ".localskills"]);
3310
+ function dirHasFilesBesides(dir, excludeRootFile) {
3311
+ const stack = [dir];
3312
+ while (stack.length > 0) {
3313
+ const current = stack.pop();
3314
+ let entries;
3315
+ try {
3316
+ entries = readdirSync4(current, { withFileTypes: true });
3317
+ } catch {
3318
+ continue;
3319
+ }
3320
+ for (const entry of entries) {
3321
+ if (SCAN_JUNK.has(entry.name)) continue;
3322
+ if (entry.isDirectory()) {
3323
+ stack.push(join15(current, entry.name));
3324
+ continue;
3325
+ }
3326
+ if (current === dir && entry.name === excludeRootFile) continue;
3327
+ return true;
3328
+ }
3329
+ }
3330
+ return false;
3331
+ }
3049
3332
  function scanClaudeSkills(skillsDir, scope, results) {
3050
3333
  if (!existsSync14(skillsDir)) return;
3051
3334
  let entries;
3052
3335
  try {
3053
- entries = readdirSync2(skillsDir);
3336
+ entries = readdirSync4(skillsDir);
3054
3337
  } catch {
3055
3338
  return;
3056
3339
  }
3057
3340
  for (const entry of entries) {
3058
- const skillFile = join14(skillsDir, entry, "SKILL.md");
3341
+ const skillDir = join15(skillsDir, entry);
3342
+ const skillFile = join15(skillDir, "SKILL.md");
3059
3343
  if (!existsSync14(skillFile)) continue;
3060
3344
  try {
3061
3345
  const raw = readFileSync5(skillFile, "utf-8");
3062
3346
  const content = stripFrontmatter(raw).trim();
3063
- if (!content) continue;
3347
+ const isPackage = dirHasFilesBesides(skillDir, "SKILL.md");
3348
+ if (!isPackage && !content) continue;
3064
3349
  results.push({
3065
3350
  filePath: skillFile,
3351
+ ...isPackage ? { dir: skillDir } : {},
3352
+ format: isPackage ? "package" : "text",
3066
3353
  platform: "claude",
3067
3354
  scope,
3068
3355
  suggestedName: nameFromSlug(entry),
3069
3356
  suggestedSlug: entry,
3070
3357
  content,
3358
+ // for packages: SKILL.md body, used only for the hint/preview
3071
3359
  contentType: "skill"
3072
3360
  });
3073
3361
  } catch {
@@ -3094,6 +3382,7 @@ function scanSingleFile(filePath, platform, scope, results) {
3094
3382
  if (!content2) continue;
3095
3383
  results.push({
3096
3384
  filePath,
3385
+ format: "text",
3097
3386
  platform,
3098
3387
  scope,
3099
3388
  suggestedName: nameFromSlug(slug2),
@@ -3109,6 +3398,7 @@ function scanSingleFile(filePath, platform, scope, results) {
3109
3398
  const slug = slugFromFilename(filePath);
3110
3399
  results.push({
3111
3400
  filePath,
3401
+ format: "text",
3112
3402
  platform,
3113
3403
  scope,
3114
3404
  suggestedName: nameFromSlug(slug),
@@ -3118,8 +3408,177 @@ function scanSingleFile(filePath, platform, scope, results) {
3118
3408
  });
3119
3409
  }
3120
3410
 
3411
+ // src/lib/pack.ts
3412
+ import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
3413
+ import { join as join16, relative as relative2, sep as sep2 } from "path";
3414
+ import { zipSync } from "fflate";
3415
+ var PackError = class extends Error {
3416
+ constructor(message) {
3417
+ super(message);
3418
+ this.name = "PackError";
3419
+ }
3420
+ };
3421
+ function zipBlob(zip) {
3422
+ return new Blob([new Uint8Array(zip)], { type: "application/zip" });
3423
+ }
3424
+ var SKIP = /* @__PURE__ */ new Set([
3425
+ ".DS_Store",
3426
+ ".git",
3427
+ "node_modules",
3428
+ ".localskills",
3429
+ "Thumbs.db"
3430
+ ]);
3431
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3432
+ ".png",
3433
+ ".jpg",
3434
+ ".jpeg",
3435
+ ".gif",
3436
+ ".webp",
3437
+ ".ico",
3438
+ ".bmp",
3439
+ ".tiff",
3440
+ ".woff",
3441
+ ".woff2",
3442
+ ".ttf",
3443
+ ".eot",
3444
+ ".otf",
3445
+ ".pdf",
3446
+ ".zip",
3447
+ ".tar",
3448
+ ".gz",
3449
+ ".bz2",
3450
+ ".7z",
3451
+ ".rar",
3452
+ ".exe",
3453
+ ".dll",
3454
+ ".so",
3455
+ ".dylib",
3456
+ ".bin",
3457
+ ".wasm",
3458
+ ".pyc",
3459
+ ".class",
3460
+ ".o",
3461
+ ".obj",
3462
+ ".mp3",
3463
+ ".mp4",
3464
+ ".wav",
3465
+ ".avi",
3466
+ ".mov",
3467
+ ".mkv",
3468
+ ".sqlite",
3469
+ ".db"
3470
+ ]);
3471
+ function getExtension(path) {
3472
+ const dot = path.lastIndexOf(".");
3473
+ return dot === -1 ? "" : path.substring(dot).toLowerCase();
3474
+ }
3475
+ function isBinaryByContent(data) {
3476
+ const sample = data.subarray(0, 8192);
3477
+ for (let i = 0; i < sample.length; i++) if (sample[i] === 0) return true;
3478
+ return false;
3479
+ }
3480
+ function validatePath(path) {
3481
+ if (path.startsWith("/") || path.startsWith("\\")) return false;
3482
+ if (path.includes("..")) return false;
3483
+ if (path.includes("\0")) return false;
3484
+ if (path.includes("\\")) return false;
3485
+ if (path === "__proto__") return false;
3486
+ return true;
3487
+ }
3488
+ function walk(root, current, acc) {
3489
+ for (const entry of readdirSync5(current, { withFileTypes: true })) {
3490
+ if (SKIP.has(entry.name)) continue;
3491
+ const abs = join16(current, entry.name);
3492
+ const rel = relative2(root, abs).split(sep2).join("/");
3493
+ let isFile = entry.isFile();
3494
+ let size;
3495
+ if (entry.isDirectory()) {
3496
+ walk(root, abs, acc);
3497
+ continue;
3498
+ }
3499
+ if (entry.isSymbolicLink()) {
3500
+ let st3;
3501
+ try {
3502
+ st3 = statSync3(abs);
3503
+ } catch {
3504
+ acc.warnings.push(`Skipped broken symlink: ${rel}`);
3505
+ continue;
3506
+ }
3507
+ if (st3.isDirectory()) {
3508
+ acc.warnings.push(`Skipped symlinked directory: ${rel}/`);
3509
+ continue;
3510
+ }
3511
+ isFile = st3.isFile();
3512
+ size = st3.size;
3513
+ }
3514
+ if (!isFile) continue;
3515
+ acc.count++;
3516
+ if (acc.count > PACKAGE_MAX_FILE_COUNT) {
3517
+ throw new PackError(
3518
+ `Folder contains more than ${PACKAGE_MAX_FILE_COUNT} files`
3519
+ );
3520
+ }
3521
+ size ??= statSync3(abs).size;
3522
+ if (acc.bytes + size > PACKAGE_MAX_DECOMPRESSED_BYTES) {
3523
+ throw new PackError(
3524
+ `Folder exceeds ${PACKAGE_MAX_DECOMPRESSED_BYTES / 1024 / 1024} MB uncompressed limit`
3525
+ );
3526
+ }
3527
+ const data = new Uint8Array(readFileSync6(abs));
3528
+ acc.bytes += data.length;
3529
+ if (acc.bytes > PACKAGE_MAX_DECOMPRESSED_BYTES) {
3530
+ throw new PackError(
3531
+ `Folder exceeds ${PACKAGE_MAX_DECOMPRESSED_BYTES / 1024 / 1024} MB uncompressed limit`
3532
+ );
3533
+ }
3534
+ acc.files.set(rel, data);
3535
+ }
3536
+ }
3537
+ function packFolder(dir, opts = {}) {
3538
+ const requireSkillMd = opts.requireSkillMd ?? true;
3539
+ const stat = statSync3(dir);
3540
+ if (!stat.isDirectory()) throw new PackError(`Not a directory: ${dir}`);
3541
+ const acc = { files: /* @__PURE__ */ new Map(), count: 0, bytes: 0, warnings: [] };
3542
+ walk(dir, dir, acc);
3543
+ const entries = acc.files;
3544
+ if (entries.size === 0) throw new PackError("Folder contains no files");
3545
+ if (requireSkillMd && !entries.has("SKILL.md")) {
3546
+ throw new PackError(
3547
+ "No SKILL.md at the folder root. A skill folder must contain a SKILL.md file."
3548
+ );
3549
+ }
3550
+ let totalSize = 0;
3551
+ const files = [];
3552
+ for (const [path, data] of entries) {
3553
+ if (!validatePath(path)) throw new PackError(`Invalid file path: ${path}`);
3554
+ totalSize += data.length;
3555
+ const binary = BINARY_EXTENSIONS.has(getExtension(path)) || isBinaryByContent(data);
3556
+ const file = { path, size: data.length, isBinary: binary };
3557
+ if (!binary && data.length > 0) {
3558
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(data);
3559
+ file.preview = text.length > PACKAGE_PREVIEW_BYTES ? text.substring(0, PACKAGE_PREVIEW_BYTES) : text;
3560
+ }
3561
+ files.push(file);
3562
+ }
3563
+ files.sort((a, b) => a.path.localeCompare(b.path));
3564
+ const zipInput = /* @__PURE__ */ Object.create(null);
3565
+ for (const [path, data] of entries) zipInput[path] = data;
3566
+ const zip = Buffer.from(zipSync(zipInput));
3567
+ if (zip.length > PACKAGE_MAX_COMPRESSED_BYTES) {
3568
+ throw new PackError(
3569
+ `Archive exceeds ${PACKAGE_MAX_COMPRESSED_BYTES / 1024 / 1024} MB compressed limit`
3570
+ );
3571
+ }
3572
+ return {
3573
+ zip,
3574
+ manifest: { version: 1, totalSize, archiveSize: zip.length, files },
3575
+ fileCount: files.length,
3576
+ warnings: acc.warnings
3577
+ };
3578
+ }
3579
+
3121
3580
  // src/commands/publish.ts
3122
- var publishCommand = new Command6("publish").description("Publish local skill files to localskills.sh").argument("[file]", "Path to a specific file to publish").option("-t, --team <id>", "Team ID to publish to").option("-n, --name <name>", "Skill name").option(
3581
+ var publishCommand = new Command6("publish").description("Publish local skills to localskills.sh").argument("[path]", "Path to a skill file, or a skill folder (uploaded as a package)").option("-t, --team <id>", "Team ID to publish to").option("-n, --name <name>", "Skill name").option(
3123
3582
  "--visibility <visibility>",
3124
3583
  "Visibility: public, private, or unlisted",
3125
3584
  "private"
@@ -3139,30 +3598,46 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
3139
3598
  if (fileArg) {
3140
3599
  const filePath = resolve4(fileArg);
3141
3600
  if (!existsSync15(filePath)) {
3142
- console.error(`File not found: ${filePath}`);
3601
+ console.error(`Path not found: ${filePath}`);
3143
3602
  process.exit(1);
3144
3603
  return;
3145
3604
  }
3146
- const raw = readFileSync6(filePath, "utf-8");
3605
+ if (statSync4(filePath).isDirectory()) {
3606
+ const contentType2 = validateContentType(opts.type || "skill");
3607
+ const visibility2 = validateVisibility(opts.visibility || "private");
3608
+ const tenantId2 = await resolveTeam(teams, opts.team);
3609
+ const skillName2 = opts.name || titleFromSlug(basename3(filePath));
3610
+ const ok2 = await uploadPackage(client, {
3611
+ name: skillName2,
3612
+ dir: filePath,
3613
+ tenantId: tenantId2,
3614
+ visibility: visibility2,
3615
+ type: contentType2
3616
+ });
3617
+ if (!ok2) process.exit(1);
3618
+ return;
3619
+ }
3620
+ const raw = readFileSync7(filePath, "utf-8");
3147
3621
  const content = stripFrontmatter(raw).trim();
3148
3622
  if (!content) {
3149
3623
  console.error("File is empty after stripping frontmatter.");
3150
3624
  process.exit(1);
3151
3625
  return;
3152
3626
  }
3153
- const defaultSlug = basename2(filePath, extname2(filePath));
3627
+ const defaultSlug = basename3(filePath, extname2(filePath));
3154
3628
  const defaultName = titleFromSlug(defaultSlug);
3155
3629
  const skillName = opts.name || defaultName;
3156
3630
  const contentType = validateContentType(opts.type || "skill");
3157
3631
  const visibility = validateVisibility(opts.visibility || "private");
3158
3632
  const tenantId = await resolveTeam(teams, opts.team);
3159
- await uploadSkill(client, {
3633
+ const ok = await uploadSkill(client, {
3160
3634
  name: skillName,
3161
3635
  content,
3162
3636
  tenantId,
3163
3637
  visibility,
3164
3638
  type: contentType
3165
3639
  });
3640
+ if (!ok) process.exit(1);
3166
3641
  } else {
3167
3642
  We("localskills publish");
3168
3643
  const spinner = bt2();
@@ -3187,6 +3662,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
3187
3662
  required: true
3188
3663
  }));
3189
3664
  const tenantId = await resolveTeam(teams, opts.team);
3665
+ let failures = 0;
3190
3666
  for (const skill of skills) {
3191
3667
  R2.step(`Publishing ${skill.suggestedName}...`);
3192
3668
  const name = cancelGuard(await Ze({
@@ -3214,15 +3690,23 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
3214
3690
  ],
3215
3691
  initialValue: skill.contentType
3216
3692
  }));
3217
- await uploadSkill(client, {
3693
+ const ok = skill.format === "package" && skill.dir ? await uploadPackage(client, {
3694
+ name,
3695
+ dir: skill.dir,
3696
+ tenantId,
3697
+ visibility,
3698
+ type: contentType
3699
+ }) : await uploadSkill(client, {
3218
3700
  name,
3219
3701
  content: skill.content,
3220
3702
  tenantId,
3221
3703
  visibility,
3222
3704
  type: contentType
3223
3705
  });
3706
+ if (!ok) failures++;
3224
3707
  }
3225
- Le("Done!");
3708
+ Le(failures > 0 ? `Done, with ${failures} failure(s).` : "Done!");
3709
+ if (failures > 0) process.exit(1);
3226
3710
  }
3227
3711
  }
3228
3712
  );
@@ -3259,10 +3743,37 @@ async function uploadSkill(client, params) {
3259
3743
  });
3260
3744
  if (!res.success || !res.data) {
3261
3745
  spinner.stop(`Failed: ${res.error || "Unknown error"}`);
3262
- return;
3746
+ return false;
3263
3747
  }
3264
3748
  spinner.stop(`Published!`);
3265
3749
  R2.success(`\u2192 localskills.sh/s/${res.data.slug}`);
3750
+ return true;
3751
+ }
3752
+ async function uploadPackage(client, params) {
3753
+ const spinner = bt2();
3754
+ let packed;
3755
+ try {
3756
+ packed = packFolder(params.dir, { requireSkillMd: params.type === "skill" });
3757
+ } catch (e2) {
3758
+ R2.error(e2 instanceof PackError ? e2.message : String(e2));
3759
+ return false;
3760
+ }
3761
+ for (const w of packed.warnings) R2.warn(w);
3762
+ spinner.start(`Uploading ${params.name} (${packed.fileCount} files)...`);
3763
+ const form = new FormData();
3764
+ form.append("name", params.name);
3765
+ form.append("tenantId", params.tenantId);
3766
+ form.append("visibility", params.visibility);
3767
+ form.append("type", params.type);
3768
+ form.append("file", zipBlob(packed.zip), "skill.zip");
3769
+ const res = await client.postForm("/api/skills", form);
3770
+ if (!res.success || !res.data) {
3771
+ spinner.stop(`Failed: ${res.error || "Unknown error"}`);
3772
+ return false;
3773
+ }
3774
+ spinner.stop(`Published! (${packed.fileCount} files)`);
3775
+ R2.success(`\u2192 localskills.sh/s/${res.data.slug}`);
3776
+ return true;
3266
3777
  }
3267
3778
  function validateContentType(value) {
3268
3779
  if (value === "skill" || value === "rule") {
@@ -3279,7 +3790,7 @@ function validateVisibility(value) {
3279
3790
  process.exit(1);
3280
3791
  }
3281
3792
  function shortenPath(filePath) {
3282
- const home = homedir10();
3793
+ const home = homedir11();
3283
3794
  if (filePath.startsWith(home)) {
3284
3795
  return "~" + filePath.slice(home.length);
3285
3796
  }
@@ -3292,41 +3803,78 @@ function shortenPath(filePath) {
3292
3803
 
3293
3804
  // src/commands/push.ts
3294
3805
  import { Command as Command7 } from "commander";
3295
- import { readFileSync as readFileSync7, existsSync as existsSync16 } from "fs";
3806
+ import { readFileSync as readFileSync8, existsSync as existsSync16, statSync as statSync5 } from "fs";
3296
3807
  import { resolve as resolve5 } from "path";
3297
- var pushCommand = new Command7("push").description("Push a new version of an existing skill").argument("<file>", "Path to the skill file").requiredOption("-s, --skill <id>", "Skill ID or slug").option("--version <semver>", "Explicit semver (e.g., 1.1.0)").option("--patch", "Bump patch version").option("--minor", "Bump minor version").option("--major", "Bump major version").option("-m, --message <message>", "Version message").action(
3808
+ var pushCommand = new Command7("push").description("Push a new version of an existing skill").argument("<path>", "Path to the skill file, or a skill folder (uploaded as a package)").requiredOption("-s, --skill <id>", "Skill ID or slug").option("--version <semver>", "Explicit semver (e.g., 1.1.0)").option("--patch", "Bump patch version").option("--minor", "Bump minor version").option("--major", "Bump major version").option("-m, --message <message>", "Version message").action(
3298
3809
  async (fileArg, opts) => {
3299
3810
  const client = new ApiClient();
3300
3811
  requireAuth(client);
3301
3812
  const filePath = resolve5(fileArg);
3302
3813
  if (!existsSync16(filePath)) {
3303
- console.error(`File not found: ${filePath}`);
3814
+ console.error(`Path not found: ${filePath}`);
3304
3815
  process.exit(1);
3305
3816
  return;
3306
3817
  }
3307
- const raw = readFileSync7(filePath, "utf-8");
3308
- const content = stripFrontmatter(raw).trim();
3309
- if (!content) {
3310
- console.error("File is empty after stripping frontmatter.");
3818
+ const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
3819
+ if (bumpFlags.length > 1) {
3820
+ console.error("Specify only one of --patch, --minor, or --major");
3311
3821
  process.exit(1);
3312
3822
  return;
3313
3823
  }
3314
- const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
3315
3824
  if (opts.version && bumpFlags.length > 0) {
3316
3825
  console.error("Cannot specify both --version and --patch/--minor/--major");
3317
3826
  process.exit(1);
3318
3827
  return;
3319
3828
  }
3829
+ if (opts.version && !isValidSemVer(opts.version)) {
3830
+ console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
3831
+ process.exit(1);
3832
+ return;
3833
+ }
3834
+ if (statSync5(filePath).isDirectory()) {
3835
+ let packed;
3836
+ try {
3837
+ packed = packFolder(filePath, { requireSkillMd: false });
3838
+ } catch (e2) {
3839
+ console.error(e2 instanceof PackError ? e2.message : String(e2));
3840
+ process.exit(1);
3841
+ return;
3842
+ }
3843
+ for (const w of packed.warnings) R2.warn(w);
3844
+ const form = new FormData();
3845
+ form.append("file", zipBlob(packed.zip), "skill.zip");
3846
+ if (opts.message) form.append("message", opts.message);
3847
+ if (opts.version) form.append("semver", opts.version);
3848
+ else if (opts.major) form.append("bump", "major");
3849
+ else if (opts.minor) form.append("bump", "minor");
3850
+ else if (opts.patch) form.append("bump", "patch");
3851
+ const spinner2 = bt2();
3852
+ spinner2.start(`Pushing new version (${packed.fileCount} files)...`);
3853
+ const res2 = await client.postForm(
3854
+ `/api/skills/${encodeURIComponent(opts.skill)}/versions`,
3855
+ form
3856
+ );
3857
+ if (!res2.success || !res2.data) {
3858
+ spinner2.stop(`Failed: ${res2.error || "Unknown error"}`);
3859
+ process.exit(1);
3860
+ return;
3861
+ }
3862
+ spinner2.stop(`Pushed ${formatVersionLabel(res2.data.semver, res2.data.version)}`);
3863
+ Le("Done!");
3864
+ return;
3865
+ }
3866
+ const raw = readFileSync8(filePath, "utf-8");
3867
+ const content = stripFrontmatter(raw).trim();
3868
+ if (!content) {
3869
+ console.error("File is empty after stripping frontmatter.");
3870
+ process.exit(1);
3871
+ return;
3872
+ }
3320
3873
  let body = {
3321
3874
  content,
3322
3875
  message: opts.message
3323
3876
  };
3324
3877
  if (opts.version) {
3325
- if (!isValidSemVer(opts.version)) {
3326
- console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
3327
- process.exit(1);
3328
- return;
3329
- }
3330
3878
  body.semver = opts.version;
3331
3879
  } else if (opts.major) {
3332
3880
  body.bump = "major";
@@ -3354,30 +3902,49 @@ var pushCommand = new Command7("push").description("Push a new version of an exi
3354
3902
 
3355
3903
  // src/commands/share.ts
3356
3904
  import { Command as Command8 } from "commander";
3357
- import { readFileSync as readFileSync8, existsSync as existsSync17 } from "fs";
3358
- import { resolve as resolve6, basename as basename3, extname as extname3 } from "path";
3905
+ import { readFileSync as readFileSync9, existsSync as existsSync17, statSync as statSync6 } from "fs";
3906
+ import { resolve as resolve6, basename as basename4, extname as extname3 } from "path";
3359
3907
  import { generateKeyPairSync } from "crypto";
3360
- var shareCommand = new Command8("share").description("Share a skill anonymously (no login required)").argument("[file]", "Path to a specific file to share").option("-n, --name <name>", "Skill name").option("--type <type>", "Content type: skill or rule", "skill").action(async (fileArg, opts) => {
3908
+ var shareCommand = new Command8("share").description("Share a skill anonymously (no login required)").argument("[path]", "Path to a skill file, or a skill folder (uploaded as a package)").option("-n, --name <name>", "Skill name").option("--type <type>", "Content type: skill or rule", "skill").action(async (fileArg, opts) => {
3361
3909
  We("localskills share");
3910
+ if (opts.type && opts.type !== "skill" && opts.type !== "rule") {
3911
+ R2.error(`Invalid type: ${opts.type}. Use skill or rule.`);
3912
+ process.exit(1);
3913
+ }
3362
3914
  await ensureAnonymousIdentity();
3363
3915
  const client = new ApiClient();
3364
3916
  if (fileArg) {
3365
3917
  const filePath = resolve6(fileArg);
3366
3918
  if (!existsSync17(filePath)) {
3367
- R2.error(`File not found: ${filePath}`);
3919
+ R2.error(`Path not found: ${filePath}`);
3368
3920
  process.exit(1);
3369
3921
  }
3370
- const raw = readFileSync8(filePath, "utf-8");
3922
+ if (statSync6(filePath).isDirectory()) {
3923
+ const contentType2 = opts.type === "rule" ? "rule" : "skill";
3924
+ const skillName2 = opts.name || titleFromSlug(basename4(filePath));
3925
+ const ok2 = await uploadAnonymousPackage(client, {
3926
+ name: skillName2,
3927
+ dir: filePath,
3928
+ type: contentType2
3929
+ });
3930
+ Le(ok2 ? "Done!" : "Share failed.");
3931
+ if (!ok2) process.exit(1);
3932
+ return;
3933
+ }
3934
+ const raw = readFileSync9(filePath, "utf-8");
3371
3935
  const content = stripFrontmatter(raw).trim();
3372
3936
  if (!content) {
3373
3937
  R2.error("File is empty after stripping frontmatter.");
3374
3938
  process.exit(1);
3375
3939
  }
3376
- const defaultSlug = basename3(filePath, extname3(filePath));
3940
+ const defaultSlug = basename4(filePath, extname3(filePath));
3377
3941
  const defaultName = titleFromSlug(defaultSlug);
3378
3942
  const skillName = opts.name || defaultName;
3379
3943
  const contentType = opts.type === "rule" ? "rule" : "skill";
3380
- await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
3944
+ const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
3945
+ Le(ok ? "Done!" : "Share failed.");
3946
+ if (!ok) process.exit(1);
3947
+ return;
3381
3948
  } else {
3382
3949
  const spinner = bt2();
3383
3950
  spinner.start("Scanning for skills...");
@@ -3411,13 +3978,18 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3411
3978
  }
3412
3979
  })
3413
3980
  );
3414
- await uploadAnonymousSkill(client, {
3981
+ const ok = selected.format === "package" && selected.dir ? await uploadAnonymousPackage(client, {
3982
+ name,
3983
+ dir: selected.dir,
3984
+ type: selected.contentType
3985
+ }) : await uploadAnonymousSkill(client, {
3415
3986
  name,
3416
3987
  content: selected.content,
3417
3988
  type: selected.contentType
3418
3989
  });
3990
+ Le(ok ? "Done!" : "Share failed.");
3991
+ if (!ok) process.exit(1);
3419
3992
  }
3420
- Le("Done!");
3421
3993
  });
3422
3994
  async function ensureAnonymousIdentity() {
3423
3995
  const config = loadConfig();
@@ -3425,6 +3997,10 @@ async function ensureAnonymousIdentity() {
3425
3997
  const client = new ApiClient();
3426
3998
  const res2 = await client.get("/api/cli/auth");
3427
3999
  if (res2.success) return;
4000
+ if (res2.networkError) {
4001
+ R2.error(`Could not verify your login: ${res2.error}. Try again.`);
4002
+ process.exit(1);
4003
+ }
3428
4004
  }
3429
4005
  let keyPair = getAnonymousKey();
3430
4006
  if (!keyPair) {
@@ -3475,11 +4051,46 @@ async function uploadAnonymousSkill(client, params) {
3475
4051
  });
3476
4052
  if (!res.success || !res.data) {
3477
4053
  s.stop(`Failed: ${res.error || "Unknown error"}`);
3478
- return;
4054
+ return false;
4055
+ }
4056
+ s.stop("Shared!");
4057
+ R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
4058
+ R2.info(`Install: localskills install ${res.data.publicId}`);
4059
+ return true;
4060
+ }
4061
+ async function uploadAnonymousPackage(client, params) {
4062
+ const s = bt2();
4063
+ let packed;
4064
+ try {
4065
+ packed = packFolder(params.dir, { requireSkillMd: params.type === "skill" });
4066
+ } catch (e2) {
4067
+ R2.error(e2 instanceof PackError ? e2.message : String(e2));
4068
+ return false;
4069
+ }
4070
+ for (const w of packed.warnings) R2.warn(w);
4071
+ s.start(`Sharing "${params.name}" (${packed.fileCount} files)...`);
4072
+ const teamsRes = await client.get("/api/tenants");
4073
+ if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
4074
+ s.stop("Failed to find your team. Try running `localskills share` again.");
4075
+ process.exit(1);
4076
+ return false;
4077
+ }
4078
+ const tenantId = teamsRes.data[0].id;
4079
+ const form = new FormData();
4080
+ form.append("name", params.name);
4081
+ form.append("tenantId", tenantId);
4082
+ form.append("visibility", "unlisted");
4083
+ form.append("type", params.type);
4084
+ form.append("file", zipBlob(packed.zip), "skill.zip");
4085
+ const res = await client.postForm("/api/skills", form);
4086
+ if (!res.success || !res.data) {
4087
+ s.stop(`Failed: ${res.error || "Unknown error"}`);
4088
+ return false;
3479
4089
  }
3480
4090
  s.stop("Shared!");
3481
4091
  R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
3482
4092
  R2.info(`Install: localskills install ${res.data.publicId}`);
4093
+ return true;
3483
4094
  }
3484
4095
 
3485
4096
  // src/commands/profile.ts