@localskills/cli 0.3.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +744 -413
  2. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -161,7 +161,7 @@ var require_src = __commonJS({
161
161
  });
162
162
 
163
163
  // src/index.ts
164
- import { Command as Command7 } from "commander";
164
+ import { Command as Command9 } from "commander";
165
165
 
166
166
  // src/commands/auth.ts
167
167
  import { Command } from "commander";
@@ -1114,7 +1114,8 @@ var DEFAULT_CONFIG = {
1114
1114
  defaults: {
1115
1115
  scope: "project",
1116
1116
  method: "symlink"
1117
- }
1117
+ },
1118
+ anonymous_key: null
1118
1119
  };
1119
1120
  function loadConfig() {
1120
1121
  if (!existsSync(CONFIG_PATH)) {
@@ -1159,6 +1160,8 @@ function migrateV1toV2(v1) {
1159
1160
  name: skill.slug,
1160
1161
  hash: skill.hash,
1161
1162
  version: 0,
1163
+ semver: null,
1164
+ semverRange: null,
1162
1165
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
1163
1166
  installations: [
1164
1167
  {
@@ -1187,6 +1190,14 @@ function clearToken() {
1187
1190
  config.token = null;
1188
1191
  saveConfig(config);
1189
1192
  }
1193
+ function getAnonymousKey() {
1194
+ return loadConfig().anonymous_key ?? null;
1195
+ }
1196
+ function setAnonymousKey(key) {
1197
+ const config = loadConfig();
1198
+ config.anonymous_key = key;
1199
+ saveConfig(config);
1200
+ }
1190
1201
 
1191
1202
  // src/lib/api-client.ts
1192
1203
  var ApiClient = class {
@@ -1229,25 +1240,16 @@ var ApiClient = class {
1229
1240
  });
1230
1241
  return this.handleResponse(res);
1231
1242
  }
1232
- async put(path, body) {
1233
- const res = await fetch(`${this.baseUrl}${path}`, {
1234
- method: "PUT",
1235
- headers: this.headers(),
1236
- body: JSON.stringify(body)
1237
- });
1238
- return this.handleResponse(res);
1239
- }
1240
- async delete(path) {
1241
- const res = await fetch(`${this.baseUrl}${path}`, {
1242
- method: "DELETE",
1243
- headers: this.headers()
1244
- });
1245
- return this.handleResponse(res);
1246
- }
1247
- async getRaw(path) {
1248
- return fetch(`${this.baseUrl}${path}`, {
1249
- headers: this.headers()
1250
- });
1243
+ async fetchBinary(url) {
1244
+ const headers = {};
1245
+ if (this.token) {
1246
+ headers["Authorization"] = `Bearer ${this.token}`;
1247
+ }
1248
+ const res = await fetch(url, { headers });
1249
+ if (!res.ok) {
1250
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1251
+ }
1252
+ return Buffer.from(await res.arrayBuffer());
1251
1253
  }
1252
1254
  isAuthenticated() {
1253
1255
  return this.token !== null;
@@ -1285,9 +1287,29 @@ function openBrowser(url) {
1285
1287
  }
1286
1288
  }
1287
1289
  function sleep(ms) {
1288
- return new Promise((resolve4) => setTimeout(resolve4, ms));
1290
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1289
1291
  }
1290
- var loginCommand = new Command("login").description("Log in to localskills.sh").option("--token <token>", "Use an API token directly (headless mode)").action(async (opts) => {
1292
+ var loginCommand = new Command("login").description("Log in to localskills.sh").option("--token <token>", "Use an API token directly (headless mode)").option("--oidc-token <token>", "Exchange a CI/CD OIDC token for an API token").option("--team <slug>", "Team slug (required with --oidc-token)").action(async (opts) => {
1293
+ if (opts.oidcToken) {
1294
+ if (!opts.team) {
1295
+ console.error("Error: --team is required with --oidc-token");
1296
+ process.exit(1);
1297
+ }
1298
+ const client2 = new ApiClient();
1299
+ const res = await client2.post(
1300
+ "/api/oidc/token",
1301
+ { token: opts.oidcToken, team: opts.team }
1302
+ );
1303
+ if (!res.success || !res.data) {
1304
+ console.error(`OIDC login failed: ${res.error || "unknown error"}`);
1305
+ process.exit(1);
1306
+ }
1307
+ setToken(res.data.token);
1308
+ console.log(
1309
+ `Authenticated via OIDC (expires ${new Date(res.data.expiresAt).toISOString()})`
1310
+ );
1311
+ return;
1312
+ }
1291
1313
  if (opts.token) {
1292
1314
  setToken(opts.token);
1293
1315
  const client2 = new ApiClient();
@@ -1382,22 +1404,75 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
1382
1404
 
1383
1405
  // src/commands/install.ts
1384
1406
  import { Command as Command2 } from "commander";
1407
+ import { mkdirSync as mkdirSync7 } from "fs";
1408
+
1409
+ // ../../packages/shared/dist/utils/semver.js
1410
+ var SEMVER_RE = /^\d+\.\d+\.\d+$/;
1411
+ var RANGE_RE = /^(\^|~|>=)?\d+\.\d+\.\d+$|^\*$/;
1412
+ function parseSemVer(v) {
1413
+ if (!SEMVER_RE.test(v))
1414
+ return null;
1415
+ const [major, minor, patch] = v.split(".").map(Number);
1416
+ if (major > 999999 || minor > 999999 || patch > 999999)
1417
+ return null;
1418
+ return { major, minor, patch };
1419
+ }
1420
+ function isValidSemVer(v) {
1421
+ return parseSemVer(v) !== null;
1422
+ }
1423
+ function isValidSemVerRange(range) {
1424
+ return RANGE_RE.test(range);
1425
+ }
1426
+
1427
+ // ../../packages/shared/dist/utils/index.js
1428
+ function titleFromSlug(slug) {
1429
+ return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1430
+ }
1431
+
1432
+ // src/lib/cli-helpers.ts
1433
+ function requireAuth(client) {
1434
+ if (!client.isAuthenticated()) {
1435
+ console.error("Not authenticated. Run `localskills login` first.");
1436
+ process.exit(1);
1437
+ }
1438
+ }
1439
+ function buildVersionQuery(range) {
1440
+ if (!range) return "";
1441
+ if (isValidSemVer(range)) {
1442
+ return `?semver=${encodeURIComponent(range)}`;
1443
+ }
1444
+ if (isValidSemVerRange(range)) {
1445
+ return `?range=${encodeURIComponent(range)}`;
1446
+ }
1447
+ console.error(`Invalid version specifier: ${range}`);
1448
+ process.exit(1);
1449
+ }
1450
+ function formatVersionLabel(semver, version) {
1451
+ return semver ? `v${semver}` : `v${version}`;
1452
+ }
1453
+ function cancelGuard(value) {
1454
+ if (Ct(value)) {
1455
+ Ne("Cancelled.");
1456
+ process.exit(0);
1457
+ }
1458
+ return value;
1459
+ }
1385
1460
 
1386
1461
  // src/lib/cache.ts
1387
1462
  import {
1388
- existsSync as existsSync11,
1389
- mkdirSync as mkdirSync10,
1463
+ existsSync as existsSync13,
1464
+ mkdirSync as mkdirSync5,
1390
1465
  readFileSync as readFileSync4,
1391
1466
  readdirSync,
1392
- writeFileSync as writeFileSync9,
1393
- rmSync as rmSync4
1467
+ writeFileSync as writeFileSync5,
1468
+ rmSync as rmSync3
1394
1469
  } from "fs";
1395
- import { join as join10, resolve as resolve2 } from "path";
1396
- import { homedir as homedir7 } from "os";
1470
+ import { join as join12, resolve as resolve2 } from "path";
1471
+ import { homedir as homedir8 } from "os";
1397
1472
 
1398
1473
  // src/lib/installers/cursor.ts
1399
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1400
- import { join as join2 } from "path";
1474
+ import { existsSync as existsSync4 } from "fs";
1475
+ import { join as join3 } from "path";
1401
1476
  import { homedir as homedir2 } from "os";
1402
1477
 
1403
1478
  // src/lib/content-transform.ts
@@ -1437,10 +1512,13 @@ function stripFrontmatter(content) {
1437
1512
  return content;
1438
1513
  }
1439
1514
 
1515
+ // src/lib/installers/common.ts
1516
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1517
+ import { join as join2 } from "path";
1518
+
1440
1519
  // src/lib/symlink.ts
1441
1520
  import {
1442
1521
  symlinkSync,
1443
- readlinkSync,
1444
1522
  unlinkSync,
1445
1523
  lstatSync,
1446
1524
  existsSync as existsSync2,
@@ -1477,6 +1555,31 @@ function isSymlink(path) {
1477
1555
  }
1478
1556
  }
1479
1557
 
1558
+ // src/lib/installers/common.ts
1559
+ function safeSlugName(slug) {
1560
+ return slug.replace(/\//g, "-");
1561
+ }
1562
+ var DEFAULT_METHOD = "symlink";
1563
+ function installFileOrSymlink(opts, targetPath) {
1564
+ if (opts.method === "symlink") {
1565
+ createSymlink(opts.cachePath, targetPath);
1566
+ } else {
1567
+ mkdirSync3(join2(targetPath, ".."), { recursive: true });
1568
+ writeFileSync2(targetPath, opts.content);
1569
+ }
1570
+ return targetPath;
1571
+ }
1572
+ function uninstallFile(installation) {
1573
+ if (installation.method === "symlink") {
1574
+ removeSymlink(installation.path);
1575
+ } else if (existsSync3(installation.path)) {
1576
+ unlinkSync2(installation.path);
1577
+ }
1578
+ }
1579
+ function defaultTransformContent(content) {
1580
+ return toPlainMD(content);
1581
+ }
1582
+
1480
1583
  // src/lib/installers/cursor.ts
1481
1584
  var descriptor = {
1482
1585
  id: "cursor",
@@ -1490,40 +1593,30 @@ function detect(projectDir) {
1490
1593
  const home = homedir2();
1491
1594
  const cwd = projectDir || process.cwd();
1492
1595
  return {
1493
- global: existsSync3(join2(home, ".cursor")),
1494
- project: existsSync3(join2(cwd, ".cursor"))
1596
+ global: existsSync4(join3(home, ".cursor")),
1597
+ project: existsSync4(join3(cwd, ".cursor"))
1495
1598
  };
1496
1599
  }
1497
1600
  function resolvePath(slug, scope, projectDir, _contentType) {
1498
- const safeName = slug.replace(/\//g, "-");
1601
+ const safeName = safeSlugName(slug);
1499
1602
  if (scope === "global") {
1500
- return join2(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
1603
+ return join3(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
1501
1604
  }
1502
1605
  const dir = projectDir || process.cwd();
1503
- return join2(dir, ".cursor", "rules", `${safeName}.mdc`);
1606
+ return join3(dir, ".cursor", "rules", `${safeName}.mdc`);
1504
1607
  }
1505
1608
  function transformContent(content, skill) {
1506
1609
  return toCursorMDC(content, skill);
1507
1610
  }
1508
1611
  function install(opts) {
1509
1612
  const targetPath = resolvePath(opts.slug, opts.scope, opts.projectDir);
1510
- if (opts.method === "symlink") {
1511
- createSymlink(opts.cachePath, targetPath);
1512
- } else {
1513
- mkdirSync3(join2(targetPath, ".."), { recursive: true });
1514
- writeFileSync2(targetPath, opts.content);
1515
- }
1516
- return targetPath;
1613
+ return installFileOrSymlink(opts, targetPath);
1517
1614
  }
1518
1615
  function uninstall(installation, _slug) {
1519
- if (installation.method === "symlink") {
1520
- removeSymlink(installation.path);
1521
- } else if (existsSync3(installation.path)) {
1522
- unlinkSync2(installation.path);
1523
- }
1616
+ uninstallFile(installation);
1524
1617
  }
1525
1618
  function defaultMethod() {
1526
- return "symlink";
1619
+ return DEFAULT_METHOD;
1527
1620
  }
1528
1621
  var cursorAdapter = {
1529
1622
  descriptor,
@@ -1536,8 +1629,8 @@ var cursorAdapter = {
1536
1629
  };
1537
1630
 
1538
1631
  // src/lib/installers/claude.ts
1539
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, rmSync as rmSync3 } from "fs";
1540
- import { join as join3 } from "path";
1632
+ import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
1633
+ import { join as join4 } from "path";
1541
1634
  import { homedir as homedir3 } from "os";
1542
1635
  var descriptor2 = {
1543
1636
  id: "claude",
@@ -1551,17 +1644,17 @@ function detect2(projectDir) {
1551
1644
  const home = homedir3();
1552
1645
  const cwd = projectDir || process.cwd();
1553
1646
  return {
1554
- global: existsSync4(join3(home, ".claude")),
1555
- project: existsSync4(join3(cwd, ".claude"))
1647
+ global: existsSync5(join4(home, ".claude")),
1648
+ project: existsSync5(join4(cwd, ".claude"))
1556
1649
  };
1557
1650
  }
1558
1651
  function resolvePath2(slug, scope, projectDir, contentType) {
1559
- const safeName = slug.replace(/\//g, "-");
1560
- const base = scope === "global" ? join3(homedir3(), ".claude") : join3(projectDir || process.cwd(), ".claude");
1652
+ const safeName = safeSlugName(slug);
1653
+ const base = scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
1561
1654
  if (contentType === "rule") {
1562
- return join3(base, "rules", `${safeName}.md`);
1655
+ return join4(base, "rules", `${safeName}.md`);
1563
1656
  }
1564
- return join3(base, "skills", safeName, "SKILL.md");
1657
+ return join4(base, "skills", safeName, "SKILL.md");
1565
1658
  }
1566
1659
  function transformContent2(content, skill) {
1567
1660
  if (skill.type === "rule") {
@@ -1571,32 +1664,21 @@ function transformContent2(content, skill) {
1571
1664
  }
1572
1665
  function install2(opts) {
1573
1666
  const targetPath = resolvePath2(opts.slug, opts.scope, opts.projectDir, opts.contentType);
1574
- const targetDir = join3(targetPath, "..");
1575
- if (opts.method === "symlink") {
1576
- createSymlink(opts.cachePath, targetPath);
1577
- } else {
1578
- mkdirSync4(targetDir, { recursive: true });
1579
- writeFileSync3(targetPath, opts.content);
1580
- }
1581
- return targetPath;
1667
+ return installFileOrSymlink(opts, targetPath);
1582
1668
  }
1583
1669
  function uninstall2(installation, _slug) {
1584
- if (installation.method === "symlink") {
1585
- removeSymlink(installation.path);
1586
- } else if (existsSync4(installation.path)) {
1587
- unlinkSync3(installation.path);
1588
- }
1589
- const parentDir = join3(installation.path, "..");
1670
+ uninstallFile(installation);
1671
+ const parentDir = join4(installation.path, "..");
1590
1672
  try {
1591
1673
  const { readdirSync: readdirSync3 } = __require("fs");
1592
- if (existsSync4(parentDir) && readdirSync3(parentDir).length === 0) {
1593
- rmSync3(parentDir, { recursive: true });
1674
+ if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
1675
+ rmSync2(parentDir, { recursive: true });
1594
1676
  }
1595
1677
  } catch {
1596
1678
  }
1597
1679
  }
1598
1680
  function defaultMethod2() {
1599
- return "symlink";
1681
+ return DEFAULT_METHOD;
1600
1682
  }
1601
1683
  var claudeAdapter = {
1602
1684
  descriptor: descriptor2,
@@ -1609,19 +1691,50 @@ var claudeAdapter = {
1609
1691
  };
1610
1692
 
1611
1693
  // src/lib/installers/codex.ts
1612
- import { join as join4 } from "path";
1694
+ import { join as join6 } from "path";
1695
+ import { homedir as homedir5 } from "os";
1696
+
1697
+ // src/lib/detect.ts
1698
+ import { existsSync as existsSync6 } from "fs";
1699
+ import { execFileSync } from "child_process";
1613
1700
  import { homedir as homedir4 } from "os";
1614
- import { execSync } from "child_process";
1701
+ import { join as join5 } from "path";
1702
+ function commandExists(cmd) {
1703
+ try {
1704
+ execFileSync("which", [cmd], { stdio: "ignore" });
1705
+ return true;
1706
+ } catch {
1707
+ return false;
1708
+ }
1709
+ }
1710
+ function detectInstalledPlatforms(projectDir) {
1711
+ const detected = [];
1712
+ const home = homedir4();
1713
+ const cwd = projectDir || process.cwd();
1714
+ if (existsSync6(join5(home, ".cursor")) || existsSync6(join5(cwd, ".cursor")))
1715
+ detected.push("cursor");
1716
+ if (existsSync6(join5(home, ".claude")) || commandExists("claude"))
1717
+ detected.push("claude");
1718
+ if (commandExists("codex")) detected.push("codex");
1719
+ if (existsSync6(join5(home, ".codeium")) || existsSync6(join5(cwd, ".windsurf")))
1720
+ detected.push("windsurf");
1721
+ if (existsSync6(join5(cwd, ".clinerules"))) detected.push("cline");
1722
+ if (existsSync6(join5(cwd, ".github"))) detected.push("copilot");
1723
+ if (commandExists("opencode") || existsSync6(join5(cwd, ".opencode")))
1724
+ detected.push("opencode");
1725
+ if (commandExists("aider")) detected.push("aider");
1726
+ return detected;
1727
+ }
1615
1728
 
1616
1729
  // src/lib/marked-sections.ts
1617
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
1730
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1618
1731
  import { dirname as dirname2 } from "path";
1619
1732
  var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
1620
1733
  var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
1621
1734
  function upsertSection(filePath, slug, content) {
1622
- mkdirSync5(dirname2(filePath), { recursive: true });
1735
+ mkdirSync4(dirname2(filePath), { recursive: true });
1623
1736
  let existing = "";
1624
- if (existsSync5(filePath)) {
1737
+ if (existsSync7(filePath)) {
1625
1738
  existing = readFileSync2(filePath, "utf-8");
1626
1739
  }
1627
1740
  const start = START_MARKER(slug);
@@ -1638,10 +1751,10 @@ ${end}`;
1638
1751
  const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
1639
1752
  result = existing + separator + section + "\n";
1640
1753
  }
1641
- writeFileSync4(filePath, result);
1754
+ writeFileSync3(filePath, result);
1642
1755
  }
1643
1756
  function removeSection(filePath, slug) {
1644
- if (!existsSync5(filePath)) return false;
1757
+ if (!existsSync7(filePath)) return false;
1645
1758
  const existing = readFileSync2(filePath, "utf-8");
1646
1759
  const start = START_MARKER(slug);
1647
1760
  const end = END_MARKER(slug);
@@ -1653,11 +1766,11 @@ function removeSection(filePath, slug) {
1653
1766
  while (before.endsWith("\n\n")) before = before.slice(0, -1);
1654
1767
  while (after.startsWith("\n\n")) after = after.slice(1);
1655
1768
  const result = (before + after).trim();
1656
- writeFileSync4(filePath, result ? result + "\n" : "");
1769
+ writeFileSync3(filePath, result ? result + "\n" : "");
1657
1770
  return true;
1658
1771
  }
1659
1772
  function listSections(filePath) {
1660
- if (!existsSync5(filePath)) return [];
1773
+ if (!existsSync7(filePath)) return [];
1661
1774
  const content = readFileSync2(filePath, "utf-8");
1662
1775
  const regex = /<!-- localskills:start:(.+?) -->/g;
1663
1776
  const slugs = [];
@@ -1678,22 +1791,17 @@ var descriptor3 = {
1678
1791
  fileExtension: ".md"
1679
1792
  };
1680
1793
  function detect3() {
1681
- let hasCommand = false;
1682
- try {
1683
- execSync("which codex", { stdio: "ignore" });
1684
- hasCommand = true;
1685
- } catch {
1686
- }
1794
+ const hasCommand = commandExists("codex");
1687
1795
  return { global: hasCommand, project: hasCommand };
1688
1796
  }
1689
1797
  function resolvePath3(slug, scope, projectDir, _contentType) {
1690
1798
  if (scope === "global") {
1691
- return join4(homedir4(), ".codex", "AGENTS.md");
1799
+ return join6(homedir5(), ".codex", "AGENTS.md");
1692
1800
  }
1693
- return join4(projectDir || process.cwd(), "AGENTS.md");
1801
+ return join6(projectDir || process.cwd(), "AGENTS.md");
1694
1802
  }
1695
1803
  function transformContent3(content) {
1696
- return toPlainMD(content);
1804
+ return defaultTransformContent(content);
1697
1805
  }
1698
1806
  function install3(opts) {
1699
1807
  const filePath = resolvePath3(opts.slug, opts.scope, opts.projectDir);
@@ -1719,9 +1827,9 @@ var codexAdapter = {
1719
1827
  };
1720
1828
 
1721
1829
  // src/lib/installers/windsurf.ts
1722
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4 } from "fs";
1723
- import { join as join5 } from "path";
1724
- import { homedir as homedir5 } from "os";
1830
+ import { existsSync as existsSync8 } from "fs";
1831
+ import { join as join7 } from "path";
1832
+ import { homedir as homedir6 } from "os";
1725
1833
  var descriptor4 = {
1726
1834
  id: "windsurf",
1727
1835
  name: "Windsurf",
@@ -1732,22 +1840,22 @@ var descriptor4 = {
1732
1840
  fileExtension: ".md"
1733
1841
  };
1734
1842
  function detect4(projectDir) {
1735
- const home = homedir5();
1843
+ const home = homedir6();
1736
1844
  const cwd = projectDir || process.cwd();
1737
1845
  return {
1738
- global: existsSync6(join5(home, ".codeium")),
1739
- project: existsSync6(join5(cwd, ".windsurf"))
1846
+ global: existsSync8(join7(home, ".codeium")),
1847
+ project: existsSync8(join7(cwd, ".windsurf"))
1740
1848
  };
1741
1849
  }
1742
1850
  function resolvePath4(slug, scope, projectDir, _contentType) {
1743
- const safeName = slug.replace(/\//g, "-");
1851
+ const safeName = safeSlugName(slug);
1744
1852
  if (scope === "global") {
1745
- return join5(homedir5(), ".codeium", "windsurf", "memories", "global_rules.md");
1853
+ return join7(homedir6(), ".codeium", "windsurf", "memories", "global_rules.md");
1746
1854
  }
1747
- return join5(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
1855
+ return join7(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
1748
1856
  }
1749
1857
  function transformContent4(content) {
1750
- return toPlainMD(content);
1858
+ return defaultTransformContent(content);
1751
1859
  }
1752
1860
  function install4(opts) {
1753
1861
  const targetPath = resolvePath4(opts.slug, opts.scope, opts.projectDir);
@@ -1755,21 +1863,16 @@ function install4(opts) {
1755
1863
  upsertSection(targetPath, opts.slug, `## ${opts.slug}
1756
1864
 
1757
1865
  ${opts.content}`);
1758
- } else if (opts.method === "symlink") {
1759
- createSymlink(opts.cachePath, targetPath);
1760
1866
  } else {
1761
- mkdirSync6(join5(targetPath, ".."), { recursive: true });
1762
- writeFileSync5(targetPath, opts.content);
1867
+ installFileOrSymlink(opts, targetPath);
1763
1868
  }
1764
1869
  return targetPath;
1765
1870
  }
1766
1871
  function uninstall4(installation, slug) {
1767
1872
  if (installation.method === "section") {
1768
1873
  removeSection(installation.path, slug);
1769
- } else if (installation.method === "symlink") {
1770
- removeSymlink(installation.path);
1771
- } else if (existsSync6(installation.path)) {
1772
- unlinkSync4(installation.path);
1874
+ } else {
1875
+ uninstallFile(installation);
1773
1876
  }
1774
1877
  }
1775
1878
  function defaultMethod4(scope) {
@@ -1786,8 +1889,8 @@ var windsurfAdapter = {
1786
1889
  };
1787
1890
 
1788
1891
  // src/lib/installers/cline.ts
1789
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync5 } from "fs";
1790
- import { join as join6 } from "path";
1892
+ import { existsSync as existsSync9 } from "fs";
1893
+ import { join as join8 } from "path";
1791
1894
  var descriptor5 = {
1792
1895
  id: "cline",
1793
1896
  name: "Cline",
@@ -1800,38 +1903,28 @@ function detect5(projectDir) {
1800
1903
  const cwd = projectDir || process.cwd();
1801
1904
  return {
1802
1905
  global: false,
1803
- project: existsSync7(join6(cwd, ".clinerules"))
1906
+ project: existsSync9(join8(cwd, ".clinerules"))
1804
1907
  };
1805
1908
  }
1806
1909
  function resolvePath5(slug, scope, projectDir, _contentType) {
1807
1910
  if (scope === "global") {
1808
1911
  throw new Error("Cline does not support global installation");
1809
1912
  }
1810
- const safeName = slug.replace(/\//g, "-");
1811
- return join6(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
1913
+ const safeName = safeSlugName(slug);
1914
+ return join8(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
1812
1915
  }
1813
1916
  function transformContent5(content) {
1814
- return toPlainMD(content);
1917
+ return defaultTransformContent(content);
1815
1918
  }
1816
1919
  function install5(opts) {
1817
1920
  const targetPath = resolvePath5(opts.slug, opts.scope, opts.projectDir);
1818
- if (opts.method === "symlink") {
1819
- createSymlink(opts.cachePath, targetPath);
1820
- } else {
1821
- mkdirSync7(join6(targetPath, ".."), { recursive: true });
1822
- writeFileSync6(targetPath, opts.content);
1823
- }
1824
- return targetPath;
1921
+ return installFileOrSymlink(opts, targetPath);
1825
1922
  }
1826
1923
  function uninstall5(installation, _slug) {
1827
- if (installation.method === "symlink") {
1828
- removeSymlink(installation.path);
1829
- } else if (existsSync7(installation.path)) {
1830
- unlinkSync5(installation.path);
1831
- }
1924
+ uninstallFile(installation);
1832
1925
  }
1833
1926
  function defaultMethod5() {
1834
- return "symlink";
1927
+ return DEFAULT_METHOD;
1835
1928
  }
1836
1929
  var clineAdapter = {
1837
1930
  descriptor: descriptor5,
@@ -1844,8 +1937,8 @@ var clineAdapter = {
1844
1937
  };
1845
1938
 
1846
1939
  // src/lib/installers/copilot.ts
1847
- import { existsSync as existsSync8 } from "fs";
1848
- import { join as join7 } from "path";
1940
+ import { existsSync as existsSync10 } from "fs";
1941
+ import { join as join9 } from "path";
1849
1942
  var descriptor6 = {
1850
1943
  id: "copilot",
1851
1944
  name: "GitHub Copilot",
@@ -1858,17 +1951,17 @@ function detect6(projectDir) {
1858
1951
  const cwd = projectDir || process.cwd();
1859
1952
  return {
1860
1953
  global: false,
1861
- project: existsSync8(join7(cwd, ".github"))
1954
+ project: existsSync10(join9(cwd, ".github"))
1862
1955
  };
1863
1956
  }
1864
1957
  function resolvePath6(slug, scope, projectDir, _contentType) {
1865
1958
  if (scope === "global") {
1866
1959
  throw new Error("GitHub Copilot does not support global installation");
1867
1960
  }
1868
- return join7(projectDir || process.cwd(), ".github", "copilot-instructions.md");
1961
+ return join9(projectDir || process.cwd(), ".github", "copilot-instructions.md");
1869
1962
  }
1870
1963
  function transformContent6(content) {
1871
- return toPlainMD(content);
1964
+ return defaultTransformContent(content);
1872
1965
  }
1873
1966
  function install6(opts) {
1874
1967
  const filePath = resolvePath6(opts.slug, opts.scope, opts.projectDir);
@@ -1894,10 +1987,9 @@ var copilotAdapter = {
1894
1987
  };
1895
1988
 
1896
1989
  // src/lib/installers/opencode.ts
1897
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync6 } from "fs";
1898
- import { join as join8 } from "path";
1899
- import { homedir as homedir6 } from "os";
1900
- import { execSync as execSync2 } from "child_process";
1990
+ import { existsSync as existsSync11 } from "fs";
1991
+ import { join as join10 } from "path";
1992
+ import { homedir as homedir7 } from "os";
1901
1993
  var descriptor7 = {
1902
1994
  id: "opencode",
1903
1995
  name: "OpenCode",
@@ -1908,46 +2000,31 @@ var descriptor7 = {
1908
2000
  };
1909
2001
  function detect7(projectDir) {
1910
2002
  const cwd = projectDir || process.cwd();
1911
- let hasCommand = false;
1912
- try {
1913
- execSync2("which opencode", { stdio: "ignore" });
1914
- hasCommand = true;
1915
- } catch {
1916
- }
2003
+ const hasCommand = commandExists("opencode");
1917
2004
  return {
1918
2005
  global: hasCommand,
1919
- project: hasCommand || existsSync9(join8(cwd, ".opencode"))
2006
+ project: hasCommand || existsSync11(join10(cwd, ".opencode"))
1920
2007
  };
1921
2008
  }
1922
2009
  function resolvePath7(slug, scope, projectDir, _contentType) {
1923
- const safeName = slug.replace(/\//g, "-");
2010
+ const safeName = safeSlugName(slug);
1924
2011
  if (scope === "global") {
1925
- return join8(homedir6(), ".config", "opencode", "rules", `${safeName}.md`);
2012
+ return join10(homedir7(), ".config", "opencode", "rules", `${safeName}.md`);
1926
2013
  }
1927
- return join8(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
2014
+ return join10(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
1928
2015
  }
1929
2016
  function transformContent7(content) {
1930
- return toPlainMD(content);
2017
+ return defaultTransformContent(content);
1931
2018
  }
1932
2019
  function install7(opts) {
1933
2020
  const targetPath = resolvePath7(opts.slug, opts.scope, opts.projectDir);
1934
- if (opts.method === "symlink") {
1935
- createSymlink(opts.cachePath, targetPath);
1936
- } else {
1937
- mkdirSync8(join8(targetPath, ".."), { recursive: true });
1938
- writeFileSync7(targetPath, opts.content);
1939
- }
1940
- return targetPath;
2021
+ return installFileOrSymlink(opts, targetPath);
1941
2022
  }
1942
2023
  function uninstall7(installation, _slug) {
1943
- if (installation.method === "symlink") {
1944
- removeSymlink(installation.path);
1945
- } else if (existsSync9(installation.path)) {
1946
- unlinkSync6(installation.path);
1947
- }
2024
+ uninstallFile(installation);
1948
2025
  }
1949
2026
  function defaultMethod7() {
1950
- return "symlink";
2027
+ return DEFAULT_METHOD;
1951
2028
  }
1952
2029
  var opencodeAdapter = {
1953
2030
  descriptor: descriptor7,
@@ -1960,9 +2037,8 @@ var opencodeAdapter = {
1960
2037
  };
1961
2038
 
1962
2039
  // src/lib/installers/aider.ts
1963
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync7, readFileSync as readFileSync3 } from "fs";
1964
- import { join as join9 } from "path";
1965
- import { execSync as execSync3 } from "child_process";
2040
+ import { existsSync as existsSync12, writeFileSync as writeFileSync4, readFileSync as readFileSync3 } from "fs";
2041
+ import { join as join11 } from "path";
1966
2042
  var descriptor8 = {
1967
2043
  id: "aider",
1968
2044
  name: "Aider",
@@ -1972,29 +2048,24 @@ var descriptor8 = {
1972
2048
  fileExtension: ".md"
1973
2049
  };
1974
2050
  function detect8() {
1975
- let hasCommand = false;
1976
- try {
1977
- execSync3("which aider", { stdio: "ignore" });
1978
- hasCommand = true;
1979
- } catch {
1980
- }
2051
+ const hasCommand = commandExists("aider");
1981
2052
  return { global: false, project: hasCommand };
1982
2053
  }
1983
2054
  function resolvePath8(slug, scope, projectDir, contentType) {
1984
2055
  if (scope === "global") {
1985
2056
  throw new Error("Aider does not support global installation");
1986
2057
  }
1987
- const safeName = slug.replace(/\//g, "-");
2058
+ const safeName = safeSlugName(slug);
1988
2059
  const subdir = contentType === "rule" ? "rules" : "skills";
1989
- return join9(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
2060
+ return join11(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
1990
2061
  }
1991
2062
  function transformContent8(content) {
1992
- return toPlainMD(content);
2063
+ return defaultTransformContent(content);
1993
2064
  }
1994
2065
  function addAiderRead(projectDir, relativePath) {
1995
- const configPath = join9(projectDir, ".aider.conf.yml");
2066
+ const configPath = join11(projectDir, ".aider.conf.yml");
1996
2067
  let content = "";
1997
- if (existsSync10(configPath)) {
2068
+ if (existsSync12(configPath)) {
1998
2069
  content = readFileSync3(configPath, "utf-8");
1999
2070
  }
2000
2071
  if (content.includes(relativePath)) return;
@@ -2004,44 +2075,35 @@ function addAiderRead(projectDir, relativePath) {
2004
2075
  } else {
2005
2076
  content = content.trimEnd() + (content ? "\n" : "") + readLine + "\n";
2006
2077
  }
2007
- writeFileSync8(configPath, content);
2078
+ writeFileSync4(configPath, content);
2008
2079
  }
2009
2080
  function removeAiderRead(projectDir, relativePath) {
2010
- const configPath = join9(projectDir, ".aider.conf.yml");
2011
- if (!existsSync10(configPath)) return;
2081
+ const configPath = join11(projectDir, ".aider.conf.yml");
2082
+ if (!existsSync12(configPath)) return;
2012
2083
  let content = readFileSync3(configPath, "utf-8");
2013
2084
  const lines = content.split("\n");
2014
2085
  const filtered = lines.filter((line) => !line.includes(relativePath));
2015
- writeFileSync8(configPath, filtered.join("\n"));
2086
+ writeFileSync4(configPath, filtered.join("\n"));
2016
2087
  }
2017
2088
  function install8(opts) {
2018
2089
  const targetPath = resolvePath8(opts.slug, opts.scope, opts.projectDir, opts.contentType);
2019
2090
  const projectDir = opts.projectDir || process.cwd();
2020
- if (opts.method === "symlink") {
2021
- createSymlink(opts.cachePath, targetPath);
2022
- } else {
2023
- mkdirSync9(join9(targetPath, ".."), { recursive: true });
2024
- writeFileSync8(targetPath, opts.content);
2025
- }
2026
- const safeName = opts.slug.replace(/\//g, "-");
2091
+ installFileOrSymlink(opts, targetPath);
2092
+ const safeName = safeSlugName(opts.slug);
2027
2093
  const subdir = opts.contentType === "rule" ? "rules" : "skills";
2028
2094
  const relativePath = `.aider/${subdir}/${safeName}.md`;
2029
2095
  addAiderRead(projectDir, relativePath);
2030
2096
  return targetPath;
2031
2097
  }
2032
2098
  function uninstall8(installation, slug) {
2033
- if (installation.method === "symlink") {
2034
- removeSymlink(installation.path);
2035
- } else if (existsSync10(installation.path)) {
2036
- unlinkSync7(installation.path);
2037
- }
2099
+ uninstallFile(installation);
2038
2100
  const projectDir = installation.projectDir || process.cwd();
2039
- const safeName = slug.replace(/\//g, "-");
2101
+ const safeName = safeSlugName(slug);
2040
2102
  removeAiderRead(projectDir, `.aider/skills/${safeName}.md`);
2041
2103
  removeAiderRead(projectDir, `.aider/rules/${safeName}.md`);
2042
2104
  }
2043
2105
  function defaultMethod8() {
2044
- return "symlink";
2106
+ return DEFAULT_METHOD;
2045
2107
  }
2046
2108
  var aiderAdapter = {
2047
2109
  descriptor: descriptor8,
@@ -2074,7 +2136,7 @@ function getAllAdapters() {
2074
2136
  }
2075
2137
 
2076
2138
  // src/lib/cache.ts
2077
- var CACHE_DIR = join10(homedir7(), ".localskills", "cache");
2139
+ var CACHE_DIR = join12(homedir8(), ".localskills", "cache");
2078
2140
  function slugToDir(slug) {
2079
2141
  if (slug.includes("..") || slug.includes("\0")) {
2080
2142
  throw new Error("Invalid slug: contains forbidden characters");
@@ -2082,7 +2144,7 @@ function slugToDir(slug) {
2082
2144
  return slug.replace(/\//g, "--");
2083
2145
  }
2084
2146
  function getCacheDir(slug) {
2085
- const dir = resolve2(join10(CACHE_DIR, slugToDir(slug)));
2147
+ const dir = resolve2(join12(CACHE_DIR, slugToDir(slug)));
2086
2148
  if (!dir.startsWith(resolve2(CACHE_DIR) + "/") && dir !== resolve2(CACHE_DIR)) {
2087
2149
  throw new Error("Invalid slug: path traversal detected");
2088
2150
  }
@@ -2090,17 +2152,18 @@ function getCacheDir(slug) {
2090
2152
  }
2091
2153
  function store(slug, content, skill, version) {
2092
2154
  const dir = getCacheDir(slug);
2093
- mkdirSync10(dir, { recursive: true });
2094
- writeFileSync9(join10(dir, "raw.md"), content);
2155
+ mkdirSync5(dir, { recursive: true });
2156
+ writeFileSync5(join12(dir, "raw.md"), content);
2095
2157
  const meta = {
2096
2158
  hash: skill.contentHash,
2097
2159
  version,
2160
+ semver: skill.currentSemver ?? null,
2098
2161
  name: skill.name,
2099
2162
  description: skill.description,
2100
2163
  type: skill.type ?? "skill",
2101
2164
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2102
2165
  };
2103
- writeFileSync9(join10(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2166
+ writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2104
2167
  clearPlatformFiles(slug);
2105
2168
  }
2106
2169
  function getPlatformFile(slug, platform, skill) {
@@ -2111,98 +2174,103 @@ function getPlatformFile(slug, platform, skill) {
2111
2174
  const transformed = adapter.transformContent(raw, skill);
2112
2175
  if (platform === "claude") {
2113
2176
  if (skill.type === "rule") {
2114
- const claudeRuleDir = join10(dir, "claude-rule");
2115
- mkdirSync10(claudeRuleDir, { recursive: true });
2116
- const filePath3 = join10(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
2117
- writeFileSync9(filePath3, transformed);
2177
+ const claudeRuleDir = join12(dir, "claude-rule");
2178
+ mkdirSync5(claudeRuleDir, { recursive: true });
2179
+ const filePath3 = join12(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
2180
+ writeFileSync5(filePath3, transformed);
2118
2181
  return filePath3;
2119
2182
  }
2120
- const claudeDir = join10(dir, "claude");
2121
- mkdirSync10(claudeDir, { recursive: true });
2122
- const filePath2 = join10(claudeDir, "SKILL.md");
2123
- writeFileSync9(filePath2, transformed);
2183
+ const claudeDir = join12(dir, "claude");
2184
+ mkdirSync5(claudeDir, { recursive: true });
2185
+ const filePath2 = join12(claudeDir, "SKILL.md");
2186
+ writeFileSync5(filePath2, transformed);
2124
2187
  return filePath2;
2125
2188
  }
2126
2189
  const ext = adapter.descriptor.fileExtension;
2127
- const filePath = join10(dir, `${platform}${ext}`);
2128
- writeFileSync9(filePath, transformed);
2190
+ const filePath = join12(dir, `${platform}${ext}`);
2191
+ writeFileSync5(filePath, transformed);
2129
2192
  return filePath;
2130
2193
  }
2131
2194
  function getRawContent(slug) {
2132
- const filePath = join10(getCacheDir(slug), "raw.md");
2133
- if (!existsSync11(filePath)) return null;
2195
+ const filePath = join12(getCacheDir(slug), "raw.md");
2196
+ if (!existsSync13(filePath)) return null;
2134
2197
  return readFileSync4(filePath, "utf-8");
2135
2198
  }
2136
2199
  function purge(slug) {
2137
2200
  const dir = getCacheDir(slug);
2138
- if (existsSync11(dir)) {
2139
- rmSync4(dir, { recursive: true, force: true });
2201
+ if (existsSync13(dir)) {
2202
+ rmSync3(dir, { recursive: true, force: true });
2140
2203
  }
2141
2204
  }
2205
+ function storePackage(slug, zipBuffer, manifest, skill, version) {
2206
+ const dir = getCacheDir(slug);
2207
+ mkdirSync5(dir, { recursive: true });
2208
+ writeFileSync5(join12(dir, "package.zip"), zipBuffer);
2209
+ writeFileSync5(join12(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
2210
+ const meta = {
2211
+ hash: skill.contentHash,
2212
+ version,
2213
+ semver: skill.currentSemver ?? null,
2214
+ name: skill.name,
2215
+ description: skill.description,
2216
+ type: skill.type ?? "skill",
2217
+ format: "package",
2218
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2219
+ };
2220
+ writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2221
+ }
2142
2222
  function clearPlatformFiles(slug) {
2143
2223
  const dir = getCacheDir(slug);
2144
- if (!existsSync11(dir)) return;
2145
- const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json"]);
2224
+ if (!existsSync13(dir)) return;
2225
+ const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "package.zip", "manifest.json"]);
2146
2226
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2147
2227
  if (!keep.has(entry.name)) {
2148
- rmSync4(join10(dir, entry.name), { recursive: true, force: true });
2228
+ rmSync3(join12(dir, entry.name), { recursive: true, force: true });
2149
2229
  }
2150
2230
  }
2151
2231
  }
2152
2232
 
2153
- // src/lib/detect.ts
2154
- import { existsSync as existsSync12 } from "fs";
2155
- import { execFileSync } from "child_process";
2156
- import { homedir as homedir8 } from "os";
2157
- import { join as join11 } from "path";
2158
- function commandExists(cmd) {
2159
- try {
2160
- execFileSync("which", [cmd], { stdio: "ignore" });
2161
- return true;
2162
- } catch {
2163
- return false;
2233
+ // src/lib/extract.ts
2234
+ import { unzipSync } from "fflate";
2235
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
2236
+ import { join as join13, dirname as dirname3, resolve as resolve3 } from "path";
2237
+ function extractPackage(zipBuffer, targetDir) {
2238
+ const resolvedTarget = resolve3(targetDir);
2239
+ const extracted = unzipSync(new Uint8Array(zipBuffer));
2240
+ const writtenFiles = [];
2241
+ for (const [path, data] of Object.entries(extracted)) {
2242
+ if (path.endsWith("/")) continue;
2243
+ if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
2244
+ continue;
2245
+ }
2246
+ const fullPath = resolve3(join13(targetDir, path));
2247
+ if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
2248
+ continue;
2249
+ }
2250
+ mkdirSync6(dirname3(fullPath), { recursive: true });
2251
+ writeFileSync6(fullPath, Buffer.from(data));
2252
+ writtenFiles.push(path);
2164
2253
  }
2165
- }
2166
- function detectInstalledPlatforms(projectDir) {
2167
- const detected = [];
2168
- const home = homedir8();
2169
- const cwd = projectDir || process.cwd();
2170
- if (existsSync12(join11(home, ".cursor")) || existsSync12(join11(cwd, ".cursor")))
2171
- detected.push("cursor");
2172
- if (existsSync12(join11(home, ".claude")) || commandExists("claude"))
2173
- detected.push("claude");
2174
- if (commandExists("codex")) detected.push("codex");
2175
- if (existsSync12(join11(home, ".codeium")) || existsSync12(join11(cwd, ".windsurf")))
2176
- detected.push("windsurf");
2177
- if (existsSync12(join11(cwd, ".clinerules"))) detected.push("cline");
2178
- if (existsSync12(join11(cwd, ".github"))) detected.push("copilot");
2179
- if (commandExists("opencode") || existsSync12(join11(cwd, ".opencode")))
2180
- detected.push("opencode");
2181
- if (commandExists("aider")) detected.push("aider");
2182
- return detected;
2254
+ return writtenFiles;
2183
2255
  }
2184
2256
 
2185
2257
  // src/lib/interactive.ts
2186
2258
  async function interactiveInstall(availableSkills, detectedPlatforms) {
2187
2259
  We("localskills install");
2188
- const slug = await Je({
2260
+ const slug = cancelGuard(await Je({
2189
2261
  message: "Which skill would you like to install?",
2190
2262
  options: availableSkills.map((s) => ({
2191
2263
  value: s.slug,
2192
2264
  label: s.name,
2193
2265
  hint: truncate(s.description, 60)
2194
2266
  }))
2195
- });
2196
- if (Ct(slug)) {
2197
- Ne("Cancelled.");
2198
- process.exit(0);
2199
- }
2267
+ }));
2200
2268
  const rest = await interactiveTargets(detectedPlatforms);
2201
2269
  return { slug, ...rest };
2202
2270
  }
2203
2271
  async function interactiveTargets(detectedPlatforms) {
2204
2272
  const allAdapters = getAllAdapters();
2205
- const platforms = await je({
2273
+ const platforms = cancelGuard(await je({
2206
2274
  message: "Which platforms should receive this skill?",
2207
2275
  options: allAdapters.map((a) => ({
2208
2276
  value: a.descriptor.id,
@@ -2211,12 +2279,8 @@ async function interactiveTargets(detectedPlatforms) {
2211
2279
  })),
2212
2280
  initialValues: detectedPlatforms.length > 0 ? detectedPlatforms : void 0,
2213
2281
  required: true
2214
- });
2215
- if (Ct(platforms)) {
2216
- Ne("Cancelled.");
2217
- process.exit(0);
2218
- }
2219
- const scope = await Je({
2282
+ }));
2283
+ const scope = cancelGuard(await Je({
2220
2284
  message: "Install scope?",
2221
2285
  options: [
2222
2286
  {
@@ -2231,12 +2295,8 @@ async function interactiveTargets(detectedPlatforms) {
2231
2295
  }
2232
2296
  ],
2233
2297
  initialValue: "project"
2234
- });
2235
- if (Ct(scope)) {
2236
- Ne("Cancelled.");
2237
- process.exit(0);
2238
- }
2239
- const method = await Je({
2298
+ }));
2299
+ const method = cancelGuard(await Je({
2240
2300
  message: "Install method?",
2241
2301
  options: [
2242
2302
  {
@@ -2251,31 +2311,18 @@ async function interactiveTargets(detectedPlatforms) {
2251
2311
  }
2252
2312
  ],
2253
2313
  initialValue: "symlink"
2254
- });
2255
- if (Ct(method)) {
2256
- Ne("Cancelled.");
2257
- process.exit(0);
2258
- }
2259
- return {
2260
- platforms,
2261
- scope,
2262
- method
2263
- };
2314
+ }));
2315
+ return { platforms, scope, method };
2264
2316
  }
2265
2317
  async function interactiveUninstall(installedSlugs) {
2266
2318
  We("localskills uninstall");
2267
- const slug = await Je({
2319
+ return cancelGuard(await Je({
2268
2320
  message: "Which skill would you like to uninstall?",
2269
2321
  options: installedSlugs.map((s) => ({
2270
2322
  value: s,
2271
2323
  label: s
2272
2324
  }))
2273
- });
2274
- if (Ct(slug)) {
2275
- Ne("Cancelled.");
2276
- process.exit(0);
2277
- }
2278
- return slug;
2325
+ }));
2279
2326
  }
2280
2327
  function truncate(str, max) {
2281
2328
  if (str.length <= max) return str;
@@ -2311,6 +2358,19 @@ function parsePlatforms(raw) {
2311
2358
  }
2312
2359
  return platforms;
2313
2360
  }
2361
+ function buildSkillRecord(cacheKey, skill, version, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
2362
+ return {
2363
+ slug: cacheKey,
2364
+ name: skill.name,
2365
+ type: skill.type ?? "skill",
2366
+ hash: skill.contentHash,
2367
+ version,
2368
+ semver: resolvedSemver ?? null,
2369
+ semverRange: requestedRange ?? null,
2370
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
2371
+ installations: existingInstallations ? [...existingInstallations, ...newInstallations] : newInstallations
2372
+ };
2373
+ }
2314
2374
  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(
2315
2375
  async (slugArg, opts) => {
2316
2376
  const client = new ApiClient();
@@ -2363,10 +2423,17 @@ var installCommand = new Command2("install").description("Install a skill locall
2363
2423
  method = explicitMethod || answers.method;
2364
2424
  }
2365
2425
  }
2426
+ let requestedRange = null;
2427
+ if (slug.includes("@")) {
2428
+ const atIdx = slug.lastIndexOf("@");
2429
+ requestedRange = slug.substring(atIdx + 1);
2430
+ slug = slug.substring(0, atIdx);
2431
+ }
2432
+ const versionQuery = buildVersionQuery(requestedRange);
2366
2433
  const spinner = bt2();
2367
2434
  spinner.start(`Fetching ${slug}...`);
2368
2435
  const res = await client.get(
2369
- `/api/skills/${encodeURIComponent(slug)}/content`
2436
+ `/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
2370
2437
  );
2371
2438
  if (!res.success || !res.data) {
2372
2439
  spinner.stop("Failed.");
@@ -2378,9 +2445,62 @@ var installCommand = new Command2("install").description("Install a skill locall
2378
2445
  process.exit(1);
2379
2446
  return;
2380
2447
  }
2381
- const { skill, content, version } = res.data;
2382
- spinner.stop(`Fetched ${skill.name} v${version}`);
2383
- const cacheKey = skill.publicId || slug;
2448
+ const resData = res.data;
2449
+ const format = resData.format ?? "text";
2450
+ const cacheKey = resData.skill.publicId || slug;
2451
+ if (format === "package") {
2452
+ const { skill: skill2, downloadUrl, manifest, version: version2, semver: resolvedSemver2 } = resData;
2453
+ spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version2)} (package, ${manifest.files.length} files)`);
2454
+ const dlSpinner = bt2();
2455
+ dlSpinner.start("Downloading package...");
2456
+ const zipBuffer = await client.fetchBinary(downloadUrl);
2457
+ dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
2458
+ storePackage(cacheKey, zipBuffer, manifest, skill2, version2);
2459
+ const installations2 = [];
2460
+ const results2 = [];
2461
+ for (const platformId of platforms) {
2462
+ const adapter = getAdapter(platformId);
2463
+ const desc = adapter.descriptor;
2464
+ if (scope === "global" && !desc.supportsGlobal) {
2465
+ R2.warn(`${desc.name} does not support global \u2014 skipping.`);
2466
+ continue;
2467
+ }
2468
+ if (scope === "project" && !desc.supportsProject) {
2469
+ R2.warn(`${desc.name} does not support project \u2014 skipping.`);
2470
+ continue;
2471
+ }
2472
+ const targetPath = adapter.resolvePath(cacheKey, scope, projectDir, skill2.type ?? "skill");
2473
+ mkdirSync7(targetPath, { recursive: true });
2474
+ const written = extractPackage(zipBuffer, targetPath);
2475
+ const installation = {
2476
+ platform: platformId,
2477
+ scope,
2478
+ method: "copy",
2479
+ path: targetPath,
2480
+ projectDir,
2481
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2482
+ };
2483
+ installations2.push(installation);
2484
+ results2.push(`${desc.name} \u2192 ${targetPath} (${written.length} files extracted)`);
2485
+ }
2486
+ config.installed_skills[cacheKey] = buildSkillRecord(
2487
+ cacheKey,
2488
+ skill2,
2489
+ version2,
2490
+ resolvedSemver2,
2491
+ requestedRange,
2492
+ config.installed_skills[cacheKey]?.installations,
2493
+ installations2
2494
+ );
2495
+ saveConfig(config);
2496
+ for (const r of results2) {
2497
+ R2.success(r);
2498
+ }
2499
+ Le(`Done! Installed to ${installations2.length} target(s).`);
2500
+ return;
2501
+ }
2502
+ const { skill, content, version, semver: resolvedSemver } = resData;
2503
+ spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version)}`);
2384
2504
  store(cacheKey, content, skill, version);
2385
2505
  const contentType = skill.type ?? "skill";
2386
2506
  const installations = [];
@@ -2420,17 +2540,15 @@ var installCommand = new Command2("install").description("Install a skill locall
2420
2540
  const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
2421
2541
  results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
2422
2542
  }
2423
- const existing = config.installed_skills[cacheKey];
2424
- const skillRecord = {
2425
- slug: cacheKey,
2426
- name: skill.name,
2427
- type: contentType,
2428
- hash: skill.contentHash,
2543
+ config.installed_skills[cacheKey] = buildSkillRecord(
2544
+ cacheKey,
2545
+ skill,
2429
2546
  version,
2430
- cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
2431
- installations: existing ? [...existing.installations, ...installations] : installations
2432
- };
2433
- config.installed_skills[cacheKey] = skillRecord;
2547
+ resolvedSemver,
2548
+ requestedRange,
2549
+ config.installed_skills[cacheKey]?.installations,
2550
+ installations
2551
+ );
2434
2552
  saveConfig(config);
2435
2553
  for (const r of results) {
2436
2554
  R2.success(r);
@@ -2489,33 +2607,62 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
2489
2607
 
2490
2608
  // src/commands/list.ts
2491
2609
  import { Command as Command4 } from "commander";
2492
- var listCommand = new Command4("list").description("List available skills").option("--public", "Show public skills only").action(async (opts) => {
2610
+ var listCommand = new Command4("list").description("List available skills").option("--public", "Show public skills only").option("--tag <tag>", "Filter by tag (requires --public)").option("--search <query>", "Search skills (requires --public)").action(async (opts) => {
2493
2611
  const client = new ApiClient();
2494
- if (!client.isAuthenticated()) {
2495
- console.error("Not authenticated. Run `localskills login` first.");
2612
+ if ((opts.tag || opts.search) && !opts.public) {
2613
+ console.error("The --tag and --search flags require --public.");
2496
2614
  process.exit(1);
2497
2615
  }
2498
- const path = opts.public ? "/api/skills?visibility=public" : "/api/skills";
2499
- const res = await client.get(path);
2500
- if (!res.success || !res.data) {
2501
- console.error(`Error: ${res.error || "Failed to fetch skills"}`);
2502
- process.exit(1);
2503
- return;
2504
- }
2505
- if (res.data.length === 0) {
2506
- console.log("No skills found.");
2507
- return;
2508
- }
2509
- console.log("Available skills:\n");
2510
- for (const skill of res.data) {
2511
- const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
2512
- console.log(` ${skill.slug}${vis} \u2014 ${skill.description || skill.name}`);
2513
- }
2514
- console.log(`
2616
+ if (opts.public) {
2617
+ const params = new URLSearchParams();
2618
+ if (opts.tag) params.set("tag", opts.tag);
2619
+ if (opts.search) params.set("q", opts.search);
2620
+ const qs = params.toString();
2621
+ const path = qs ? `/api/explore?${qs}` : "/api/explore";
2622
+ const res = await client.get(path);
2623
+ if (!res.success || !res.data) {
2624
+ console.error(`Error: ${res.error || "Failed to fetch skills"}`);
2625
+ process.exit(1);
2626
+ return;
2627
+ }
2628
+ if (res.data.length === 0) {
2629
+ console.log("No skills found.");
2630
+ return;
2631
+ }
2632
+ console.log("Public skills:\n");
2633
+ for (const skill of res.data) {
2634
+ const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
2635
+ const tags = skill.tags.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
2636
+ console.log(` ${skill.slug} ${ver}${tags} \u2014 ${skill.description || skill.name}`);
2637
+ }
2638
+ console.log(`
2639
+ ${res.data.length} skill(s) found.`);
2640
+ } else {
2641
+ requireAuth(client);
2642
+ const res = await client.get("/api/skills");
2643
+ if (!res.success || !res.data) {
2644
+ console.error(`Error: ${res.error || "Failed to fetch skills"}`);
2645
+ process.exit(1);
2646
+ return;
2647
+ }
2648
+ if (res.data.length === 0) {
2649
+ console.log("No skills found.");
2650
+ return;
2651
+ }
2652
+ console.log("Available skills:\n");
2653
+ for (const skill of res.data) {
2654
+ const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
2655
+ const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
2656
+ const tags = skill.tags?.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
2657
+ console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${skill.description || skill.name}`);
2658
+ }
2659
+ console.log(`
2515
2660
  ${res.data.length} skill(s) found.`);
2661
+ }
2516
2662
  });
2517
2663
 
2518
2664
  // src/commands/pull.ts
2665
+ import { mkdirSync as mkdirSync8 } from "fs";
2519
2666
  import { Command as Command5 } from "commander";
2520
2667
  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) => {
2521
2668
  const config = loadConfig();
@@ -2535,53 +2682,70 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
2535
2682
  R2.warn(`${slug} \u2014 not found in config, skipping.`);
2536
2683
  continue;
2537
2684
  }
2685
+ const versionQuery = buildVersionQuery(installed.semverRange);
2538
2686
  spinner.start(`Checking ${slug}...`);
2539
2687
  const res = await client.get(
2540
- `/api/skills/${encodeURIComponent(slug)}/content`
2688
+ `/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
2541
2689
  );
2542
2690
  if (!res.success || !res.data) {
2543
2691
  spinner.stop(`${slug} \u2014 failed: ${res.error || "not found"}`);
2544
2692
  continue;
2545
2693
  }
2546
- const { skill, content, version } = res.data;
2694
+ const resData = res.data;
2695
+ const format = resData.format ?? "text";
2696
+ const { skill, version } = resData;
2547
2697
  if (skill.contentHash === installed.hash) {
2548
2698
  spinner.stop(`${slug} \u2014 up to date`);
2549
2699
  skipped++;
2550
2700
  continue;
2551
2701
  }
2552
- store(slug, content, skill, version);
2553
- for (const installation of installed.installations) {
2554
- if (installation.method === "symlink") {
2555
- getPlatformFile(slug, installation.platform, skill);
2556
- continue;
2702
+ if (format === "package") {
2703
+ const { downloadUrl, manifest } = resData;
2704
+ const zipBuffer = await client.fetchBinary(downloadUrl);
2705
+ storePackage(slug, zipBuffer, manifest, skill, version);
2706
+ for (const installation of installed.installations) {
2707
+ const adapter = getAdapter(installation.platform);
2708
+ const targetPath = adapter.resolvePath(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
2709
+ mkdirSync8(targetPath, { recursive: true });
2710
+ extractPackage(zipBuffer, targetPath);
2557
2711
  }
2558
- const adapter = getAdapter(installation.platform);
2559
- const transformed = adapter.transformContent(content, skill);
2560
- if (installation.method === "section") {
2561
- upsertSection(
2562
- installation.path,
2563
- slug,
2564
- `## ${slug}
2712
+ } else {
2713
+ const { content } = resData;
2714
+ store(slug, content, skill, version);
2715
+ for (const installation of installed.installations) {
2716
+ if (installation.method === "symlink") {
2717
+ getPlatformFile(slug, installation.platform, skill);
2718
+ continue;
2719
+ }
2720
+ const adapter = getAdapter(installation.platform);
2721
+ const transformed = adapter.transformContent(content, skill);
2722
+ if (installation.method === "section") {
2723
+ upsertSection(
2724
+ installation.path,
2725
+ slug,
2726
+ `## ${slug}
2565
2727
 
2566
2728
  ${transformed}`
2567
- );
2568
- } else {
2569
- const cachePath = getPlatformFile(slug, installation.platform, skill);
2570
- adapter.install({
2571
- slug,
2572
- content: transformed,
2573
- scope: installation.scope,
2574
- method: "copy",
2575
- cachePath,
2576
- projectDir: installation.projectDir
2577
- });
2729
+ );
2730
+ } else {
2731
+ const cachePath = getPlatformFile(slug, installation.platform, skill);
2732
+ adapter.install({
2733
+ slug,
2734
+ content: transformed,
2735
+ scope: installation.scope,
2736
+ method: "copy",
2737
+ cachePath,
2738
+ projectDir: installation.projectDir
2739
+ });
2740
+ }
2578
2741
  }
2579
2742
  }
2580
2743
  installed.hash = skill.contentHash;
2581
2744
  installed.version = version;
2745
+ installed.semver = resData.semver ?? null;
2582
2746
  installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
2583
2747
  updated++;
2584
- spinner.stop(`${slug} \u2014 updated to v${version}`);
2748
+ spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version)}`);
2585
2749
  }
2586
2750
  saveConfig(config);
2587
2751
  Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
@@ -2589,44 +2753,44 @@ ${transformed}`
2589
2753
 
2590
2754
  // src/commands/publish.ts
2591
2755
  import { Command as Command6 } from "commander";
2592
- import { readFileSync as readFileSync6, existsSync as existsSync14 } from "fs";
2593
- import { resolve as resolve3, basename as basename2, extname as extname2 } from "path";
2756
+ import { readFileSync as readFileSync6, existsSync as existsSync15 } from "fs";
2757
+ import { resolve as resolve4, basename as basename2, extname as extname2 } from "path";
2594
2758
  import { homedir as homedir10 } from "os";
2595
2759
 
2596
2760
  // src/lib/scanner.ts
2597
- import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
2598
- import { join as join12, basename, extname } from "path";
2761
+ import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
2762
+ import { join as join14, basename, extname } from "path";
2599
2763
  import { homedir as homedir9 } from "os";
2600
- import { readlinkSync as readlinkSync2, lstatSync as lstatSync2 } from "fs";
2764
+ import { readlinkSync, lstatSync as lstatSync2 } from "fs";
2601
2765
  function scanForSkills(projectDir) {
2602
2766
  const home = homedir9();
2603
2767
  const cwd = projectDir || process.cwd();
2604
2768
  const results = [];
2605
- scanDirectory(join12(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
2606
- scanDirectory(join12(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
2607
- scanClaudeSkills(join12(home, ".claude", "skills"), "global", results);
2608
- scanClaudeSkills(join12(cwd, ".claude", "skills"), "project", results);
2609
- scanDirectory(join12(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
2610
- scanDirectory(join12(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
2611
- scanSingleFile(join12(home, ".codex", "AGENTS.md"), "codex", "global", results);
2612
- scanSingleFile(join12(cwd, "AGENTS.md"), "codex", "project", results);
2769
+ scanDirectory(join14(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
2770
+ scanDirectory(join14(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
2771
+ scanClaudeSkills(join14(home, ".claude", "skills"), "global", results);
2772
+ scanClaudeSkills(join14(cwd, ".claude", "skills"), "project", results);
2773
+ scanDirectory(join14(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
2774
+ scanDirectory(join14(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
2775
+ scanSingleFile(join14(home, ".codex", "AGENTS.md"), "codex", "global", results);
2776
+ scanSingleFile(join14(cwd, "AGENTS.md"), "codex", "project", results);
2613
2777
  scanSingleFile(
2614
- join12(home, ".codeium", "windsurf", "memories", "global_rules.md"),
2778
+ join14(home, ".codeium", "windsurf", "memories", "global_rules.md"),
2615
2779
  "windsurf",
2616
2780
  "global",
2617
2781
  results
2618
2782
  );
2619
- scanDirectory(join12(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
2620
- scanDirectory(join12(cwd, ".clinerules"), ".md", "cline", "project", results);
2783
+ scanDirectory(join14(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
2784
+ scanDirectory(join14(cwd, ".clinerules"), ".md", "cline", "project", results);
2621
2785
  scanSingleFile(
2622
- join12(cwd, ".github", "copilot-instructions.md"),
2786
+ join14(cwd, ".github", "copilot-instructions.md"),
2623
2787
  "copilot",
2624
2788
  "project",
2625
2789
  results
2626
2790
  );
2627
- scanDirectory(join12(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
2628
- scanDirectory(join12(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
2629
- scanDirectory(join12(cwd, ".aider", "skills"), ".md", "aider", "project", results);
2791
+ scanDirectory(join14(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
2792
+ scanDirectory(join14(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
2793
+ scanDirectory(join14(cwd, ".aider", "skills"), ".md", "aider", "project", results);
2630
2794
  return results;
2631
2795
  }
2632
2796
  function filterTracked(detected, config) {
@@ -2636,13 +2800,13 @@ function filterTracked(detected, config) {
2636
2800
  trackedPaths.add(inst.path);
2637
2801
  }
2638
2802
  }
2639
- const cacheDir = join12(homedir9(), ".localskills", "cache");
2803
+ const cacheDir = join14(homedir9(), ".localskills", "cache");
2640
2804
  return detected.filter((skill) => {
2641
2805
  if (trackedPaths.has(skill.filePath)) return false;
2642
2806
  try {
2643
2807
  const stat = lstatSync2(skill.filePath);
2644
2808
  if (stat.isSymbolicLink()) {
2645
- const target = readlinkSync2(skill.filePath);
2809
+ const target = readlinkSync(skill.filePath);
2646
2810
  if (target.startsWith(cacheDir)) return false;
2647
2811
  }
2648
2812
  } catch {
@@ -2653,11 +2817,9 @@ function filterTracked(detected, config) {
2653
2817
  function slugFromFilename(filename) {
2654
2818
  return basename(filename, extname(filename));
2655
2819
  }
2656
- function nameFromSlug(slug) {
2657
- return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2658
- }
2820
+ var nameFromSlug = titleFromSlug;
2659
2821
  function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
2660
- if (!existsSync13(dir)) return;
2822
+ if (!existsSync14(dir)) return;
2661
2823
  let entries;
2662
2824
  try {
2663
2825
  entries = readdirSync2(dir);
@@ -2666,7 +2828,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
2666
2828
  }
2667
2829
  for (const entry of entries) {
2668
2830
  if (!entry.endsWith(ext)) continue;
2669
- const filePath = join12(dir, entry);
2831
+ const filePath = join14(dir, entry);
2670
2832
  try {
2671
2833
  const raw = readFileSync5(filePath, "utf-8");
2672
2834
  const content = stripFrontmatter(raw).trim();
@@ -2686,7 +2848,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
2686
2848
  }
2687
2849
  }
2688
2850
  function scanClaudeSkills(skillsDir, scope, results) {
2689
- if (!existsSync13(skillsDir)) return;
2851
+ if (!existsSync14(skillsDir)) return;
2690
2852
  let entries;
2691
2853
  try {
2692
2854
  entries = readdirSync2(skillsDir);
@@ -2694,8 +2856,8 @@ function scanClaudeSkills(skillsDir, scope, results) {
2694
2856
  return;
2695
2857
  }
2696
2858
  for (const entry of entries) {
2697
- const skillFile = join12(skillsDir, entry, "SKILL.md");
2698
- if (!existsSync13(skillFile)) continue;
2859
+ const skillFile = join14(skillsDir, entry, "SKILL.md");
2860
+ if (!existsSync14(skillFile)) continue;
2699
2861
  try {
2700
2862
  const raw = readFileSync5(skillFile, "utf-8");
2701
2863
  const content = stripFrontmatter(raw).trim();
@@ -2714,7 +2876,7 @@ function scanClaudeSkills(skillsDir, scope, results) {
2714
2876
  }
2715
2877
  }
2716
2878
  function scanSingleFile(filePath, platform, scope, results) {
2717
- if (!existsSync13(filePath)) return;
2879
+ if (!existsSync14(filePath)) return;
2718
2880
  let raw;
2719
2881
  try {
2720
2882
  raw = readFileSync5(filePath, "utf-8");
@@ -2765,10 +2927,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2765
2927
  ).option("--type <type>", "Content type: skill or rule", "skill").option("-m, --message <message>", "Version message").action(
2766
2928
  async (fileArg, opts) => {
2767
2929
  const client = new ApiClient();
2768
- if (!client.isAuthenticated()) {
2769
- console.error("Not authenticated. Run `localskills login` first.");
2770
- process.exit(1);
2771
- }
2930
+ requireAuth(client);
2772
2931
  const teamsRes = await client.get("/api/tenants");
2773
2932
  if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
2774
2933
  console.error(
@@ -2779,8 +2938,8 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2779
2938
  }
2780
2939
  const teams = teamsRes.data;
2781
2940
  if (fileArg) {
2782
- const filePath = resolve3(fileArg);
2783
- if (!existsSync14(filePath)) {
2941
+ const filePath = resolve4(fileArg);
2942
+ if (!existsSync15(filePath)) {
2784
2943
  console.error(`File not found: ${filePath}`);
2785
2944
  process.exit(1);
2786
2945
  return;
@@ -2793,7 +2952,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2793
2952
  return;
2794
2953
  }
2795
2954
  const defaultSlug = basename2(filePath, extname2(filePath));
2796
- const defaultName = defaultSlug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2955
+ const defaultName = titleFromSlug(defaultSlug);
2797
2956
  const skillName = opts.name || defaultName;
2798
2957
  const contentType = validateContentType(opts.type || "skill");
2799
2958
  const visibility = validateVisibility(opts.visibility || "private");
@@ -2819,7 +2978,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2819
2978
  Le("Nothing to publish.");
2820
2979
  return;
2821
2980
  }
2822
- const selected = await je({
2981
+ const skills = cancelGuard(await je({
2823
2982
  message: "Select items to publish",
2824
2983
  options: detected.map((s) => ({
2825
2984
  value: s,
@@ -2827,28 +2986,19 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2827
2986
  hint: `${s.platform}/${s.scope}/${s.contentType} ${shortenPath(s.filePath)}`
2828
2987
  })),
2829
2988
  required: true
2830
- });
2831
- if (Ct(selected)) {
2832
- Ne("Cancelled.");
2833
- process.exit(0);
2834
- }
2835
- const skills = selected;
2989
+ }));
2836
2990
  const tenantId = await resolveTeam(teams, opts.team);
2837
2991
  for (const skill of skills) {
2838
2992
  R2.step(`Publishing ${skill.suggestedName}...`);
2839
- const name = await Ze({
2993
+ const name = cancelGuard(await Ze({
2840
2994
  message: "Skill name?",
2841
2995
  initialValue: skill.suggestedName,
2842
2996
  validate: (v) => {
2843
2997
  if (!v || v.length < 1) return "Name is required";
2844
2998
  if (v.length > 100) return "Name must be 100 characters or less";
2845
2999
  }
2846
- });
2847
- if (Ct(name)) {
2848
- Ne("Cancelled.");
2849
- process.exit(0);
2850
- }
2851
- const visibility = await Je({
3000
+ }));
3001
+ const visibility = cancelGuard(await Je({
2852
3002
  message: "Visibility?",
2853
3003
  options: [
2854
3004
  { value: "private", label: "Private", hint: "Only team members" },
@@ -2856,23 +3006,15 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2856
3006
  { value: "unlisted", label: "Unlisted", hint: "Accessible via direct link" }
2857
3007
  ],
2858
3008
  initialValue: "private"
2859
- });
2860
- if (Ct(visibility)) {
2861
- Ne("Cancelled.");
2862
- process.exit(0);
2863
- }
2864
- const contentType = await Je({
3009
+ }));
3010
+ const contentType = cancelGuard(await Je({
2865
3011
  message: "Type?",
2866
3012
  options: [
2867
3013
  { value: "skill", label: "Skill", hint: "Reusable agent instructions" },
2868
3014
  { value: "rule", label: "Rule", hint: "Governance constraints" }
2869
3015
  ],
2870
3016
  initialValue: skill.contentType
2871
- });
2872
- if (Ct(contentType)) {
2873
- Ne("Cancelled.");
2874
- process.exit(0);
2875
- }
3017
+ }));
2876
3018
  await uploadSkill(client, {
2877
3019
  name,
2878
3020
  content: skill.content,
@@ -2897,19 +3039,14 @@ async function resolveTeam(teams, teamFlag) {
2897
3039
  if (teams.length === 1) {
2898
3040
  return teams[0].id;
2899
3041
  }
2900
- const selected = await Je({
3042
+ return cancelGuard(await Je({
2901
3043
  message: "Which team?",
2902
3044
  options: teams.map((t) => ({
2903
3045
  value: t.id,
2904
3046
  label: t.name,
2905
3047
  hint: t.slug
2906
3048
  }))
2907
- });
2908
- if (Ct(selected)) {
2909
- Ne("Cancelled.");
2910
- process.exit(0);
2911
- }
2912
- return selected;
3049
+ }));
2913
3050
  }
2914
3051
  async function uploadSkill(client, params) {
2915
3052
  const spinner = bt2();
@@ -2954,8 +3091,200 @@ function shortenPath(filePath) {
2954
3091
  return filePath;
2955
3092
  }
2956
3093
 
3094
+ // src/commands/push.ts
3095
+ import { Command as Command7 } from "commander";
3096
+ import { readFileSync as readFileSync7, existsSync as existsSync16 } from "fs";
3097
+ import { resolve as resolve5 } from "path";
3098
+ 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(
3099
+ async (fileArg, opts) => {
3100
+ const client = new ApiClient();
3101
+ requireAuth(client);
3102
+ const filePath = resolve5(fileArg);
3103
+ if (!existsSync16(filePath)) {
3104
+ console.error(`File not found: ${filePath}`);
3105
+ process.exit(1);
3106
+ return;
3107
+ }
3108
+ const raw = readFileSync7(filePath, "utf-8");
3109
+ const content = stripFrontmatter(raw).trim();
3110
+ if (!content) {
3111
+ console.error("File is empty after stripping frontmatter.");
3112
+ process.exit(1);
3113
+ return;
3114
+ }
3115
+ const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
3116
+ if (opts.version && bumpFlags.length > 0) {
3117
+ console.error("Cannot specify both --version and --patch/--minor/--major");
3118
+ process.exit(1);
3119
+ return;
3120
+ }
3121
+ let body = {
3122
+ content,
3123
+ message: opts.message
3124
+ };
3125
+ if (opts.version) {
3126
+ if (!isValidSemVer(opts.version)) {
3127
+ console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
3128
+ process.exit(1);
3129
+ return;
3130
+ }
3131
+ body.semver = opts.version;
3132
+ } else if (opts.major) {
3133
+ body.bump = "major";
3134
+ } else if (opts.minor) {
3135
+ body.bump = "minor";
3136
+ } else if (opts.patch) {
3137
+ body.bump = "patch";
3138
+ }
3139
+ const spinner = bt2();
3140
+ spinner.start("Pushing new version...");
3141
+ const res = await client.post(
3142
+ `/api/skills/${encodeURIComponent(opts.skill)}/versions`,
3143
+ body
3144
+ );
3145
+ if (!res.success || !res.data) {
3146
+ spinner.stop(`Failed: ${res.error || "Unknown error"}`);
3147
+ process.exit(1);
3148
+ return;
3149
+ }
3150
+ const v = res.data;
3151
+ spinner.stop(`Pushed ${formatVersionLabel(v.semver, v.version)}`);
3152
+ Le("Done!");
3153
+ }
3154
+ );
3155
+
3156
+ // src/commands/share.ts
3157
+ import { Command as Command8 } from "commander";
3158
+ import { readFileSync as readFileSync8, existsSync as existsSync17 } from "fs";
3159
+ import { resolve as resolve6, basename as basename3, extname as extname3 } from "path";
3160
+ import { generateKeyPairSync } from "crypto";
3161
+ 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) => {
3162
+ We("localskills share");
3163
+ await ensureAnonymousIdentity();
3164
+ const client = new ApiClient();
3165
+ if (fileArg) {
3166
+ const filePath = resolve6(fileArg);
3167
+ if (!existsSync17(filePath)) {
3168
+ R2.error(`File not found: ${filePath}`);
3169
+ process.exit(1);
3170
+ }
3171
+ const raw = readFileSync8(filePath, "utf-8");
3172
+ const content = stripFrontmatter(raw).trim();
3173
+ if (!content) {
3174
+ R2.error("File is empty after stripping frontmatter.");
3175
+ process.exit(1);
3176
+ }
3177
+ const defaultSlug = basename3(filePath, extname3(filePath));
3178
+ const defaultName = titleFromSlug(defaultSlug);
3179
+ const skillName = opts.name || defaultName;
3180
+ const contentType = opts.type === "rule" ? "rule" : "skill";
3181
+ await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
3182
+ } else {
3183
+ const spinner = bt2();
3184
+ spinner.start("Scanning for skills...");
3185
+ const config = loadConfig();
3186
+ const allDetected = scanForSkills();
3187
+ const detected = filterTracked(allDetected, config);
3188
+ spinner.stop(
3189
+ detected.length > 0 ? `Found ${detected.length} skill file${detected.length !== 1 ? "s" : ""}.` : "No skill files found."
3190
+ );
3191
+ if (detected.length === 0) {
3192
+ Le("Nothing to share. Pass a file path: localskills share <file>");
3193
+ return;
3194
+ }
3195
+ const selected = cancelGuard(
3196
+ await Je({
3197
+ message: "Select a skill to share",
3198
+ options: detected.map((s) => ({
3199
+ value: s,
3200
+ label: s.suggestedName,
3201
+ hint: `${s.platform} \xB7 ${s.contentType}`
3202
+ }))
3203
+ })
3204
+ );
3205
+ const name = cancelGuard(
3206
+ await Ze({
3207
+ message: "Skill name?",
3208
+ initialValue: selected.suggestedName,
3209
+ validate: (v) => {
3210
+ if (!v || v.length < 1) return "Name is required";
3211
+ if (v.length > 100) return "Name must be 100 characters or less";
3212
+ }
3213
+ })
3214
+ );
3215
+ await uploadAnonymousSkill(client, {
3216
+ name,
3217
+ content: selected.content,
3218
+ type: selected.contentType
3219
+ });
3220
+ }
3221
+ Le("Done!");
3222
+ });
3223
+ async function ensureAnonymousIdentity() {
3224
+ const config = loadConfig();
3225
+ if (config.token) {
3226
+ const client = new ApiClient();
3227
+ const res2 = await client.get("/api/cli/auth");
3228
+ if (res2.success) return;
3229
+ }
3230
+ let keyPair = getAnonymousKey();
3231
+ if (!keyPair) {
3232
+ const s2 = bt2();
3233
+ s2.start("Generating anonymous identity...");
3234
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
3235
+ publicKeyEncoding: { type: "spki", format: "der" },
3236
+ privateKeyEncoding: { type: "pkcs8", format: "der" }
3237
+ });
3238
+ const rawPubKey = publicKey.subarray(publicKey.length - 32);
3239
+ const rawPrivKey = privateKey.subarray(privateKey.length - 32);
3240
+ keyPair = {
3241
+ publicKey: rawPubKey.toString("base64"),
3242
+ privateKey: rawPrivKey.toString("base64")
3243
+ };
3244
+ setAnonymousKey(keyPair);
3245
+ s2.stop("Identity created.");
3246
+ }
3247
+ const s = bt2();
3248
+ s.start("Connecting to localskills.sh...");
3249
+ const tempClient = new ApiClient();
3250
+ const res = await tempClient.post("/api/cli/auth/anonymous", {
3251
+ publicKey: keyPair.publicKey,
3252
+ algorithm: "Ed25519"
3253
+ });
3254
+ if (!res.success || !res.data) {
3255
+ s.stop(`Registration failed: ${res.error || "Unknown error"}`);
3256
+ process.exit(1);
3257
+ }
3258
+ setToken(res.data.token);
3259
+ s.stop(`Connected as ${res.data.username}`);
3260
+ }
3261
+ async function uploadAnonymousSkill(client, params) {
3262
+ const s = bt2();
3263
+ s.start(`Sharing "${params.name}"...`);
3264
+ const teamsRes = await client.get("/api/tenants");
3265
+ if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
3266
+ s.stop("Failed to find your team. Try running `localskills share` again.");
3267
+ process.exit(1);
3268
+ }
3269
+ const tenantId = teamsRes.data[0].id;
3270
+ const res = await client.post("/api/skills", {
3271
+ name: params.name,
3272
+ content: params.content,
3273
+ tenantId,
3274
+ visibility: "unlisted",
3275
+ type: params.type
3276
+ });
3277
+ if (!res.success || !res.data) {
3278
+ s.stop(`Failed: ${res.error || "Unknown error"}`);
3279
+ return;
3280
+ }
3281
+ s.stop("Shared!");
3282
+ R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
3283
+ R2.info(`Install: localskills install ${res.data.publicId}`);
3284
+ }
3285
+
2957
3286
  // src/index.ts
2958
- var program = new Command7();
3287
+ var program = new Command9();
2959
3288
  program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0");
2960
3289
  program.addCommand(loginCommand);
2961
3290
  program.addCommand(logoutCommand);
@@ -2965,4 +3294,6 @@ program.addCommand(uninstallCommand);
2965
3294
  program.addCommand(listCommand);
2966
3295
  program.addCommand(pullCommand);
2967
3296
  program.addCommand(publishCommand);
3297
+ program.addCommand(pushCommand);
3298
+ program.addCommand(shareCommand);
2968
3299
  program.parse();