@localskills/cli 0.2.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 +769 -420
  2. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -161,12 +161,12 @@ 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";
168
168
  import { randomBytes, createHash } from "crypto";
169
- import { execSync } from "child_process";
169
+ import { spawn } from "child_process";
170
170
 
171
171
  // ../../node_modules/.pnpm/@clack+core@1.0.1/node_modules/@clack/core/dist/index.mjs
172
172
  var import_picocolors = __toESM(require_picocolors(), 1);
@@ -1101,7 +1101,7 @@ ${l}
1101
1101
  } }).prompt();
1102
1102
 
1103
1103
  // src/lib/config.ts
1104
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1104
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
1105
1105
  import { join } from "path";
1106
1106
  import { homedir } from "os";
1107
1107
  var CONFIG_DIR = join(homedir(), ".localskills");
@@ -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)) {
@@ -1131,8 +1132,15 @@ function loadConfig() {
1131
1132
  }
1132
1133
  }
1133
1134
  function saveConfig(config) {
1134
- mkdirSync(CONFIG_DIR, { recursive: true });
1135
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
1135
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1136
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
1137
+ mode: 384
1138
+ });
1139
+ try {
1140
+ chmodSync(CONFIG_DIR, 448);
1141
+ chmodSync(CONFIG_PATH, 384);
1142
+ } catch {
1143
+ }
1136
1144
  }
1137
1145
  function migrateV1toV2(v1) {
1138
1146
  const v2 = {
@@ -1152,6 +1160,8 @@ function migrateV1toV2(v1) {
1152
1160
  name: skill.slug,
1153
1161
  hash: skill.hash,
1154
1162
  version: 0,
1163
+ semver: null,
1164
+ semverRange: null,
1155
1165
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
1156
1166
  installations: [
1157
1167
  {
@@ -1180,6 +1190,14 @@ function clearToken() {
1180
1190
  config.token = null;
1181
1191
  saveConfig(config);
1182
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
+ }
1183
1201
 
1184
1202
  // src/lib/api-client.ts
1185
1203
  var ApiClient = class {
@@ -1222,25 +1240,16 @@ var ApiClient = class {
1222
1240
  });
1223
1241
  return this.handleResponse(res);
1224
1242
  }
1225
- async put(path, body) {
1226
- const res = await fetch(`${this.baseUrl}${path}`, {
1227
- method: "PUT",
1228
- headers: this.headers(),
1229
- body: JSON.stringify(body)
1230
- });
1231
- return this.handleResponse(res);
1232
- }
1233
- async delete(path) {
1234
- const res = await fetch(`${this.baseUrl}${path}`, {
1235
- method: "DELETE",
1236
- headers: this.headers()
1237
- });
1238
- return this.handleResponse(res);
1239
- }
1240
- async getRaw(path) {
1241
- return fetch(`${this.baseUrl}${path}`, {
1242
- headers: this.headers()
1243
- });
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());
1244
1253
  }
1245
1254
  isAuthenticated() {
1246
1255
  return this.token !== null;
@@ -1256,20 +1265,51 @@ function generateUserCode(length = 8) {
1256
1265
  function openBrowser(url) {
1257
1266
  try {
1258
1267
  const platform = process.platform;
1268
+ let command = "";
1269
+ let args = [];
1259
1270
  if (platform === "darwin") {
1260
- execSync(`open "${url}"`, { stdio: "ignore" });
1271
+ command = "open";
1272
+ args = [url];
1261
1273
  } else if (platform === "win32") {
1262
- execSync(`start "" "${url}"`, { stdio: "ignore" });
1274
+ command = "rundll32";
1275
+ args = ["url.dll,FileProtocolHandler", url];
1263
1276
  } else {
1264
- execSync(`xdg-open "${url}"`, { stdio: "ignore" });
1277
+ command = "xdg-open";
1278
+ args = [url];
1265
1279
  }
1280
+ const child = spawn(command, args, {
1281
+ stdio: "ignore",
1282
+ detached: true,
1283
+ shell: false
1284
+ });
1285
+ child.unref();
1266
1286
  } catch {
1267
1287
  }
1268
1288
  }
1269
1289
  function sleep(ms) {
1270
- return new Promise((resolve4) => setTimeout(resolve4, ms));
1290
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1271
1291
  }
1272
- 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
+ }
1273
1313
  if (opts.token) {
1274
1314
  setToken(opts.token);
1275
1315
  const client2 = new ApiClient();
@@ -1364,22 +1404,75 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
1364
1404
 
1365
1405
  // src/commands/install.ts
1366
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
+ }
1367
1460
 
1368
1461
  // src/lib/cache.ts
1369
1462
  import {
1370
- existsSync as existsSync11,
1371
- mkdirSync as mkdirSync10,
1463
+ existsSync as existsSync13,
1464
+ mkdirSync as mkdirSync5,
1372
1465
  readFileSync as readFileSync4,
1373
1466
  readdirSync,
1374
- writeFileSync as writeFileSync9,
1375
- rmSync as rmSync4
1467
+ writeFileSync as writeFileSync5,
1468
+ rmSync as rmSync3
1376
1469
  } from "fs";
1377
- import { join as join10, resolve as resolve2 } from "path";
1378
- import { homedir as homedir7 } from "os";
1470
+ import { join as join12, resolve as resolve2 } from "path";
1471
+ import { homedir as homedir8 } from "os";
1379
1472
 
1380
1473
  // src/lib/installers/cursor.ts
1381
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1382
- import { join as join2 } from "path";
1474
+ import { existsSync as existsSync4 } from "fs";
1475
+ import { join as join3 } from "path";
1383
1476
  import { homedir as homedir2 } from "os";
1384
1477
 
1385
1478
  // src/lib/content-transform.ts
@@ -1419,10 +1512,13 @@ function stripFrontmatter(content) {
1419
1512
  return content;
1420
1513
  }
1421
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
+
1422
1519
  // src/lib/symlink.ts
1423
1520
  import {
1424
1521
  symlinkSync,
1425
- readlinkSync,
1426
1522
  unlinkSync,
1427
1523
  lstatSync,
1428
1524
  existsSync as existsSync2,
@@ -1459,6 +1555,31 @@ function isSymlink(path) {
1459
1555
  }
1460
1556
  }
1461
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
+
1462
1583
  // src/lib/installers/cursor.ts
1463
1584
  var descriptor = {
1464
1585
  id: "cursor",
@@ -1472,40 +1593,30 @@ function detect(projectDir) {
1472
1593
  const home = homedir2();
1473
1594
  const cwd = projectDir || process.cwd();
1474
1595
  return {
1475
- global: existsSync3(join2(home, ".cursor")),
1476
- project: existsSync3(join2(cwd, ".cursor"))
1596
+ global: existsSync4(join3(home, ".cursor")),
1597
+ project: existsSync4(join3(cwd, ".cursor"))
1477
1598
  };
1478
1599
  }
1479
1600
  function resolvePath(slug, scope, projectDir, _contentType) {
1480
- const safeName = slug.replace(/\//g, "-");
1601
+ const safeName = safeSlugName(slug);
1481
1602
  if (scope === "global") {
1482
- return join2(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
1603
+ return join3(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
1483
1604
  }
1484
1605
  const dir = projectDir || process.cwd();
1485
- return join2(dir, ".cursor", "rules", `${safeName}.mdc`);
1606
+ return join3(dir, ".cursor", "rules", `${safeName}.mdc`);
1486
1607
  }
1487
1608
  function transformContent(content, skill) {
1488
1609
  return toCursorMDC(content, skill);
1489
1610
  }
1490
1611
  function install(opts) {
1491
1612
  const targetPath = resolvePath(opts.slug, opts.scope, opts.projectDir);
1492
- if (opts.method === "symlink") {
1493
- createSymlink(opts.cachePath, targetPath);
1494
- } else {
1495
- mkdirSync3(join2(targetPath, ".."), { recursive: true });
1496
- writeFileSync2(targetPath, opts.content);
1497
- }
1498
- return targetPath;
1613
+ return installFileOrSymlink(opts, targetPath);
1499
1614
  }
1500
1615
  function uninstall(installation, _slug) {
1501
- if (installation.method === "symlink") {
1502
- removeSymlink(installation.path);
1503
- } else if (existsSync3(installation.path)) {
1504
- unlinkSync2(installation.path);
1505
- }
1616
+ uninstallFile(installation);
1506
1617
  }
1507
1618
  function defaultMethod() {
1508
- return "symlink";
1619
+ return DEFAULT_METHOD;
1509
1620
  }
1510
1621
  var cursorAdapter = {
1511
1622
  descriptor,
@@ -1518,8 +1629,8 @@ var cursorAdapter = {
1518
1629
  };
1519
1630
 
1520
1631
  // src/lib/installers/claude.ts
1521
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, rmSync as rmSync3 } from "fs";
1522
- import { join as join3 } from "path";
1632
+ import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
1633
+ import { join as join4 } from "path";
1523
1634
  import { homedir as homedir3 } from "os";
1524
1635
  var descriptor2 = {
1525
1636
  id: "claude",
@@ -1533,17 +1644,17 @@ function detect2(projectDir) {
1533
1644
  const home = homedir3();
1534
1645
  const cwd = projectDir || process.cwd();
1535
1646
  return {
1536
- global: existsSync4(join3(home, ".claude")),
1537
- project: existsSync4(join3(cwd, ".claude"))
1647
+ global: existsSync5(join4(home, ".claude")),
1648
+ project: existsSync5(join4(cwd, ".claude"))
1538
1649
  };
1539
1650
  }
1540
1651
  function resolvePath2(slug, scope, projectDir, contentType) {
1541
- const safeName = slug.replace(/\//g, "-");
1542
- 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");
1543
1654
  if (contentType === "rule") {
1544
- return join3(base, "rules", `${safeName}.md`);
1655
+ return join4(base, "rules", `${safeName}.md`);
1545
1656
  }
1546
- return join3(base, "skills", safeName, "SKILL.md");
1657
+ return join4(base, "skills", safeName, "SKILL.md");
1547
1658
  }
1548
1659
  function transformContent2(content, skill) {
1549
1660
  if (skill.type === "rule") {
@@ -1553,32 +1664,21 @@ function transformContent2(content, skill) {
1553
1664
  }
1554
1665
  function install2(opts) {
1555
1666
  const targetPath = resolvePath2(opts.slug, opts.scope, opts.projectDir, opts.contentType);
1556
- const targetDir = join3(targetPath, "..");
1557
- if (opts.method === "symlink") {
1558
- createSymlink(opts.cachePath, targetPath);
1559
- } else {
1560
- mkdirSync4(targetDir, { recursive: true });
1561
- writeFileSync3(targetPath, opts.content);
1562
- }
1563
- return targetPath;
1667
+ return installFileOrSymlink(opts, targetPath);
1564
1668
  }
1565
1669
  function uninstall2(installation, _slug) {
1566
- if (installation.method === "symlink") {
1567
- removeSymlink(installation.path);
1568
- } else if (existsSync4(installation.path)) {
1569
- unlinkSync3(installation.path);
1570
- }
1571
- const parentDir = join3(installation.path, "..");
1670
+ uninstallFile(installation);
1671
+ const parentDir = join4(installation.path, "..");
1572
1672
  try {
1573
1673
  const { readdirSync: readdirSync3 } = __require("fs");
1574
- if (existsSync4(parentDir) && readdirSync3(parentDir).length === 0) {
1575
- rmSync3(parentDir, { recursive: true });
1674
+ if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
1675
+ rmSync2(parentDir, { recursive: true });
1576
1676
  }
1577
1677
  } catch {
1578
1678
  }
1579
1679
  }
1580
1680
  function defaultMethod2() {
1581
- return "symlink";
1681
+ return DEFAULT_METHOD;
1582
1682
  }
1583
1683
  var claudeAdapter = {
1584
1684
  descriptor: descriptor2,
@@ -1591,19 +1691,50 @@ var claudeAdapter = {
1591
1691
  };
1592
1692
 
1593
1693
  // src/lib/installers/codex.ts
1594
- 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";
1595
1700
  import { homedir as homedir4 } from "os";
1596
- import { execSync as execSync2 } 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
+ }
1597
1728
 
1598
1729
  // src/lib/marked-sections.ts
1599
- 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";
1600
1731
  import { dirname as dirname2 } from "path";
1601
1732
  var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
1602
1733
  var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
1603
1734
  function upsertSection(filePath, slug, content) {
1604
- mkdirSync5(dirname2(filePath), { recursive: true });
1735
+ mkdirSync4(dirname2(filePath), { recursive: true });
1605
1736
  let existing = "";
1606
- if (existsSync5(filePath)) {
1737
+ if (existsSync7(filePath)) {
1607
1738
  existing = readFileSync2(filePath, "utf-8");
1608
1739
  }
1609
1740
  const start = START_MARKER(slug);
@@ -1620,10 +1751,10 @@ ${end}`;
1620
1751
  const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
1621
1752
  result = existing + separator + section + "\n";
1622
1753
  }
1623
- writeFileSync4(filePath, result);
1754
+ writeFileSync3(filePath, result);
1624
1755
  }
1625
1756
  function removeSection(filePath, slug) {
1626
- if (!existsSync5(filePath)) return false;
1757
+ if (!existsSync7(filePath)) return false;
1627
1758
  const existing = readFileSync2(filePath, "utf-8");
1628
1759
  const start = START_MARKER(slug);
1629
1760
  const end = END_MARKER(slug);
@@ -1635,11 +1766,11 @@ function removeSection(filePath, slug) {
1635
1766
  while (before.endsWith("\n\n")) before = before.slice(0, -1);
1636
1767
  while (after.startsWith("\n\n")) after = after.slice(1);
1637
1768
  const result = (before + after).trim();
1638
- writeFileSync4(filePath, result ? result + "\n" : "");
1769
+ writeFileSync3(filePath, result ? result + "\n" : "");
1639
1770
  return true;
1640
1771
  }
1641
1772
  function listSections(filePath) {
1642
- if (!existsSync5(filePath)) return [];
1773
+ if (!existsSync7(filePath)) return [];
1643
1774
  const content = readFileSync2(filePath, "utf-8");
1644
1775
  const regex = /<!-- localskills:start:(.+?) -->/g;
1645
1776
  const slugs = [];
@@ -1660,22 +1791,17 @@ var descriptor3 = {
1660
1791
  fileExtension: ".md"
1661
1792
  };
1662
1793
  function detect3() {
1663
- let hasCommand = false;
1664
- try {
1665
- execSync2("which codex", { stdio: "ignore" });
1666
- hasCommand = true;
1667
- } catch {
1668
- }
1794
+ const hasCommand = commandExists("codex");
1669
1795
  return { global: hasCommand, project: hasCommand };
1670
1796
  }
1671
1797
  function resolvePath3(slug, scope, projectDir, _contentType) {
1672
1798
  if (scope === "global") {
1673
- return join4(homedir4(), ".codex", "AGENTS.md");
1799
+ return join6(homedir5(), ".codex", "AGENTS.md");
1674
1800
  }
1675
- return join4(projectDir || process.cwd(), "AGENTS.md");
1801
+ return join6(projectDir || process.cwd(), "AGENTS.md");
1676
1802
  }
1677
1803
  function transformContent3(content) {
1678
- return toPlainMD(content);
1804
+ return defaultTransformContent(content);
1679
1805
  }
1680
1806
  function install3(opts) {
1681
1807
  const filePath = resolvePath3(opts.slug, opts.scope, opts.projectDir);
@@ -1701,9 +1827,9 @@ var codexAdapter = {
1701
1827
  };
1702
1828
 
1703
1829
  // src/lib/installers/windsurf.ts
1704
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4 } from "fs";
1705
- import { join as join5 } from "path";
1706
- 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";
1707
1833
  var descriptor4 = {
1708
1834
  id: "windsurf",
1709
1835
  name: "Windsurf",
@@ -1714,22 +1840,22 @@ var descriptor4 = {
1714
1840
  fileExtension: ".md"
1715
1841
  };
1716
1842
  function detect4(projectDir) {
1717
- const home = homedir5();
1843
+ const home = homedir6();
1718
1844
  const cwd = projectDir || process.cwd();
1719
1845
  return {
1720
- global: existsSync6(join5(home, ".codeium")),
1721
- project: existsSync6(join5(cwd, ".windsurf"))
1846
+ global: existsSync8(join7(home, ".codeium")),
1847
+ project: existsSync8(join7(cwd, ".windsurf"))
1722
1848
  };
1723
1849
  }
1724
1850
  function resolvePath4(slug, scope, projectDir, _contentType) {
1725
- const safeName = slug.replace(/\//g, "-");
1851
+ const safeName = safeSlugName(slug);
1726
1852
  if (scope === "global") {
1727
- return join5(homedir5(), ".codeium", "windsurf", "memories", "global_rules.md");
1853
+ return join7(homedir6(), ".codeium", "windsurf", "memories", "global_rules.md");
1728
1854
  }
1729
- return join5(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
1855
+ return join7(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
1730
1856
  }
1731
1857
  function transformContent4(content) {
1732
- return toPlainMD(content);
1858
+ return defaultTransformContent(content);
1733
1859
  }
1734
1860
  function install4(opts) {
1735
1861
  const targetPath = resolvePath4(opts.slug, opts.scope, opts.projectDir);
@@ -1737,21 +1863,16 @@ function install4(opts) {
1737
1863
  upsertSection(targetPath, opts.slug, `## ${opts.slug}
1738
1864
 
1739
1865
  ${opts.content}`);
1740
- } else if (opts.method === "symlink") {
1741
- createSymlink(opts.cachePath, targetPath);
1742
1866
  } else {
1743
- mkdirSync6(join5(targetPath, ".."), { recursive: true });
1744
- writeFileSync5(targetPath, opts.content);
1867
+ installFileOrSymlink(opts, targetPath);
1745
1868
  }
1746
1869
  return targetPath;
1747
1870
  }
1748
1871
  function uninstall4(installation, slug) {
1749
1872
  if (installation.method === "section") {
1750
1873
  removeSection(installation.path, slug);
1751
- } else if (installation.method === "symlink") {
1752
- removeSymlink(installation.path);
1753
- } else if (existsSync6(installation.path)) {
1754
- unlinkSync4(installation.path);
1874
+ } else {
1875
+ uninstallFile(installation);
1755
1876
  }
1756
1877
  }
1757
1878
  function defaultMethod4(scope) {
@@ -1768,8 +1889,8 @@ var windsurfAdapter = {
1768
1889
  };
1769
1890
 
1770
1891
  // src/lib/installers/cline.ts
1771
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync5 } from "fs";
1772
- import { join as join6 } from "path";
1892
+ import { existsSync as existsSync9 } from "fs";
1893
+ import { join as join8 } from "path";
1773
1894
  var descriptor5 = {
1774
1895
  id: "cline",
1775
1896
  name: "Cline",
@@ -1782,38 +1903,28 @@ function detect5(projectDir) {
1782
1903
  const cwd = projectDir || process.cwd();
1783
1904
  return {
1784
1905
  global: false,
1785
- project: existsSync7(join6(cwd, ".clinerules"))
1906
+ project: existsSync9(join8(cwd, ".clinerules"))
1786
1907
  };
1787
1908
  }
1788
1909
  function resolvePath5(slug, scope, projectDir, _contentType) {
1789
1910
  if (scope === "global") {
1790
1911
  throw new Error("Cline does not support global installation");
1791
1912
  }
1792
- const safeName = slug.replace(/\//g, "-");
1793
- return join6(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
1913
+ const safeName = safeSlugName(slug);
1914
+ return join8(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
1794
1915
  }
1795
1916
  function transformContent5(content) {
1796
- return toPlainMD(content);
1917
+ return defaultTransformContent(content);
1797
1918
  }
1798
1919
  function install5(opts) {
1799
1920
  const targetPath = resolvePath5(opts.slug, opts.scope, opts.projectDir);
1800
- if (opts.method === "symlink") {
1801
- createSymlink(opts.cachePath, targetPath);
1802
- } else {
1803
- mkdirSync7(join6(targetPath, ".."), { recursive: true });
1804
- writeFileSync6(targetPath, opts.content);
1805
- }
1806
- return targetPath;
1921
+ return installFileOrSymlink(opts, targetPath);
1807
1922
  }
1808
1923
  function uninstall5(installation, _slug) {
1809
- if (installation.method === "symlink") {
1810
- removeSymlink(installation.path);
1811
- } else if (existsSync7(installation.path)) {
1812
- unlinkSync5(installation.path);
1813
- }
1924
+ uninstallFile(installation);
1814
1925
  }
1815
1926
  function defaultMethod5() {
1816
- return "symlink";
1927
+ return DEFAULT_METHOD;
1817
1928
  }
1818
1929
  var clineAdapter = {
1819
1930
  descriptor: descriptor5,
@@ -1826,8 +1937,8 @@ var clineAdapter = {
1826
1937
  };
1827
1938
 
1828
1939
  // src/lib/installers/copilot.ts
1829
- import { existsSync as existsSync8 } from "fs";
1830
- import { join as join7 } from "path";
1940
+ import { existsSync as existsSync10 } from "fs";
1941
+ import { join as join9 } from "path";
1831
1942
  var descriptor6 = {
1832
1943
  id: "copilot",
1833
1944
  name: "GitHub Copilot",
@@ -1840,17 +1951,17 @@ function detect6(projectDir) {
1840
1951
  const cwd = projectDir || process.cwd();
1841
1952
  return {
1842
1953
  global: false,
1843
- project: existsSync8(join7(cwd, ".github"))
1954
+ project: existsSync10(join9(cwd, ".github"))
1844
1955
  };
1845
1956
  }
1846
1957
  function resolvePath6(slug, scope, projectDir, _contentType) {
1847
1958
  if (scope === "global") {
1848
1959
  throw new Error("GitHub Copilot does not support global installation");
1849
1960
  }
1850
- return join7(projectDir || process.cwd(), ".github", "copilot-instructions.md");
1961
+ return join9(projectDir || process.cwd(), ".github", "copilot-instructions.md");
1851
1962
  }
1852
1963
  function transformContent6(content) {
1853
- return toPlainMD(content);
1964
+ return defaultTransformContent(content);
1854
1965
  }
1855
1966
  function install6(opts) {
1856
1967
  const filePath = resolvePath6(opts.slug, opts.scope, opts.projectDir);
@@ -1876,10 +1987,9 @@ var copilotAdapter = {
1876
1987
  };
1877
1988
 
1878
1989
  // src/lib/installers/opencode.ts
1879
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync6 } from "fs";
1880
- import { join as join8 } from "path";
1881
- import { homedir as homedir6 } from "os";
1882
- import { execSync as execSync3 } from "child_process";
1990
+ import { existsSync as existsSync11 } from "fs";
1991
+ import { join as join10 } from "path";
1992
+ import { homedir as homedir7 } from "os";
1883
1993
  var descriptor7 = {
1884
1994
  id: "opencode",
1885
1995
  name: "OpenCode",
@@ -1890,46 +2000,31 @@ var descriptor7 = {
1890
2000
  };
1891
2001
  function detect7(projectDir) {
1892
2002
  const cwd = projectDir || process.cwd();
1893
- let hasCommand = false;
1894
- try {
1895
- execSync3("which opencode", { stdio: "ignore" });
1896
- hasCommand = true;
1897
- } catch {
1898
- }
2003
+ const hasCommand = commandExists("opencode");
1899
2004
  return {
1900
2005
  global: hasCommand,
1901
- project: hasCommand || existsSync9(join8(cwd, ".opencode"))
2006
+ project: hasCommand || existsSync11(join10(cwd, ".opencode"))
1902
2007
  };
1903
2008
  }
1904
2009
  function resolvePath7(slug, scope, projectDir, _contentType) {
1905
- const safeName = slug.replace(/\//g, "-");
2010
+ const safeName = safeSlugName(slug);
1906
2011
  if (scope === "global") {
1907
- return join8(homedir6(), ".config", "opencode", "rules", `${safeName}.md`);
2012
+ return join10(homedir7(), ".config", "opencode", "rules", `${safeName}.md`);
1908
2013
  }
1909
- return join8(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
2014
+ return join10(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
1910
2015
  }
1911
2016
  function transformContent7(content) {
1912
- return toPlainMD(content);
2017
+ return defaultTransformContent(content);
1913
2018
  }
1914
2019
  function install7(opts) {
1915
2020
  const targetPath = resolvePath7(opts.slug, opts.scope, opts.projectDir);
1916
- if (opts.method === "symlink") {
1917
- createSymlink(opts.cachePath, targetPath);
1918
- } else {
1919
- mkdirSync8(join8(targetPath, ".."), { recursive: true });
1920
- writeFileSync7(targetPath, opts.content);
1921
- }
1922
- return targetPath;
2021
+ return installFileOrSymlink(opts, targetPath);
1923
2022
  }
1924
2023
  function uninstall7(installation, _slug) {
1925
- if (installation.method === "symlink") {
1926
- removeSymlink(installation.path);
1927
- } else if (existsSync9(installation.path)) {
1928
- unlinkSync6(installation.path);
1929
- }
2024
+ uninstallFile(installation);
1930
2025
  }
1931
2026
  function defaultMethod7() {
1932
- return "symlink";
2027
+ return DEFAULT_METHOD;
1933
2028
  }
1934
2029
  var opencodeAdapter = {
1935
2030
  descriptor: descriptor7,
@@ -1942,9 +2037,8 @@ var opencodeAdapter = {
1942
2037
  };
1943
2038
 
1944
2039
  // src/lib/installers/aider.ts
1945
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync7, readFileSync as readFileSync3 } from "fs";
1946
- import { join as join9 } from "path";
1947
- import { execSync as execSync4 } from "child_process";
2040
+ import { existsSync as existsSync12, writeFileSync as writeFileSync4, readFileSync as readFileSync3 } from "fs";
2041
+ import { join as join11 } from "path";
1948
2042
  var descriptor8 = {
1949
2043
  id: "aider",
1950
2044
  name: "Aider",
@@ -1954,29 +2048,24 @@ var descriptor8 = {
1954
2048
  fileExtension: ".md"
1955
2049
  };
1956
2050
  function detect8() {
1957
- let hasCommand = false;
1958
- try {
1959
- execSync4("which aider", { stdio: "ignore" });
1960
- hasCommand = true;
1961
- } catch {
1962
- }
2051
+ const hasCommand = commandExists("aider");
1963
2052
  return { global: false, project: hasCommand };
1964
2053
  }
1965
2054
  function resolvePath8(slug, scope, projectDir, contentType) {
1966
2055
  if (scope === "global") {
1967
2056
  throw new Error("Aider does not support global installation");
1968
2057
  }
1969
- const safeName = slug.replace(/\//g, "-");
2058
+ const safeName = safeSlugName(slug);
1970
2059
  const subdir = contentType === "rule" ? "rules" : "skills";
1971
- return join9(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
2060
+ return join11(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
1972
2061
  }
1973
2062
  function transformContent8(content) {
1974
- return toPlainMD(content);
2063
+ return defaultTransformContent(content);
1975
2064
  }
1976
2065
  function addAiderRead(projectDir, relativePath) {
1977
- const configPath = join9(projectDir, ".aider.conf.yml");
2066
+ const configPath = join11(projectDir, ".aider.conf.yml");
1978
2067
  let content = "";
1979
- if (existsSync10(configPath)) {
2068
+ if (existsSync12(configPath)) {
1980
2069
  content = readFileSync3(configPath, "utf-8");
1981
2070
  }
1982
2071
  if (content.includes(relativePath)) return;
@@ -1986,44 +2075,35 @@ function addAiderRead(projectDir, relativePath) {
1986
2075
  } else {
1987
2076
  content = content.trimEnd() + (content ? "\n" : "") + readLine + "\n";
1988
2077
  }
1989
- writeFileSync8(configPath, content);
2078
+ writeFileSync4(configPath, content);
1990
2079
  }
1991
2080
  function removeAiderRead(projectDir, relativePath) {
1992
- const configPath = join9(projectDir, ".aider.conf.yml");
1993
- if (!existsSync10(configPath)) return;
2081
+ const configPath = join11(projectDir, ".aider.conf.yml");
2082
+ if (!existsSync12(configPath)) return;
1994
2083
  let content = readFileSync3(configPath, "utf-8");
1995
2084
  const lines = content.split("\n");
1996
2085
  const filtered = lines.filter((line) => !line.includes(relativePath));
1997
- writeFileSync8(configPath, filtered.join("\n"));
2086
+ writeFileSync4(configPath, filtered.join("\n"));
1998
2087
  }
1999
2088
  function install8(opts) {
2000
2089
  const targetPath = resolvePath8(opts.slug, opts.scope, opts.projectDir, opts.contentType);
2001
2090
  const projectDir = opts.projectDir || process.cwd();
2002
- if (opts.method === "symlink") {
2003
- createSymlink(opts.cachePath, targetPath);
2004
- } else {
2005
- mkdirSync9(join9(targetPath, ".."), { recursive: true });
2006
- writeFileSync8(targetPath, opts.content);
2007
- }
2008
- const safeName = opts.slug.replace(/\//g, "-");
2091
+ installFileOrSymlink(opts, targetPath);
2092
+ const safeName = safeSlugName(opts.slug);
2009
2093
  const subdir = opts.contentType === "rule" ? "rules" : "skills";
2010
2094
  const relativePath = `.aider/${subdir}/${safeName}.md`;
2011
2095
  addAiderRead(projectDir, relativePath);
2012
2096
  return targetPath;
2013
2097
  }
2014
2098
  function uninstall8(installation, slug) {
2015
- if (installation.method === "symlink") {
2016
- removeSymlink(installation.path);
2017
- } else if (existsSync10(installation.path)) {
2018
- unlinkSync7(installation.path);
2019
- }
2099
+ uninstallFile(installation);
2020
2100
  const projectDir = installation.projectDir || process.cwd();
2021
- const safeName = slug.replace(/\//g, "-");
2101
+ const safeName = safeSlugName(slug);
2022
2102
  removeAiderRead(projectDir, `.aider/skills/${safeName}.md`);
2023
2103
  removeAiderRead(projectDir, `.aider/rules/${safeName}.md`);
2024
2104
  }
2025
2105
  function defaultMethod8() {
2026
- return "symlink";
2106
+ return DEFAULT_METHOD;
2027
2107
  }
2028
2108
  var aiderAdapter = {
2029
2109
  descriptor: descriptor8,
@@ -2056,7 +2136,7 @@ function getAllAdapters() {
2056
2136
  }
2057
2137
 
2058
2138
  // src/lib/cache.ts
2059
- var CACHE_DIR = join10(homedir7(), ".localskills", "cache");
2139
+ var CACHE_DIR = join12(homedir8(), ".localskills", "cache");
2060
2140
  function slugToDir(slug) {
2061
2141
  if (slug.includes("..") || slug.includes("\0")) {
2062
2142
  throw new Error("Invalid slug: contains forbidden characters");
@@ -2064,7 +2144,7 @@ function slugToDir(slug) {
2064
2144
  return slug.replace(/\//g, "--");
2065
2145
  }
2066
2146
  function getCacheDir(slug) {
2067
- const dir = resolve2(join10(CACHE_DIR, slugToDir(slug)));
2147
+ const dir = resolve2(join12(CACHE_DIR, slugToDir(slug)));
2068
2148
  if (!dir.startsWith(resolve2(CACHE_DIR) + "/") && dir !== resolve2(CACHE_DIR)) {
2069
2149
  throw new Error("Invalid slug: path traversal detected");
2070
2150
  }
@@ -2072,17 +2152,18 @@ function getCacheDir(slug) {
2072
2152
  }
2073
2153
  function store(slug, content, skill, version) {
2074
2154
  const dir = getCacheDir(slug);
2075
- mkdirSync10(dir, { recursive: true });
2076
- writeFileSync9(join10(dir, "raw.md"), content);
2155
+ mkdirSync5(dir, { recursive: true });
2156
+ writeFileSync5(join12(dir, "raw.md"), content);
2077
2157
  const meta = {
2078
2158
  hash: skill.contentHash,
2079
2159
  version,
2160
+ semver: skill.currentSemver ?? null,
2080
2161
  name: skill.name,
2081
2162
  description: skill.description,
2082
2163
  type: skill.type ?? "skill",
2083
2164
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2084
2165
  };
2085
- writeFileSync9(join10(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2166
+ writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2086
2167
  clearPlatformFiles(slug);
2087
2168
  }
2088
2169
  function getPlatformFile(slug, platform, skill) {
@@ -2093,98 +2174,103 @@ function getPlatformFile(slug, platform, skill) {
2093
2174
  const transformed = adapter.transformContent(raw, skill);
2094
2175
  if (platform === "claude") {
2095
2176
  if (skill.type === "rule") {
2096
- const claudeRuleDir = join10(dir, "claude-rule");
2097
- mkdirSync10(claudeRuleDir, { recursive: true });
2098
- const filePath3 = join10(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
2099
- 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);
2100
2181
  return filePath3;
2101
2182
  }
2102
- const claudeDir = join10(dir, "claude");
2103
- mkdirSync10(claudeDir, { recursive: true });
2104
- const filePath2 = join10(claudeDir, "SKILL.md");
2105
- 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);
2106
2187
  return filePath2;
2107
2188
  }
2108
2189
  const ext = adapter.descriptor.fileExtension;
2109
- const filePath = join10(dir, `${platform}${ext}`);
2110
- writeFileSync9(filePath, transformed);
2190
+ const filePath = join12(dir, `${platform}${ext}`);
2191
+ writeFileSync5(filePath, transformed);
2111
2192
  return filePath;
2112
2193
  }
2113
2194
  function getRawContent(slug) {
2114
- const filePath = join10(getCacheDir(slug), "raw.md");
2115
- if (!existsSync11(filePath)) return null;
2195
+ const filePath = join12(getCacheDir(slug), "raw.md");
2196
+ if (!existsSync13(filePath)) return null;
2116
2197
  return readFileSync4(filePath, "utf-8");
2117
2198
  }
2118
2199
  function purge(slug) {
2119
2200
  const dir = getCacheDir(slug);
2120
- if (existsSync11(dir)) {
2121
- rmSync4(dir, { recursive: true, force: true });
2201
+ if (existsSync13(dir)) {
2202
+ rmSync3(dir, { recursive: true, force: true });
2122
2203
  }
2123
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
+ }
2124
2222
  function clearPlatformFiles(slug) {
2125
2223
  const dir = getCacheDir(slug);
2126
- if (!existsSync11(dir)) return;
2127
- 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"]);
2128
2226
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2129
2227
  if (!keep.has(entry.name)) {
2130
- rmSync4(join10(dir, entry.name), { recursive: true, force: true });
2228
+ rmSync3(join12(dir, entry.name), { recursive: true, force: true });
2131
2229
  }
2132
2230
  }
2133
2231
  }
2134
2232
 
2135
- // src/lib/detect.ts
2136
- import { existsSync as existsSync12 } from "fs";
2137
- import { execFileSync } from "child_process";
2138
- import { homedir as homedir8 } from "os";
2139
- import { join as join11 } from "path";
2140
- function commandExists(cmd) {
2141
- try {
2142
- execFileSync("which", [cmd], { stdio: "ignore" });
2143
- return true;
2144
- } catch {
2145
- 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);
2146
2253
  }
2147
- }
2148
- function detectInstalledPlatforms(projectDir) {
2149
- const detected = [];
2150
- const home = homedir8();
2151
- const cwd = projectDir || process.cwd();
2152
- if (existsSync12(join11(home, ".cursor")) || existsSync12(join11(cwd, ".cursor")))
2153
- detected.push("cursor");
2154
- if (existsSync12(join11(home, ".claude")) || commandExists("claude"))
2155
- detected.push("claude");
2156
- if (commandExists("codex")) detected.push("codex");
2157
- if (existsSync12(join11(home, ".codeium")) || existsSync12(join11(cwd, ".windsurf")))
2158
- detected.push("windsurf");
2159
- if (existsSync12(join11(cwd, ".clinerules"))) detected.push("cline");
2160
- if (existsSync12(join11(cwd, ".github"))) detected.push("copilot");
2161
- if (commandExists("opencode") || existsSync12(join11(cwd, ".opencode")))
2162
- detected.push("opencode");
2163
- if (commandExists("aider")) detected.push("aider");
2164
- return detected;
2254
+ return writtenFiles;
2165
2255
  }
2166
2256
 
2167
2257
  // src/lib/interactive.ts
2168
2258
  async function interactiveInstall(availableSkills, detectedPlatforms) {
2169
2259
  We("localskills install");
2170
- const slug = await Je({
2260
+ const slug = cancelGuard(await Je({
2171
2261
  message: "Which skill would you like to install?",
2172
2262
  options: availableSkills.map((s) => ({
2173
2263
  value: s.slug,
2174
2264
  label: s.name,
2175
2265
  hint: truncate(s.description, 60)
2176
2266
  }))
2177
- });
2178
- if (Ct(slug)) {
2179
- Ne("Cancelled.");
2180
- process.exit(0);
2181
- }
2267
+ }));
2182
2268
  const rest = await interactiveTargets(detectedPlatforms);
2183
2269
  return { slug, ...rest };
2184
2270
  }
2185
2271
  async function interactiveTargets(detectedPlatforms) {
2186
2272
  const allAdapters = getAllAdapters();
2187
- const platforms = await je({
2273
+ const platforms = cancelGuard(await je({
2188
2274
  message: "Which platforms should receive this skill?",
2189
2275
  options: allAdapters.map((a) => ({
2190
2276
  value: a.descriptor.id,
@@ -2193,12 +2279,8 @@ async function interactiveTargets(detectedPlatforms) {
2193
2279
  })),
2194
2280
  initialValues: detectedPlatforms.length > 0 ? detectedPlatforms : void 0,
2195
2281
  required: true
2196
- });
2197
- if (Ct(platforms)) {
2198
- Ne("Cancelled.");
2199
- process.exit(0);
2200
- }
2201
- const scope = await Je({
2282
+ }));
2283
+ const scope = cancelGuard(await Je({
2202
2284
  message: "Install scope?",
2203
2285
  options: [
2204
2286
  {
@@ -2213,12 +2295,8 @@ async function interactiveTargets(detectedPlatforms) {
2213
2295
  }
2214
2296
  ],
2215
2297
  initialValue: "project"
2216
- });
2217
- if (Ct(scope)) {
2218
- Ne("Cancelled.");
2219
- process.exit(0);
2220
- }
2221
- const method = await Je({
2298
+ }));
2299
+ const method = cancelGuard(await Je({
2222
2300
  message: "Install method?",
2223
2301
  options: [
2224
2302
  {
@@ -2233,31 +2311,18 @@ async function interactiveTargets(detectedPlatforms) {
2233
2311
  }
2234
2312
  ],
2235
2313
  initialValue: "symlink"
2236
- });
2237
- if (Ct(method)) {
2238
- Ne("Cancelled.");
2239
- process.exit(0);
2240
- }
2241
- return {
2242
- platforms,
2243
- scope,
2244
- method
2245
- };
2314
+ }));
2315
+ return { platforms, scope, method };
2246
2316
  }
2247
2317
  async function interactiveUninstall(installedSlugs) {
2248
2318
  We("localskills uninstall");
2249
- const slug = await Je({
2319
+ return cancelGuard(await Je({
2250
2320
  message: "Which skill would you like to uninstall?",
2251
2321
  options: installedSlugs.map((s) => ({
2252
2322
  value: s,
2253
2323
  label: s
2254
2324
  }))
2255
- });
2256
- if (Ct(slug)) {
2257
- Ne("Cancelled.");
2258
- process.exit(0);
2259
- }
2260
- return slug;
2325
+ }));
2261
2326
  }
2262
2327
  function truncate(str, max) {
2263
2328
  if (str.length <= max) return str;
@@ -2293,6 +2358,19 @@ function parsePlatforms(raw) {
2293
2358
  }
2294
2359
  return platforms;
2295
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
+ }
2296
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(
2297
2375
  async (slugArg, opts) => {
2298
2376
  const client = new ApiClient();
@@ -2345,10 +2423,17 @@ var installCommand = new Command2("install").description("Install a skill locall
2345
2423
  method = explicitMethod || answers.method;
2346
2424
  }
2347
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);
2348
2433
  const spinner = bt2();
2349
2434
  spinner.start(`Fetching ${slug}...`);
2350
2435
  const res = await client.get(
2351
- `/api/skills/${encodeURIComponent(slug)}/content`
2436
+ `/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
2352
2437
  );
2353
2438
  if (!res.success || !res.data) {
2354
2439
  spinner.stop("Failed.");
@@ -2360,9 +2445,62 @@ var installCommand = new Command2("install").description("Install a skill locall
2360
2445
  process.exit(1);
2361
2446
  return;
2362
2447
  }
2363
- const { skill, content, version } = res.data;
2364
- spinner.stop(`Fetched ${skill.name} v${version}`);
2365
- 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)}`);
2366
2504
  store(cacheKey, content, skill, version);
2367
2505
  const contentType = skill.type ?? "skill";
2368
2506
  const installations = [];
@@ -2402,17 +2540,15 @@ var installCommand = new Command2("install").description("Install a skill locall
2402
2540
  const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
2403
2541
  results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
2404
2542
  }
2405
- const existing = config.installed_skills[cacheKey];
2406
- const skillRecord = {
2407
- slug: cacheKey,
2408
- name: skill.name,
2409
- type: contentType,
2410
- hash: skill.contentHash,
2543
+ config.installed_skills[cacheKey] = buildSkillRecord(
2544
+ cacheKey,
2545
+ skill,
2411
2546
  version,
2412
- cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
2413
- installations: existing ? [...existing.installations, ...installations] : installations
2414
- };
2415
- config.installed_skills[cacheKey] = skillRecord;
2547
+ resolvedSemver,
2548
+ requestedRange,
2549
+ config.installed_skills[cacheKey]?.installations,
2550
+ installations
2551
+ );
2416
2552
  saveConfig(config);
2417
2553
  for (const r of results) {
2418
2554
  R2.success(r);
@@ -2471,33 +2607,62 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
2471
2607
 
2472
2608
  // src/commands/list.ts
2473
2609
  import { Command as Command4 } from "commander";
2474
- 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) => {
2475
2611
  const client = new ApiClient();
2476
- if (!client.isAuthenticated()) {
2477
- console.error("Not authenticated. Run `localskills login` first.");
2478
- process.exit(1);
2479
- }
2480
- const path = opts.public ? "/api/skills?visibility=public" : "/api/skills";
2481
- const res = await client.get(path);
2482
- if (!res.success || !res.data) {
2483
- console.error(`Error: ${res.error || "Failed to fetch skills"}`);
2612
+ if ((opts.tag || opts.search) && !opts.public) {
2613
+ console.error("The --tag and --search flags require --public.");
2484
2614
  process.exit(1);
2485
- return;
2486
2615
  }
2487
- if (res.data.length === 0) {
2488
- console.log("No skills found.");
2489
- return;
2490
- }
2491
- console.log("Available skills:\n");
2492
- for (const skill of res.data) {
2493
- const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
2494
- console.log(` ${skill.slug}${vis} \u2014 ${skill.description || skill.name}`);
2495
- }
2496
- 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(`
2497
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(`
2660
+ ${res.data.length} skill(s) found.`);
2661
+ }
2498
2662
  });
2499
2663
 
2500
2664
  // src/commands/pull.ts
2665
+ import { mkdirSync as mkdirSync8 } from "fs";
2501
2666
  import { Command as Command5 } from "commander";
2502
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) => {
2503
2668
  const config = loadConfig();
@@ -2517,53 +2682,70 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
2517
2682
  R2.warn(`${slug} \u2014 not found in config, skipping.`);
2518
2683
  continue;
2519
2684
  }
2685
+ const versionQuery = buildVersionQuery(installed.semverRange);
2520
2686
  spinner.start(`Checking ${slug}...`);
2521
2687
  const res = await client.get(
2522
- `/api/skills/${encodeURIComponent(slug)}/content`
2688
+ `/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
2523
2689
  );
2524
2690
  if (!res.success || !res.data) {
2525
2691
  spinner.stop(`${slug} \u2014 failed: ${res.error || "not found"}`);
2526
2692
  continue;
2527
2693
  }
2528
- const { skill, content, version } = res.data;
2694
+ const resData = res.data;
2695
+ const format = resData.format ?? "text";
2696
+ const { skill, version } = resData;
2529
2697
  if (skill.contentHash === installed.hash) {
2530
2698
  spinner.stop(`${slug} \u2014 up to date`);
2531
2699
  skipped++;
2532
2700
  continue;
2533
2701
  }
2534
- store(slug, content, skill, version);
2535
- for (const installation of installed.installations) {
2536
- if (installation.method === "symlink") {
2537
- getPlatformFile(slug, installation.platform, skill);
2538
- 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);
2539
2711
  }
2540
- const adapter = getAdapter(installation.platform);
2541
- const transformed = adapter.transformContent(content, skill);
2542
- if (installation.method === "section") {
2543
- upsertSection(
2544
- installation.path,
2545
- slug,
2546
- `## ${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}
2547
2727
 
2548
2728
  ${transformed}`
2549
- );
2550
- } else {
2551
- const cachePath = getPlatformFile(slug, installation.platform, skill);
2552
- adapter.install({
2553
- slug,
2554
- content: transformed,
2555
- scope: installation.scope,
2556
- method: "copy",
2557
- cachePath,
2558
- projectDir: installation.projectDir
2559
- });
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
+ }
2560
2741
  }
2561
2742
  }
2562
2743
  installed.hash = skill.contentHash;
2563
2744
  installed.version = version;
2745
+ installed.semver = resData.semver ?? null;
2564
2746
  installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
2565
2747
  updated++;
2566
- spinner.stop(`${slug} \u2014 updated to v${version}`);
2748
+ spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version)}`);
2567
2749
  }
2568
2750
  saveConfig(config);
2569
2751
  Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
@@ -2571,44 +2753,44 @@ ${transformed}`
2571
2753
 
2572
2754
  // src/commands/publish.ts
2573
2755
  import { Command as Command6 } from "commander";
2574
- import { readFileSync as readFileSync6, existsSync as existsSync14 } from "fs";
2575
- 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";
2576
2758
  import { homedir as homedir10 } from "os";
2577
2759
 
2578
2760
  // src/lib/scanner.ts
2579
- import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
2580
- 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";
2581
2763
  import { homedir as homedir9 } from "os";
2582
- import { readlinkSync as readlinkSync2, lstatSync as lstatSync2 } from "fs";
2764
+ import { readlinkSync, lstatSync as lstatSync2 } from "fs";
2583
2765
  function scanForSkills(projectDir) {
2584
2766
  const home = homedir9();
2585
2767
  const cwd = projectDir || process.cwd();
2586
2768
  const results = [];
2587
- scanDirectory(join12(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
2588
- scanDirectory(join12(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
2589
- scanClaudeSkills(join12(home, ".claude", "skills"), "global", results);
2590
- scanClaudeSkills(join12(cwd, ".claude", "skills"), "project", results);
2591
- scanDirectory(join12(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
2592
- scanDirectory(join12(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
2593
- scanSingleFile(join12(home, ".codex", "AGENTS.md"), "codex", "global", results);
2594
- 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);
2595
2777
  scanSingleFile(
2596
- join12(home, ".codeium", "windsurf", "memories", "global_rules.md"),
2778
+ join14(home, ".codeium", "windsurf", "memories", "global_rules.md"),
2597
2779
  "windsurf",
2598
2780
  "global",
2599
2781
  results
2600
2782
  );
2601
- scanDirectory(join12(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
2602
- 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);
2603
2785
  scanSingleFile(
2604
- join12(cwd, ".github", "copilot-instructions.md"),
2786
+ join14(cwd, ".github", "copilot-instructions.md"),
2605
2787
  "copilot",
2606
2788
  "project",
2607
2789
  results
2608
2790
  );
2609
- scanDirectory(join12(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
2610
- scanDirectory(join12(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
2611
- 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);
2612
2794
  return results;
2613
2795
  }
2614
2796
  function filterTracked(detected, config) {
@@ -2618,13 +2800,13 @@ function filterTracked(detected, config) {
2618
2800
  trackedPaths.add(inst.path);
2619
2801
  }
2620
2802
  }
2621
- const cacheDir = join12(homedir9(), ".localskills", "cache");
2803
+ const cacheDir = join14(homedir9(), ".localskills", "cache");
2622
2804
  return detected.filter((skill) => {
2623
2805
  if (trackedPaths.has(skill.filePath)) return false;
2624
2806
  try {
2625
2807
  const stat = lstatSync2(skill.filePath);
2626
2808
  if (stat.isSymbolicLink()) {
2627
- const target = readlinkSync2(skill.filePath);
2809
+ const target = readlinkSync(skill.filePath);
2628
2810
  if (target.startsWith(cacheDir)) return false;
2629
2811
  }
2630
2812
  } catch {
@@ -2635,11 +2817,9 @@ function filterTracked(detected, config) {
2635
2817
  function slugFromFilename(filename) {
2636
2818
  return basename(filename, extname(filename));
2637
2819
  }
2638
- function nameFromSlug(slug) {
2639
- return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2640
- }
2820
+ var nameFromSlug = titleFromSlug;
2641
2821
  function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
2642
- if (!existsSync13(dir)) return;
2822
+ if (!existsSync14(dir)) return;
2643
2823
  let entries;
2644
2824
  try {
2645
2825
  entries = readdirSync2(dir);
@@ -2648,7 +2828,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
2648
2828
  }
2649
2829
  for (const entry of entries) {
2650
2830
  if (!entry.endsWith(ext)) continue;
2651
- const filePath = join12(dir, entry);
2831
+ const filePath = join14(dir, entry);
2652
2832
  try {
2653
2833
  const raw = readFileSync5(filePath, "utf-8");
2654
2834
  const content = stripFrontmatter(raw).trim();
@@ -2668,7 +2848,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
2668
2848
  }
2669
2849
  }
2670
2850
  function scanClaudeSkills(skillsDir, scope, results) {
2671
- if (!existsSync13(skillsDir)) return;
2851
+ if (!existsSync14(skillsDir)) return;
2672
2852
  let entries;
2673
2853
  try {
2674
2854
  entries = readdirSync2(skillsDir);
@@ -2676,8 +2856,8 @@ function scanClaudeSkills(skillsDir, scope, results) {
2676
2856
  return;
2677
2857
  }
2678
2858
  for (const entry of entries) {
2679
- const skillFile = join12(skillsDir, entry, "SKILL.md");
2680
- if (!existsSync13(skillFile)) continue;
2859
+ const skillFile = join14(skillsDir, entry, "SKILL.md");
2860
+ if (!existsSync14(skillFile)) continue;
2681
2861
  try {
2682
2862
  const raw = readFileSync5(skillFile, "utf-8");
2683
2863
  const content = stripFrontmatter(raw).trim();
@@ -2696,7 +2876,7 @@ function scanClaudeSkills(skillsDir, scope, results) {
2696
2876
  }
2697
2877
  }
2698
2878
  function scanSingleFile(filePath, platform, scope, results) {
2699
- if (!existsSync13(filePath)) return;
2879
+ if (!existsSync14(filePath)) return;
2700
2880
  let raw;
2701
2881
  try {
2702
2882
  raw = readFileSync5(filePath, "utf-8");
@@ -2747,10 +2927,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2747
2927
  ).option("--type <type>", "Content type: skill or rule", "skill").option("-m, --message <message>", "Version message").action(
2748
2928
  async (fileArg, opts) => {
2749
2929
  const client = new ApiClient();
2750
- if (!client.isAuthenticated()) {
2751
- console.error("Not authenticated. Run `localskills login` first.");
2752
- process.exit(1);
2753
- }
2930
+ requireAuth(client);
2754
2931
  const teamsRes = await client.get("/api/tenants");
2755
2932
  if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
2756
2933
  console.error(
@@ -2761,8 +2938,8 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2761
2938
  }
2762
2939
  const teams = teamsRes.data;
2763
2940
  if (fileArg) {
2764
- const filePath = resolve3(fileArg);
2765
- if (!existsSync14(filePath)) {
2941
+ const filePath = resolve4(fileArg);
2942
+ if (!existsSync15(filePath)) {
2766
2943
  console.error(`File not found: ${filePath}`);
2767
2944
  process.exit(1);
2768
2945
  return;
@@ -2775,7 +2952,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2775
2952
  return;
2776
2953
  }
2777
2954
  const defaultSlug = basename2(filePath, extname2(filePath));
2778
- const defaultName = defaultSlug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2955
+ const defaultName = titleFromSlug(defaultSlug);
2779
2956
  const skillName = opts.name || defaultName;
2780
2957
  const contentType = validateContentType(opts.type || "skill");
2781
2958
  const visibility = validateVisibility(opts.visibility || "private");
@@ -2801,7 +2978,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2801
2978
  Le("Nothing to publish.");
2802
2979
  return;
2803
2980
  }
2804
- const selected = await je({
2981
+ const skills = cancelGuard(await je({
2805
2982
  message: "Select items to publish",
2806
2983
  options: detected.map((s) => ({
2807
2984
  value: s,
@@ -2809,28 +2986,19 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2809
2986
  hint: `${s.platform}/${s.scope}/${s.contentType} ${shortenPath(s.filePath)}`
2810
2987
  })),
2811
2988
  required: true
2812
- });
2813
- if (Ct(selected)) {
2814
- Ne("Cancelled.");
2815
- process.exit(0);
2816
- }
2817
- const skills = selected;
2989
+ }));
2818
2990
  const tenantId = await resolveTeam(teams, opts.team);
2819
2991
  for (const skill of skills) {
2820
2992
  R2.step(`Publishing ${skill.suggestedName}...`);
2821
- const name = await Ze({
2993
+ const name = cancelGuard(await Ze({
2822
2994
  message: "Skill name?",
2823
2995
  initialValue: skill.suggestedName,
2824
2996
  validate: (v) => {
2825
2997
  if (!v || v.length < 1) return "Name is required";
2826
2998
  if (v.length > 100) return "Name must be 100 characters or less";
2827
2999
  }
2828
- });
2829
- if (Ct(name)) {
2830
- Ne("Cancelled.");
2831
- process.exit(0);
2832
- }
2833
- const visibility = await Je({
3000
+ }));
3001
+ const visibility = cancelGuard(await Je({
2834
3002
  message: "Visibility?",
2835
3003
  options: [
2836
3004
  { value: "private", label: "Private", hint: "Only team members" },
@@ -2838,23 +3006,15 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2838
3006
  { value: "unlisted", label: "Unlisted", hint: "Accessible via direct link" }
2839
3007
  ],
2840
3008
  initialValue: "private"
2841
- });
2842
- if (Ct(visibility)) {
2843
- Ne("Cancelled.");
2844
- process.exit(0);
2845
- }
2846
- const contentType = await Je({
3009
+ }));
3010
+ const contentType = cancelGuard(await Je({
2847
3011
  message: "Type?",
2848
3012
  options: [
2849
3013
  { value: "skill", label: "Skill", hint: "Reusable agent instructions" },
2850
3014
  { value: "rule", label: "Rule", hint: "Governance constraints" }
2851
3015
  ],
2852
3016
  initialValue: skill.contentType
2853
- });
2854
- if (Ct(contentType)) {
2855
- Ne("Cancelled.");
2856
- process.exit(0);
2857
- }
3017
+ }));
2858
3018
  await uploadSkill(client, {
2859
3019
  name,
2860
3020
  content: skill.content,
@@ -2879,19 +3039,14 @@ async function resolveTeam(teams, teamFlag) {
2879
3039
  if (teams.length === 1) {
2880
3040
  return teams[0].id;
2881
3041
  }
2882
- const selected = await Je({
3042
+ return cancelGuard(await Je({
2883
3043
  message: "Which team?",
2884
3044
  options: teams.map((t) => ({
2885
3045
  value: t.id,
2886
3046
  label: t.name,
2887
3047
  hint: t.slug
2888
3048
  }))
2889
- });
2890
- if (Ct(selected)) {
2891
- Ne("Cancelled.");
2892
- process.exit(0);
2893
- }
2894
- return selected;
3049
+ }));
2895
3050
  }
2896
3051
  async function uploadSkill(client, params) {
2897
3052
  const spinner = bt2();
@@ -2936,8 +3091,200 @@ function shortenPath(filePath) {
2936
3091
  return filePath;
2937
3092
  }
2938
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
+
2939
3286
  // src/index.ts
2940
- var program = new Command7();
3287
+ var program = new Command9();
2941
3288
  program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0");
2942
3289
  program.addCommand(loginCommand);
2943
3290
  program.addCommand(logoutCommand);
@@ -2947,4 +3294,6 @@ program.addCommand(uninstallCommand);
2947
3294
  program.addCommand(listCommand);
2948
3295
  program.addCommand(pullCommand);
2949
3296
  program.addCommand(publishCommand);
3297
+ program.addCommand(pushCommand);
3298
+ program.addCommand(shareCommand);
2950
3299
  program.parse();