@localskills/cli 0.3.0 → 0.8.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 +1051 -427
  2. package/package.json +3 -2
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 Command10 } from "commander";
165
165
 
166
166
  // src/commands/auth.ts
167
167
  import { Command } from "commander";
@@ -521,6 +521,23 @@ var x = class {
521
521
  }
522
522
  }
523
523
  };
524
+ var kt = class extends x {
525
+ get cursor() {
526
+ return this.value ? 0 : 1;
527
+ }
528
+ get _value() {
529
+ return this.cursor === 0;
530
+ }
531
+ constructor(e2) {
532
+ super(e2, false), this.value = !!e2.initialValue, this.on("userInput", () => {
533
+ this.value = this._value;
534
+ }), this.on("confirm", (s) => {
535
+ this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = s, this.state = "submit", this.close();
536
+ }), this.on("cursor", () => {
537
+ this.value = !this.value;
538
+ });
539
+ }
540
+ };
524
541
  var Lt = class extends x {
525
542
  options;
526
543
  cursor = 0;
@@ -876,6 +893,33 @@ var X2 = (t) => {
876
893
  for (const A of f) for (const w of A) B2.push(w);
877
894
  return h && B2.push(g), B2;
878
895
  };
896
+ var Re = (t) => {
897
+ const r = t.active ?? "Yes", s = t.inactive ?? "No";
898
+ return new kt({ active: r, inactive: s, signal: t.signal, input: t.input, output: t.output, initialValue: t.initialValue ?? true, render() {
899
+ const i = t.withGuide ?? _.withGuide, a = `${i ? `${import_picocolors2.default.gray(d)}
900
+ ` : ""}${W2(this.state)} ${t.message}
901
+ `, o = this.value ? r : s;
902
+ switch (this.state) {
903
+ case "submit": {
904
+ const u = i ? `${import_picocolors2.default.gray(d)} ` : "";
905
+ return `${a}${u}${import_picocolors2.default.dim(o)}`;
906
+ }
907
+ case "cancel": {
908
+ const u = i ? `${import_picocolors2.default.gray(d)} ` : "";
909
+ return `${a}${u}${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o))}${i ? `
910
+ ${import_picocolors2.default.gray(d)}` : ""}`;
911
+ }
912
+ default: {
913
+ const u = i ? `${import_picocolors2.default.cyan(d)} ` : "", l = i ? import_picocolors2.default.cyan(x2) : "";
914
+ return `${a}${u}${this.value ? `${import_picocolors2.default.green(Q2)} ${r}` : `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(r)}`}${t.vertical ? i ? `
915
+ ${import_picocolors2.default.cyan(d)} ` : `
916
+ ` : ` ${import_picocolors2.default.dim("/")} `}${this.value ? `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.green(Q2)} ${s}`}
917
+ ${l}
918
+ `;
919
+ }
920
+ }
921
+ } }).prompt();
922
+ };
879
923
  var R2 = { message: (t = [], { symbol: r = import_picocolors2.default.gray(d), secondarySymbol: s = import_picocolors2.default.gray(d), output: i = process.stdout, spacing: a = 1, withGuide: o } = {}) => {
880
924
  const u = [], l = o ?? _.withGuide, n = l ? s : "", c = l ? `${r} ` : "", g = l ? `${s} ` : "";
881
925
  for (let p = 0; p < a; p++) u.push(n);
@@ -1101,36 +1145,124 @@ ${l}
1101
1145
  } }).prompt();
1102
1146
 
1103
1147
  // src/lib/config.ts
1104
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
1148
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from "fs";
1105
1149
  import { join } from "path";
1106
1150
  import { homedir } from "os";
1107
1151
  var CONFIG_DIR = join(homedir(), ".localskills");
1108
1152
  var CONFIG_PATH = join(CONFIG_DIR, "config.json");
1109
- var DEFAULT_CONFIG = {
1110
- config_version: 2,
1111
- api_url: "https://localskills.sh",
1153
+ var DEFAULT_PROFILE_NAME = "default";
1154
+ var DEFAULT_PROFILE = {
1112
1155
  token: null,
1113
1156
  installed_skills: {},
1114
1157
  defaults: {
1115
1158
  scope: "project",
1116
1159
  method: "symlink"
1160
+ },
1161
+ anonymous_key: null
1162
+ };
1163
+ var DEFAULT_API_URL = "https://localskills.sh";
1164
+ var ProfileNotFoundError = class extends Error {
1165
+ constructor(profileName, availableProfiles, source) {
1166
+ const sourceLabel = source === "flag" ? "" : source === "env" ? " (from LOCALSKILLS_PROFILE)" : "";
1167
+ super(`Profile "${profileName}"${sourceLabel} does not exist. Available profiles: ${availableProfiles.join(", ")}`);
1168
+ this.profileName = profileName;
1169
+ this.availableProfiles = availableProfiles;
1170
+ this.source = source;
1171
+ this.name = "ProfileNotFoundError";
1117
1172
  }
1118
1173
  };
1119
- function loadConfig() {
1174
+ var _profileOverride;
1175
+ function setProfileOverride(name) {
1176
+ _profileOverride = name;
1177
+ }
1178
+ function resolveProfileName(config) {
1179
+ if (_profileOverride) {
1180
+ if (!config.profiles[_profileOverride]) {
1181
+ throw new ProfileNotFoundError(
1182
+ _profileOverride,
1183
+ Object.keys(config.profiles),
1184
+ "flag"
1185
+ );
1186
+ }
1187
+ return _profileOverride;
1188
+ }
1189
+ const envProfile = process.env.LOCALSKILLS_PROFILE;
1190
+ if (envProfile) {
1191
+ if (!config.profiles[envProfile]) {
1192
+ throw new ProfileNotFoundError(
1193
+ envProfile,
1194
+ Object.keys(config.profiles),
1195
+ "env"
1196
+ );
1197
+ }
1198
+ return envProfile;
1199
+ }
1200
+ return config.active_profile;
1201
+ }
1202
+ function getActiveProfileName() {
1203
+ const full = loadFullConfig();
1204
+ return resolveProfileName(full);
1205
+ }
1206
+ function validateV3(config) {
1207
+ if (!config.profiles || typeof config.profiles !== "object") {
1208
+ config.profiles = {
1209
+ [DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
1210
+ };
1211
+ }
1212
+ if (!config.profiles[DEFAULT_PROFILE_NAME]) {
1213
+ config.profiles[DEFAULT_PROFILE_NAME] = { ...DEFAULT_PROFILE, installed_skills: {} };
1214
+ }
1215
+ if (!config.active_profile || !config.profiles[config.active_profile]) {
1216
+ config.active_profile = DEFAULT_PROFILE_NAME;
1217
+ }
1218
+ return config;
1219
+ }
1220
+ function loadFullConfig() {
1120
1221
  if (!existsSync(CONFIG_PATH)) {
1121
- return { ...DEFAULT_CONFIG, installed_skills: {} };
1222
+ return {
1223
+ config_version: 3,
1224
+ api_url: DEFAULT_API_URL,
1225
+ active_profile: DEFAULT_PROFILE_NAME,
1226
+ profiles: {
1227
+ [DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
1228
+ }
1229
+ };
1122
1230
  }
1123
1231
  try {
1124
1232
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1233
+ if (raw.config_version === 3) {
1234
+ return validateV3(raw);
1235
+ }
1236
+ let v2;
1125
1237
  if (!raw.config_version || raw.config_version < 2) {
1126
- return migrateV1toV2(raw);
1238
+ v2 = migrateV1toV2(raw);
1239
+ } else {
1240
+ v2 = raw;
1127
1241
  }
1128
- return { ...DEFAULT_CONFIG, ...raw };
1129
- } catch {
1130
- return { ...DEFAULT_CONFIG, installed_skills: {} };
1242
+ const v3 = migrateV2toV3(v2);
1243
+ saveFullConfig(v3);
1244
+ return v3;
1245
+ } catch (err) {
1246
+ if (err instanceof SyntaxError) {
1247
+ console.error(`Warning: Config file is corrupt (${CONFIG_PATH}). Backing up and starting fresh.`);
1248
+ try {
1249
+ copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
1250
+ console.error(` Backup saved to ${CONFIG_PATH}.bak`);
1251
+ } catch {
1252
+ }
1253
+ return {
1254
+ config_version: 3,
1255
+ api_url: DEFAULT_API_URL,
1256
+ active_profile: DEFAULT_PROFILE_NAME,
1257
+ profiles: {
1258
+ [DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
1259
+ }
1260
+ };
1261
+ }
1262
+ throw err;
1131
1263
  }
1132
1264
  }
1133
- function saveConfig(config) {
1265
+ function saveFullConfig(config) {
1134
1266
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1135
1267
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
1136
1268
  mode: 384
@@ -1141,10 +1273,35 @@ function saveConfig(config) {
1141
1273
  } catch {
1142
1274
  }
1143
1275
  }
1276
+ function loadConfig() {
1277
+ const full = loadFullConfig();
1278
+ const profileName = resolveProfileName(full);
1279
+ const profile = full.profiles[profileName] ?? { ...DEFAULT_PROFILE, installed_skills: {} };
1280
+ return {
1281
+ config_version: 2,
1282
+ api_url: full.api_url,
1283
+ token: profile.token,
1284
+ installed_skills: profile.installed_skills,
1285
+ defaults: profile.defaults,
1286
+ anonymous_key: profile.anonymous_key ?? null
1287
+ };
1288
+ }
1289
+ function saveConfig(config) {
1290
+ const full = loadFullConfig();
1291
+ const profileName = resolveProfileName(full);
1292
+ full.api_url = config.api_url;
1293
+ full.profiles[profileName] = {
1294
+ token: config.token,
1295
+ installed_skills: config.installed_skills,
1296
+ defaults: config.defaults,
1297
+ anonymous_key: config.anonymous_key ?? null
1298
+ };
1299
+ saveFullConfig(full);
1300
+ }
1144
1301
  function migrateV1toV2(v1) {
1145
1302
  const v2 = {
1146
1303
  config_version: 2,
1147
- api_url: v1.api_url || DEFAULT_CONFIG.api_url,
1304
+ api_url: v1.api_url || DEFAULT_API_URL,
1148
1305
  token: v1.token,
1149
1306
  installed_skills: {},
1150
1307
  defaults: {
@@ -1159,6 +1316,8 @@ function migrateV1toV2(v1) {
1159
1316
  name: skill.slug,
1160
1317
  hash: skill.hash,
1161
1318
  version: 0,
1319
+ semver: null,
1320
+ semverRange: null,
1162
1321
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
1163
1322
  installations: [
1164
1323
  {
@@ -1171,9 +1330,23 @@ function migrateV1toV2(v1) {
1171
1330
  ]
1172
1331
  };
1173
1332
  }
1174
- saveConfig(v2);
1175
1333
  return v2;
1176
1334
  }
1335
+ function migrateV2toV3(v2) {
1336
+ return {
1337
+ config_version: 3,
1338
+ api_url: v2.api_url || DEFAULT_API_URL,
1339
+ active_profile: DEFAULT_PROFILE_NAME,
1340
+ profiles: {
1341
+ [DEFAULT_PROFILE_NAME]: {
1342
+ token: v2.token,
1343
+ installed_skills: v2.installed_skills,
1344
+ defaults: v2.defaults,
1345
+ anonymous_key: v2.anonymous_key ?? null
1346
+ }
1347
+ }
1348
+ };
1349
+ }
1177
1350
  function getToken() {
1178
1351
  return loadConfig().token;
1179
1352
  }
@@ -1187,6 +1360,14 @@ function clearToken() {
1187
1360
  config.token = null;
1188
1361
  saveConfig(config);
1189
1362
  }
1363
+ function getAnonymousKey() {
1364
+ return loadConfig().anonymous_key ?? null;
1365
+ }
1366
+ function setAnonymousKey(key) {
1367
+ const config = loadConfig();
1368
+ config.anonymous_key = key;
1369
+ saveConfig(config);
1370
+ }
1190
1371
 
1191
1372
  // src/lib/api-client.ts
1192
1373
  var ApiClient = class {
@@ -1229,25 +1410,16 @@ var ApiClient = class {
1229
1410
  });
1230
1411
  return this.handleResponse(res);
1231
1412
  }
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
- });
1413
+ async fetchBinary(url) {
1414
+ const headers = {};
1415
+ if (this.token) {
1416
+ headers["Authorization"] = `Bearer ${this.token}`;
1417
+ }
1418
+ const res = await fetch(url, { headers });
1419
+ if (!res.ok) {
1420
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1421
+ }
1422
+ return Buffer.from(await res.arrayBuffer());
1251
1423
  }
1252
1424
  isAuthenticated() {
1253
1425
  return this.token !== null;
@@ -1285,9 +1457,29 @@ function openBrowser(url) {
1285
1457
  }
1286
1458
  }
1287
1459
  function sleep(ms) {
1288
- return new Promise((resolve4) => setTimeout(resolve4, ms));
1460
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1289
1461
  }
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) => {
1462
+ 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) => {
1463
+ if (opts.oidcToken) {
1464
+ if (!opts.team) {
1465
+ console.error("Error: --team is required with --oidc-token");
1466
+ process.exit(1);
1467
+ }
1468
+ const client2 = new ApiClient();
1469
+ const res = await client2.post(
1470
+ "/api/oidc/token",
1471
+ { token: opts.oidcToken, team: opts.team }
1472
+ );
1473
+ if (!res.success || !res.data) {
1474
+ console.error(`OIDC login failed: ${res.error || "unknown error"}`);
1475
+ process.exit(1);
1476
+ }
1477
+ setToken(res.data.token);
1478
+ console.log(
1479
+ `Authenticated via OIDC (expires ${new Date(res.data.expiresAt).toISOString()})`
1480
+ );
1481
+ return;
1482
+ }
1291
1483
  if (opts.token) {
1292
1484
  setToken(opts.token);
1293
1485
  const client2 = new ApiClient();
@@ -1382,22 +1574,75 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
1382
1574
 
1383
1575
  // src/commands/install.ts
1384
1576
  import { Command as Command2 } from "commander";
1577
+ import { mkdirSync as mkdirSync7 } from "fs";
1578
+
1579
+ // ../../packages/shared/dist/utils/semver.js
1580
+ var SEMVER_RE = /^\d+\.\d+\.\d+$/;
1581
+ var RANGE_RE = /^(\^|~|>=)?\d+\.\d+\.\d+$|^\*$/;
1582
+ function parseSemVer(v) {
1583
+ if (!SEMVER_RE.test(v))
1584
+ return null;
1585
+ const [major, minor, patch] = v.split(".").map(Number);
1586
+ if (major > 999999 || minor > 999999 || patch > 999999)
1587
+ return null;
1588
+ return { major, minor, patch };
1589
+ }
1590
+ function isValidSemVer(v) {
1591
+ return parseSemVer(v) !== null;
1592
+ }
1593
+ function isValidSemVerRange(range) {
1594
+ return RANGE_RE.test(range);
1595
+ }
1596
+
1597
+ // ../../packages/shared/dist/utils/index.js
1598
+ function titleFromSlug(slug) {
1599
+ return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1600
+ }
1601
+
1602
+ // src/lib/cli-helpers.ts
1603
+ function requireAuth(client) {
1604
+ if (!client.isAuthenticated()) {
1605
+ console.error("Not authenticated. Run `localskills login` first.");
1606
+ process.exit(1);
1607
+ }
1608
+ }
1609
+ function buildVersionQuery(range) {
1610
+ if (!range) return "";
1611
+ if (isValidSemVer(range)) {
1612
+ return `?semver=${encodeURIComponent(range)}`;
1613
+ }
1614
+ if (isValidSemVerRange(range)) {
1615
+ return `?range=${encodeURIComponent(range)}`;
1616
+ }
1617
+ console.error(`Invalid version specifier: ${range}`);
1618
+ process.exit(1);
1619
+ }
1620
+ function formatVersionLabel(semver, version) {
1621
+ return semver ? `v${semver}` : `v${version}`;
1622
+ }
1623
+ function cancelGuard(value) {
1624
+ if (Ct(value)) {
1625
+ Ne("Cancelled.");
1626
+ process.exit(0);
1627
+ }
1628
+ return value;
1629
+ }
1385
1630
 
1386
1631
  // src/lib/cache.ts
1387
1632
  import {
1388
- existsSync as existsSync11,
1389
- mkdirSync as mkdirSync10,
1633
+ existsSync as existsSync13,
1634
+ mkdirSync as mkdirSync5,
1390
1635
  readFileSync as readFileSync4,
1391
1636
  readdirSync,
1392
- writeFileSync as writeFileSync9,
1393
- rmSync as rmSync4
1637
+ writeFileSync as writeFileSync5,
1638
+ rmSync as rmSync3
1394
1639
  } from "fs";
1395
- import { join as join10, resolve as resolve2 } from "path";
1396
- import { homedir as homedir7 } from "os";
1640
+ import { join as join12, resolve as resolve2 } from "path";
1641
+ import { homedir as homedir8 } from "os";
1397
1642
 
1398
1643
  // 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";
1644
+ import { existsSync as existsSync4 } from "fs";
1645
+ import { join as join3 } from "path";
1401
1646
  import { homedir as homedir2 } from "os";
1402
1647
 
1403
1648
  // src/lib/content-transform.ts
@@ -1437,10 +1682,13 @@ function stripFrontmatter(content) {
1437
1682
  return content;
1438
1683
  }
1439
1684
 
1685
+ // src/lib/installers/common.ts
1686
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1687
+ import { join as join2 } from "path";
1688
+
1440
1689
  // src/lib/symlink.ts
1441
1690
  import {
1442
1691
  symlinkSync,
1443
- readlinkSync,
1444
1692
  unlinkSync,
1445
1693
  lstatSync,
1446
1694
  existsSync as existsSync2,
@@ -1477,6 +1725,31 @@ function isSymlink(path) {
1477
1725
  }
1478
1726
  }
1479
1727
 
1728
+ // src/lib/installers/common.ts
1729
+ function safeSlugName(slug) {
1730
+ return slug.replace(/\//g, "-");
1731
+ }
1732
+ var DEFAULT_METHOD = "symlink";
1733
+ function installFileOrSymlink(opts, targetPath) {
1734
+ if (opts.method === "symlink") {
1735
+ createSymlink(opts.cachePath, targetPath);
1736
+ } else {
1737
+ mkdirSync3(join2(targetPath, ".."), { recursive: true });
1738
+ writeFileSync2(targetPath, opts.content);
1739
+ }
1740
+ return targetPath;
1741
+ }
1742
+ function uninstallFile(installation) {
1743
+ if (installation.method === "symlink") {
1744
+ removeSymlink(installation.path);
1745
+ } else if (existsSync3(installation.path)) {
1746
+ unlinkSync2(installation.path);
1747
+ }
1748
+ }
1749
+ function defaultTransformContent(content) {
1750
+ return toPlainMD(content);
1751
+ }
1752
+
1480
1753
  // src/lib/installers/cursor.ts
1481
1754
  var descriptor = {
1482
1755
  id: "cursor",
@@ -1490,40 +1763,30 @@ function detect(projectDir) {
1490
1763
  const home = homedir2();
1491
1764
  const cwd = projectDir || process.cwd();
1492
1765
  return {
1493
- global: existsSync3(join2(home, ".cursor")),
1494
- project: existsSync3(join2(cwd, ".cursor"))
1766
+ global: existsSync4(join3(home, ".cursor")),
1767
+ project: existsSync4(join3(cwd, ".cursor"))
1495
1768
  };
1496
1769
  }
1497
1770
  function resolvePath(slug, scope, projectDir, _contentType) {
1498
- const safeName = slug.replace(/\//g, "-");
1771
+ const safeName = safeSlugName(slug);
1499
1772
  if (scope === "global") {
1500
- return join2(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
1773
+ return join3(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
1501
1774
  }
1502
1775
  const dir = projectDir || process.cwd();
1503
- return join2(dir, ".cursor", "rules", `${safeName}.mdc`);
1776
+ return join3(dir, ".cursor", "rules", `${safeName}.mdc`);
1504
1777
  }
1505
1778
  function transformContent(content, skill) {
1506
1779
  return toCursorMDC(content, skill);
1507
1780
  }
1508
1781
  function install(opts) {
1509
1782
  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;
1783
+ return installFileOrSymlink(opts, targetPath);
1517
1784
  }
1518
1785
  function uninstall(installation, _slug) {
1519
- if (installation.method === "symlink") {
1520
- removeSymlink(installation.path);
1521
- } else if (existsSync3(installation.path)) {
1522
- unlinkSync2(installation.path);
1523
- }
1786
+ uninstallFile(installation);
1524
1787
  }
1525
1788
  function defaultMethod() {
1526
- return "symlink";
1789
+ return DEFAULT_METHOD;
1527
1790
  }
1528
1791
  var cursorAdapter = {
1529
1792
  descriptor,
@@ -1536,8 +1799,8 @@ var cursorAdapter = {
1536
1799
  };
1537
1800
 
1538
1801
  // 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";
1802
+ import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
1803
+ import { join as join4 } from "path";
1541
1804
  import { homedir as homedir3 } from "os";
1542
1805
  var descriptor2 = {
1543
1806
  id: "claude",
@@ -1551,17 +1814,17 @@ function detect2(projectDir) {
1551
1814
  const home = homedir3();
1552
1815
  const cwd = projectDir || process.cwd();
1553
1816
  return {
1554
- global: existsSync4(join3(home, ".claude")),
1555
- project: existsSync4(join3(cwd, ".claude"))
1817
+ global: existsSync5(join4(home, ".claude")),
1818
+ project: existsSync5(join4(cwd, ".claude"))
1556
1819
  };
1557
1820
  }
1558
1821
  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");
1822
+ const safeName = safeSlugName(slug);
1823
+ const base = scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
1561
1824
  if (contentType === "rule") {
1562
- return join3(base, "rules", `${safeName}.md`);
1825
+ return join4(base, "rules", `${safeName}.md`);
1563
1826
  }
1564
- return join3(base, "skills", safeName, "SKILL.md");
1827
+ return join4(base, "skills", safeName, "SKILL.md");
1565
1828
  }
1566
1829
  function transformContent2(content, skill) {
1567
1830
  if (skill.type === "rule") {
@@ -1571,32 +1834,21 @@ function transformContent2(content, skill) {
1571
1834
  }
1572
1835
  function install2(opts) {
1573
1836
  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;
1837
+ return installFileOrSymlink(opts, targetPath);
1582
1838
  }
1583
1839
  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, "..");
1840
+ uninstallFile(installation);
1841
+ const parentDir = join4(installation.path, "..");
1590
1842
  try {
1591
1843
  const { readdirSync: readdirSync3 } = __require("fs");
1592
- if (existsSync4(parentDir) && readdirSync3(parentDir).length === 0) {
1593
- rmSync3(parentDir, { recursive: true });
1844
+ if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
1845
+ rmSync2(parentDir, { recursive: true });
1594
1846
  }
1595
1847
  } catch {
1596
1848
  }
1597
1849
  }
1598
1850
  function defaultMethod2() {
1599
- return "symlink";
1851
+ return DEFAULT_METHOD;
1600
1852
  }
1601
1853
  var claudeAdapter = {
1602
1854
  descriptor: descriptor2,
@@ -1609,19 +1861,50 @@ var claudeAdapter = {
1609
1861
  };
1610
1862
 
1611
1863
  // src/lib/installers/codex.ts
1612
- import { join as join4 } from "path";
1864
+ import { join as join6 } from "path";
1865
+ import { homedir as homedir5 } from "os";
1866
+
1867
+ // src/lib/detect.ts
1868
+ import { existsSync as existsSync6 } from "fs";
1869
+ import { execFileSync } from "child_process";
1613
1870
  import { homedir as homedir4 } from "os";
1614
- import { execSync } from "child_process";
1871
+ import { join as join5 } from "path";
1872
+ function commandExists(cmd) {
1873
+ try {
1874
+ execFileSync("which", [cmd], { stdio: "ignore" });
1875
+ return true;
1876
+ } catch {
1877
+ return false;
1878
+ }
1879
+ }
1880
+ function detectInstalledPlatforms(projectDir) {
1881
+ const detected = [];
1882
+ const home = homedir4();
1883
+ const cwd = projectDir || process.cwd();
1884
+ if (existsSync6(join5(home, ".cursor")) || existsSync6(join5(cwd, ".cursor")))
1885
+ detected.push("cursor");
1886
+ if (existsSync6(join5(home, ".claude")) || commandExists("claude"))
1887
+ detected.push("claude");
1888
+ if (commandExists("codex")) detected.push("codex");
1889
+ if (existsSync6(join5(home, ".codeium")) || existsSync6(join5(cwd, ".windsurf")))
1890
+ detected.push("windsurf");
1891
+ if (existsSync6(join5(cwd, ".clinerules"))) detected.push("cline");
1892
+ if (existsSync6(join5(cwd, ".github"))) detected.push("copilot");
1893
+ if (commandExists("opencode") || existsSync6(join5(cwd, ".opencode")))
1894
+ detected.push("opencode");
1895
+ if (commandExists("aider")) detected.push("aider");
1896
+ return detected;
1897
+ }
1615
1898
 
1616
1899
  // src/lib/marked-sections.ts
1617
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
1900
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1618
1901
  import { dirname as dirname2 } from "path";
1619
1902
  var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
1620
1903
  var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
1621
1904
  function upsertSection(filePath, slug, content) {
1622
- mkdirSync5(dirname2(filePath), { recursive: true });
1905
+ mkdirSync4(dirname2(filePath), { recursive: true });
1623
1906
  let existing = "";
1624
- if (existsSync5(filePath)) {
1907
+ if (existsSync7(filePath)) {
1625
1908
  existing = readFileSync2(filePath, "utf-8");
1626
1909
  }
1627
1910
  const start = START_MARKER(slug);
@@ -1638,10 +1921,10 @@ ${end}`;
1638
1921
  const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
1639
1922
  result = existing + separator + section + "\n";
1640
1923
  }
1641
- writeFileSync4(filePath, result);
1924
+ writeFileSync3(filePath, result);
1642
1925
  }
1643
1926
  function removeSection(filePath, slug) {
1644
- if (!existsSync5(filePath)) return false;
1927
+ if (!existsSync7(filePath)) return false;
1645
1928
  const existing = readFileSync2(filePath, "utf-8");
1646
1929
  const start = START_MARKER(slug);
1647
1930
  const end = END_MARKER(slug);
@@ -1653,11 +1936,11 @@ function removeSection(filePath, slug) {
1653
1936
  while (before.endsWith("\n\n")) before = before.slice(0, -1);
1654
1937
  while (after.startsWith("\n\n")) after = after.slice(1);
1655
1938
  const result = (before + after).trim();
1656
- writeFileSync4(filePath, result ? result + "\n" : "");
1939
+ writeFileSync3(filePath, result ? result + "\n" : "");
1657
1940
  return true;
1658
1941
  }
1659
1942
  function listSections(filePath) {
1660
- if (!existsSync5(filePath)) return [];
1943
+ if (!existsSync7(filePath)) return [];
1661
1944
  const content = readFileSync2(filePath, "utf-8");
1662
1945
  const regex = /<!-- localskills:start:(.+?) -->/g;
1663
1946
  const slugs = [];
@@ -1678,22 +1961,17 @@ var descriptor3 = {
1678
1961
  fileExtension: ".md"
1679
1962
  };
1680
1963
  function detect3() {
1681
- let hasCommand = false;
1682
- try {
1683
- execSync("which codex", { stdio: "ignore" });
1684
- hasCommand = true;
1685
- } catch {
1686
- }
1964
+ const hasCommand = commandExists("codex");
1687
1965
  return { global: hasCommand, project: hasCommand };
1688
1966
  }
1689
1967
  function resolvePath3(slug, scope, projectDir, _contentType) {
1690
1968
  if (scope === "global") {
1691
- return join4(homedir4(), ".codex", "AGENTS.md");
1969
+ return join6(homedir5(), ".codex", "AGENTS.md");
1692
1970
  }
1693
- return join4(projectDir || process.cwd(), "AGENTS.md");
1971
+ return join6(projectDir || process.cwd(), "AGENTS.md");
1694
1972
  }
1695
1973
  function transformContent3(content) {
1696
- return toPlainMD(content);
1974
+ return defaultTransformContent(content);
1697
1975
  }
1698
1976
  function install3(opts) {
1699
1977
  const filePath = resolvePath3(opts.slug, opts.scope, opts.projectDir);
@@ -1719,9 +1997,9 @@ var codexAdapter = {
1719
1997
  };
1720
1998
 
1721
1999
  // 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";
2000
+ import { existsSync as existsSync8 } from "fs";
2001
+ import { join as join7 } from "path";
2002
+ import { homedir as homedir6 } from "os";
1725
2003
  var descriptor4 = {
1726
2004
  id: "windsurf",
1727
2005
  name: "Windsurf",
@@ -1732,22 +2010,22 @@ var descriptor4 = {
1732
2010
  fileExtension: ".md"
1733
2011
  };
1734
2012
  function detect4(projectDir) {
1735
- const home = homedir5();
2013
+ const home = homedir6();
1736
2014
  const cwd = projectDir || process.cwd();
1737
2015
  return {
1738
- global: existsSync6(join5(home, ".codeium")),
1739
- project: existsSync6(join5(cwd, ".windsurf"))
2016
+ global: existsSync8(join7(home, ".codeium")),
2017
+ project: existsSync8(join7(cwd, ".windsurf"))
1740
2018
  };
1741
2019
  }
1742
2020
  function resolvePath4(slug, scope, projectDir, _contentType) {
1743
- const safeName = slug.replace(/\//g, "-");
2021
+ const safeName = safeSlugName(slug);
1744
2022
  if (scope === "global") {
1745
- return join5(homedir5(), ".codeium", "windsurf", "memories", "global_rules.md");
2023
+ return join7(homedir6(), ".codeium", "windsurf", "memories", "global_rules.md");
1746
2024
  }
1747
- return join5(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
2025
+ return join7(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
1748
2026
  }
1749
2027
  function transformContent4(content) {
1750
- return toPlainMD(content);
2028
+ return defaultTransformContent(content);
1751
2029
  }
1752
2030
  function install4(opts) {
1753
2031
  const targetPath = resolvePath4(opts.slug, opts.scope, opts.projectDir);
@@ -1755,21 +2033,16 @@ function install4(opts) {
1755
2033
  upsertSection(targetPath, opts.slug, `## ${opts.slug}
1756
2034
 
1757
2035
  ${opts.content}`);
1758
- } else if (opts.method === "symlink") {
1759
- createSymlink(opts.cachePath, targetPath);
1760
2036
  } else {
1761
- mkdirSync6(join5(targetPath, ".."), { recursive: true });
1762
- writeFileSync5(targetPath, opts.content);
2037
+ installFileOrSymlink(opts, targetPath);
1763
2038
  }
1764
2039
  return targetPath;
1765
2040
  }
1766
2041
  function uninstall4(installation, slug) {
1767
2042
  if (installation.method === "section") {
1768
2043
  removeSection(installation.path, slug);
1769
- } else if (installation.method === "symlink") {
1770
- removeSymlink(installation.path);
1771
- } else if (existsSync6(installation.path)) {
1772
- unlinkSync4(installation.path);
2044
+ } else {
2045
+ uninstallFile(installation);
1773
2046
  }
1774
2047
  }
1775
2048
  function defaultMethod4(scope) {
@@ -1786,8 +2059,8 @@ var windsurfAdapter = {
1786
2059
  };
1787
2060
 
1788
2061
  // 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";
2062
+ import { existsSync as existsSync9 } from "fs";
2063
+ import { join as join8 } from "path";
1791
2064
  var descriptor5 = {
1792
2065
  id: "cline",
1793
2066
  name: "Cline",
@@ -1800,38 +2073,28 @@ function detect5(projectDir) {
1800
2073
  const cwd = projectDir || process.cwd();
1801
2074
  return {
1802
2075
  global: false,
1803
- project: existsSync7(join6(cwd, ".clinerules"))
2076
+ project: existsSync9(join8(cwd, ".clinerules"))
1804
2077
  };
1805
2078
  }
1806
2079
  function resolvePath5(slug, scope, projectDir, _contentType) {
1807
2080
  if (scope === "global") {
1808
2081
  throw new Error("Cline does not support global installation");
1809
2082
  }
1810
- const safeName = slug.replace(/\//g, "-");
1811
- return join6(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
2083
+ const safeName = safeSlugName(slug);
2084
+ return join8(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
1812
2085
  }
1813
2086
  function transformContent5(content) {
1814
- return toPlainMD(content);
2087
+ return defaultTransformContent(content);
1815
2088
  }
1816
2089
  function install5(opts) {
1817
2090
  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;
2091
+ return installFileOrSymlink(opts, targetPath);
1825
2092
  }
1826
2093
  function uninstall5(installation, _slug) {
1827
- if (installation.method === "symlink") {
1828
- removeSymlink(installation.path);
1829
- } else if (existsSync7(installation.path)) {
1830
- unlinkSync5(installation.path);
1831
- }
2094
+ uninstallFile(installation);
1832
2095
  }
1833
2096
  function defaultMethod5() {
1834
- return "symlink";
2097
+ return DEFAULT_METHOD;
1835
2098
  }
1836
2099
  var clineAdapter = {
1837
2100
  descriptor: descriptor5,
@@ -1844,8 +2107,8 @@ var clineAdapter = {
1844
2107
  };
1845
2108
 
1846
2109
  // src/lib/installers/copilot.ts
1847
- import { existsSync as existsSync8 } from "fs";
1848
- import { join as join7 } from "path";
2110
+ import { existsSync as existsSync10 } from "fs";
2111
+ import { join as join9 } from "path";
1849
2112
  var descriptor6 = {
1850
2113
  id: "copilot",
1851
2114
  name: "GitHub Copilot",
@@ -1858,17 +2121,17 @@ function detect6(projectDir) {
1858
2121
  const cwd = projectDir || process.cwd();
1859
2122
  return {
1860
2123
  global: false,
1861
- project: existsSync8(join7(cwd, ".github"))
2124
+ project: existsSync10(join9(cwd, ".github"))
1862
2125
  };
1863
2126
  }
1864
2127
  function resolvePath6(slug, scope, projectDir, _contentType) {
1865
2128
  if (scope === "global") {
1866
2129
  throw new Error("GitHub Copilot does not support global installation");
1867
2130
  }
1868
- return join7(projectDir || process.cwd(), ".github", "copilot-instructions.md");
2131
+ return join9(projectDir || process.cwd(), ".github", "copilot-instructions.md");
1869
2132
  }
1870
2133
  function transformContent6(content) {
1871
- return toPlainMD(content);
2134
+ return defaultTransformContent(content);
1872
2135
  }
1873
2136
  function install6(opts) {
1874
2137
  const filePath = resolvePath6(opts.slug, opts.scope, opts.projectDir);
@@ -1894,10 +2157,9 @@ var copilotAdapter = {
1894
2157
  };
1895
2158
 
1896
2159
  // 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";
2160
+ import { existsSync as existsSync11 } from "fs";
2161
+ import { join as join10 } from "path";
2162
+ import { homedir as homedir7 } from "os";
1901
2163
  var descriptor7 = {
1902
2164
  id: "opencode",
1903
2165
  name: "OpenCode",
@@ -1908,46 +2170,31 @@ var descriptor7 = {
1908
2170
  };
1909
2171
  function detect7(projectDir) {
1910
2172
  const cwd = projectDir || process.cwd();
1911
- let hasCommand = false;
1912
- try {
1913
- execSync2("which opencode", { stdio: "ignore" });
1914
- hasCommand = true;
1915
- } catch {
1916
- }
2173
+ const hasCommand = commandExists("opencode");
1917
2174
  return {
1918
2175
  global: hasCommand,
1919
- project: hasCommand || existsSync9(join8(cwd, ".opencode"))
2176
+ project: hasCommand || existsSync11(join10(cwd, ".opencode"))
1920
2177
  };
1921
2178
  }
1922
2179
  function resolvePath7(slug, scope, projectDir, _contentType) {
1923
- const safeName = slug.replace(/\//g, "-");
2180
+ const safeName = safeSlugName(slug);
1924
2181
  if (scope === "global") {
1925
- return join8(homedir6(), ".config", "opencode", "rules", `${safeName}.md`);
2182
+ return join10(homedir7(), ".config", "opencode", "rules", `${safeName}.md`);
1926
2183
  }
1927
- return join8(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
2184
+ return join10(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
1928
2185
  }
1929
2186
  function transformContent7(content) {
1930
- return toPlainMD(content);
2187
+ return defaultTransformContent(content);
1931
2188
  }
1932
2189
  function install7(opts) {
1933
2190
  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;
2191
+ return installFileOrSymlink(opts, targetPath);
1941
2192
  }
1942
2193
  function uninstall7(installation, _slug) {
1943
- if (installation.method === "symlink") {
1944
- removeSymlink(installation.path);
1945
- } else if (existsSync9(installation.path)) {
1946
- unlinkSync6(installation.path);
1947
- }
2194
+ uninstallFile(installation);
1948
2195
  }
1949
2196
  function defaultMethod7() {
1950
- return "symlink";
2197
+ return DEFAULT_METHOD;
1951
2198
  }
1952
2199
  var opencodeAdapter = {
1953
2200
  descriptor: descriptor7,
@@ -1960,9 +2207,8 @@ var opencodeAdapter = {
1960
2207
  };
1961
2208
 
1962
2209
  // 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";
2210
+ import { existsSync as existsSync12, writeFileSync as writeFileSync4, readFileSync as readFileSync3 } from "fs";
2211
+ import { join as join11 } from "path";
1966
2212
  var descriptor8 = {
1967
2213
  id: "aider",
1968
2214
  name: "Aider",
@@ -1972,29 +2218,24 @@ var descriptor8 = {
1972
2218
  fileExtension: ".md"
1973
2219
  };
1974
2220
  function detect8() {
1975
- let hasCommand = false;
1976
- try {
1977
- execSync3("which aider", { stdio: "ignore" });
1978
- hasCommand = true;
1979
- } catch {
1980
- }
2221
+ const hasCommand = commandExists("aider");
1981
2222
  return { global: false, project: hasCommand };
1982
2223
  }
1983
2224
  function resolvePath8(slug, scope, projectDir, contentType) {
1984
2225
  if (scope === "global") {
1985
2226
  throw new Error("Aider does not support global installation");
1986
2227
  }
1987
- const safeName = slug.replace(/\//g, "-");
2228
+ const safeName = safeSlugName(slug);
1988
2229
  const subdir = contentType === "rule" ? "rules" : "skills";
1989
- return join9(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
2230
+ return join11(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
1990
2231
  }
1991
2232
  function transformContent8(content) {
1992
- return toPlainMD(content);
2233
+ return defaultTransformContent(content);
1993
2234
  }
1994
2235
  function addAiderRead(projectDir, relativePath) {
1995
- const configPath = join9(projectDir, ".aider.conf.yml");
2236
+ const configPath = join11(projectDir, ".aider.conf.yml");
1996
2237
  let content = "";
1997
- if (existsSync10(configPath)) {
2238
+ if (existsSync12(configPath)) {
1998
2239
  content = readFileSync3(configPath, "utf-8");
1999
2240
  }
2000
2241
  if (content.includes(relativePath)) return;
@@ -2004,44 +2245,35 @@ function addAiderRead(projectDir, relativePath) {
2004
2245
  } else {
2005
2246
  content = content.trimEnd() + (content ? "\n" : "") + readLine + "\n";
2006
2247
  }
2007
- writeFileSync8(configPath, content);
2248
+ writeFileSync4(configPath, content);
2008
2249
  }
2009
2250
  function removeAiderRead(projectDir, relativePath) {
2010
- const configPath = join9(projectDir, ".aider.conf.yml");
2011
- if (!existsSync10(configPath)) return;
2251
+ const configPath = join11(projectDir, ".aider.conf.yml");
2252
+ if (!existsSync12(configPath)) return;
2012
2253
  let content = readFileSync3(configPath, "utf-8");
2013
2254
  const lines = content.split("\n");
2014
2255
  const filtered = lines.filter((line) => !line.includes(relativePath));
2015
- writeFileSync8(configPath, filtered.join("\n"));
2256
+ writeFileSync4(configPath, filtered.join("\n"));
2016
2257
  }
2017
2258
  function install8(opts) {
2018
2259
  const targetPath = resolvePath8(opts.slug, opts.scope, opts.projectDir, opts.contentType);
2019
2260
  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, "-");
2261
+ installFileOrSymlink(opts, targetPath);
2262
+ const safeName = safeSlugName(opts.slug);
2027
2263
  const subdir = opts.contentType === "rule" ? "rules" : "skills";
2028
2264
  const relativePath = `.aider/${subdir}/${safeName}.md`;
2029
2265
  addAiderRead(projectDir, relativePath);
2030
2266
  return targetPath;
2031
2267
  }
2032
2268
  function uninstall8(installation, slug) {
2033
- if (installation.method === "symlink") {
2034
- removeSymlink(installation.path);
2035
- } else if (existsSync10(installation.path)) {
2036
- unlinkSync7(installation.path);
2037
- }
2269
+ uninstallFile(installation);
2038
2270
  const projectDir = installation.projectDir || process.cwd();
2039
- const safeName = slug.replace(/\//g, "-");
2271
+ const safeName = safeSlugName(slug);
2040
2272
  removeAiderRead(projectDir, `.aider/skills/${safeName}.md`);
2041
2273
  removeAiderRead(projectDir, `.aider/rules/${safeName}.md`);
2042
2274
  }
2043
2275
  function defaultMethod8() {
2044
- return "symlink";
2276
+ return DEFAULT_METHOD;
2045
2277
  }
2046
2278
  var aiderAdapter = {
2047
2279
  descriptor: descriptor8,
@@ -2074,7 +2306,7 @@ function getAllAdapters() {
2074
2306
  }
2075
2307
 
2076
2308
  // src/lib/cache.ts
2077
- var CACHE_DIR = join10(homedir7(), ".localskills", "cache");
2309
+ var CACHE_DIR = join12(homedir8(), ".localskills", "cache");
2078
2310
  function slugToDir(slug) {
2079
2311
  if (slug.includes("..") || slug.includes("\0")) {
2080
2312
  throw new Error("Invalid slug: contains forbidden characters");
@@ -2082,7 +2314,7 @@ function slugToDir(slug) {
2082
2314
  return slug.replace(/\//g, "--");
2083
2315
  }
2084
2316
  function getCacheDir(slug) {
2085
- const dir = resolve2(join10(CACHE_DIR, slugToDir(slug)));
2317
+ const dir = resolve2(join12(CACHE_DIR, slugToDir(slug)));
2086
2318
  if (!dir.startsWith(resolve2(CACHE_DIR) + "/") && dir !== resolve2(CACHE_DIR)) {
2087
2319
  throw new Error("Invalid slug: path traversal detected");
2088
2320
  }
@@ -2090,17 +2322,18 @@ function getCacheDir(slug) {
2090
2322
  }
2091
2323
  function store(slug, content, skill, version) {
2092
2324
  const dir = getCacheDir(slug);
2093
- mkdirSync10(dir, { recursive: true });
2094
- writeFileSync9(join10(dir, "raw.md"), content);
2325
+ mkdirSync5(dir, { recursive: true });
2326
+ writeFileSync5(join12(dir, "raw.md"), content);
2095
2327
  const meta = {
2096
2328
  hash: skill.contentHash,
2097
2329
  version,
2330
+ semver: skill.currentSemver ?? null,
2098
2331
  name: skill.name,
2099
2332
  description: skill.description,
2100
2333
  type: skill.type ?? "skill",
2101
2334
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2102
2335
  };
2103
- writeFileSync9(join10(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2336
+ writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2104
2337
  clearPlatformFiles(slug);
2105
2338
  }
2106
2339
  function getPlatformFile(slug, platform, skill) {
@@ -2111,98 +2344,103 @@ function getPlatformFile(slug, platform, skill) {
2111
2344
  const transformed = adapter.transformContent(raw, skill);
2112
2345
  if (platform === "claude") {
2113
2346
  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);
2347
+ const claudeRuleDir = join12(dir, "claude-rule");
2348
+ mkdirSync5(claudeRuleDir, { recursive: true });
2349
+ const filePath3 = join12(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
2350
+ writeFileSync5(filePath3, transformed);
2118
2351
  return filePath3;
2119
2352
  }
2120
- const claudeDir = join10(dir, "claude");
2121
- mkdirSync10(claudeDir, { recursive: true });
2122
- const filePath2 = join10(claudeDir, "SKILL.md");
2123
- writeFileSync9(filePath2, transformed);
2353
+ const claudeDir = join12(dir, "claude");
2354
+ mkdirSync5(claudeDir, { recursive: true });
2355
+ const filePath2 = join12(claudeDir, "SKILL.md");
2356
+ writeFileSync5(filePath2, transformed);
2124
2357
  return filePath2;
2125
2358
  }
2126
2359
  const ext = adapter.descriptor.fileExtension;
2127
- const filePath = join10(dir, `${platform}${ext}`);
2128
- writeFileSync9(filePath, transformed);
2360
+ const filePath = join12(dir, `${platform}${ext}`);
2361
+ writeFileSync5(filePath, transformed);
2129
2362
  return filePath;
2130
2363
  }
2131
2364
  function getRawContent(slug) {
2132
- const filePath = join10(getCacheDir(slug), "raw.md");
2133
- if (!existsSync11(filePath)) return null;
2365
+ const filePath = join12(getCacheDir(slug), "raw.md");
2366
+ if (!existsSync13(filePath)) return null;
2134
2367
  return readFileSync4(filePath, "utf-8");
2135
2368
  }
2136
2369
  function purge(slug) {
2137
2370
  const dir = getCacheDir(slug);
2138
- if (existsSync11(dir)) {
2139
- rmSync4(dir, { recursive: true, force: true });
2371
+ if (existsSync13(dir)) {
2372
+ rmSync3(dir, { recursive: true, force: true });
2140
2373
  }
2141
2374
  }
2375
+ function storePackage(slug, zipBuffer, manifest, skill, version) {
2376
+ const dir = getCacheDir(slug);
2377
+ mkdirSync5(dir, { recursive: true });
2378
+ writeFileSync5(join12(dir, "package.zip"), zipBuffer);
2379
+ writeFileSync5(join12(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
2380
+ const meta = {
2381
+ hash: skill.contentHash,
2382
+ version,
2383
+ semver: skill.currentSemver ?? null,
2384
+ name: skill.name,
2385
+ description: skill.description,
2386
+ type: skill.type ?? "skill",
2387
+ format: "package",
2388
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2389
+ };
2390
+ writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
2391
+ }
2142
2392
  function clearPlatformFiles(slug) {
2143
2393
  const dir = getCacheDir(slug);
2144
- if (!existsSync11(dir)) return;
2145
- const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json"]);
2394
+ if (!existsSync13(dir)) return;
2395
+ const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "package.zip", "manifest.json"]);
2146
2396
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2147
2397
  if (!keep.has(entry.name)) {
2148
- rmSync4(join10(dir, entry.name), { recursive: true, force: true });
2398
+ rmSync3(join12(dir, entry.name), { recursive: true, force: true });
2149
2399
  }
2150
2400
  }
2151
2401
  }
2152
2402
 
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;
2403
+ // src/lib/extract.ts
2404
+ import { unzipSync } from "fflate";
2405
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
2406
+ import { join as join13, dirname as dirname3, resolve as resolve3 } from "path";
2407
+ function extractPackage(zipBuffer, targetDir) {
2408
+ const resolvedTarget = resolve3(targetDir);
2409
+ const extracted = unzipSync(new Uint8Array(zipBuffer));
2410
+ const writtenFiles = [];
2411
+ for (const [path, data] of Object.entries(extracted)) {
2412
+ if (path.endsWith("/")) continue;
2413
+ if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
2414
+ continue;
2415
+ }
2416
+ const fullPath = resolve3(join13(targetDir, path));
2417
+ if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
2418
+ continue;
2419
+ }
2420
+ mkdirSync6(dirname3(fullPath), { recursive: true });
2421
+ writeFileSync6(fullPath, Buffer.from(data));
2422
+ writtenFiles.push(path);
2164
2423
  }
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;
2424
+ return writtenFiles;
2183
2425
  }
2184
2426
 
2185
2427
  // src/lib/interactive.ts
2186
2428
  async function interactiveInstall(availableSkills, detectedPlatforms) {
2187
2429
  We("localskills install");
2188
- const slug = await Je({
2430
+ const slug = cancelGuard(await Je({
2189
2431
  message: "Which skill would you like to install?",
2190
2432
  options: availableSkills.map((s) => ({
2191
2433
  value: s.slug,
2192
2434
  label: s.name,
2193
2435
  hint: truncate(s.description, 60)
2194
2436
  }))
2195
- });
2196
- if (Ct(slug)) {
2197
- Ne("Cancelled.");
2198
- process.exit(0);
2199
- }
2437
+ }));
2200
2438
  const rest = await interactiveTargets(detectedPlatforms);
2201
2439
  return { slug, ...rest };
2202
2440
  }
2203
2441
  async function interactiveTargets(detectedPlatforms) {
2204
2442
  const allAdapters = getAllAdapters();
2205
- const platforms = await je({
2443
+ const platforms = cancelGuard(await je({
2206
2444
  message: "Which platforms should receive this skill?",
2207
2445
  options: allAdapters.map((a) => ({
2208
2446
  value: a.descriptor.id,
@@ -2211,12 +2449,8 @@ async function interactiveTargets(detectedPlatforms) {
2211
2449
  })),
2212
2450
  initialValues: detectedPlatforms.length > 0 ? detectedPlatforms : void 0,
2213
2451
  required: true
2214
- });
2215
- if (Ct(platforms)) {
2216
- Ne("Cancelled.");
2217
- process.exit(0);
2218
- }
2219
- const scope = await Je({
2452
+ }));
2453
+ const scope = cancelGuard(await Je({
2220
2454
  message: "Install scope?",
2221
2455
  options: [
2222
2456
  {
@@ -2231,12 +2465,8 @@ async function interactiveTargets(detectedPlatforms) {
2231
2465
  }
2232
2466
  ],
2233
2467
  initialValue: "project"
2234
- });
2235
- if (Ct(scope)) {
2236
- Ne("Cancelled.");
2237
- process.exit(0);
2238
- }
2239
- const method = await Je({
2468
+ }));
2469
+ const method = cancelGuard(await Je({
2240
2470
  message: "Install method?",
2241
2471
  options: [
2242
2472
  {
@@ -2251,31 +2481,18 @@ async function interactiveTargets(detectedPlatforms) {
2251
2481
  }
2252
2482
  ],
2253
2483
  initialValue: "symlink"
2254
- });
2255
- if (Ct(method)) {
2256
- Ne("Cancelled.");
2257
- process.exit(0);
2258
- }
2259
- return {
2260
- platforms,
2261
- scope,
2262
- method
2263
- };
2484
+ }));
2485
+ return { platforms, scope, method };
2264
2486
  }
2265
2487
  async function interactiveUninstall(installedSlugs) {
2266
2488
  We("localskills uninstall");
2267
- const slug = await Je({
2489
+ return cancelGuard(await Je({
2268
2490
  message: "Which skill would you like to uninstall?",
2269
2491
  options: installedSlugs.map((s) => ({
2270
2492
  value: s,
2271
2493
  label: s
2272
2494
  }))
2273
- });
2274
- if (Ct(slug)) {
2275
- Ne("Cancelled.");
2276
- process.exit(0);
2277
- }
2278
- return slug;
2495
+ }));
2279
2496
  }
2280
2497
  function truncate(str, max) {
2281
2498
  if (str.length <= max) return str;
@@ -2311,6 +2528,19 @@ function parsePlatforms(raw) {
2311
2528
  }
2312
2529
  return platforms;
2313
2530
  }
2531
+ function buildSkillRecord(cacheKey, skill, version, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
2532
+ return {
2533
+ slug: cacheKey,
2534
+ name: skill.name,
2535
+ type: skill.type ?? "skill",
2536
+ hash: skill.contentHash,
2537
+ version,
2538
+ semver: resolvedSemver ?? null,
2539
+ semverRange: requestedRange ?? null,
2540
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
2541
+ installations: existingInstallations ? [...existingInstallations, ...newInstallations] : newInstallations
2542
+ };
2543
+ }
2314
2544
  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
2545
  async (slugArg, opts) => {
2316
2546
  const client = new ApiClient();
@@ -2363,10 +2593,17 @@ var installCommand = new Command2("install").description("Install a skill locall
2363
2593
  method = explicitMethod || answers.method;
2364
2594
  }
2365
2595
  }
2596
+ let requestedRange = null;
2597
+ if (slug.includes("@")) {
2598
+ const atIdx = slug.lastIndexOf("@");
2599
+ requestedRange = slug.substring(atIdx + 1);
2600
+ slug = slug.substring(0, atIdx);
2601
+ }
2602
+ const versionQuery = buildVersionQuery(requestedRange);
2366
2603
  const spinner = bt2();
2367
2604
  spinner.start(`Fetching ${slug}...`);
2368
2605
  const res = await client.get(
2369
- `/api/skills/${encodeURIComponent(slug)}/content`
2606
+ `/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
2370
2607
  );
2371
2608
  if (!res.success || !res.data) {
2372
2609
  spinner.stop("Failed.");
@@ -2378,9 +2615,62 @@ var installCommand = new Command2("install").description("Install a skill locall
2378
2615
  process.exit(1);
2379
2616
  return;
2380
2617
  }
2381
- const { skill, content, version } = res.data;
2382
- spinner.stop(`Fetched ${skill.name} v${version}`);
2383
- const cacheKey = skill.publicId || slug;
2618
+ const resData = res.data;
2619
+ const format = resData.format ?? "text";
2620
+ const cacheKey = resData.skill.publicId || slug;
2621
+ if (format === "package") {
2622
+ const { skill: skill2, downloadUrl, manifest, version: version2, semver: resolvedSemver2 } = resData;
2623
+ spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version2)} (package, ${manifest.files.length} files)`);
2624
+ const dlSpinner = bt2();
2625
+ dlSpinner.start("Downloading package...");
2626
+ const zipBuffer = await client.fetchBinary(downloadUrl);
2627
+ dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
2628
+ storePackage(cacheKey, zipBuffer, manifest, skill2, version2);
2629
+ const installations2 = [];
2630
+ const results2 = [];
2631
+ for (const platformId of platforms) {
2632
+ const adapter = getAdapter(platformId);
2633
+ const desc = adapter.descriptor;
2634
+ if (scope === "global" && !desc.supportsGlobal) {
2635
+ R2.warn(`${desc.name} does not support global \u2014 skipping.`);
2636
+ continue;
2637
+ }
2638
+ if (scope === "project" && !desc.supportsProject) {
2639
+ R2.warn(`${desc.name} does not support project \u2014 skipping.`);
2640
+ continue;
2641
+ }
2642
+ const targetPath = adapter.resolvePath(cacheKey, scope, projectDir, skill2.type ?? "skill");
2643
+ mkdirSync7(targetPath, { recursive: true });
2644
+ const written = extractPackage(zipBuffer, targetPath);
2645
+ const installation = {
2646
+ platform: platformId,
2647
+ scope,
2648
+ method: "copy",
2649
+ path: targetPath,
2650
+ projectDir,
2651
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2652
+ };
2653
+ installations2.push(installation);
2654
+ results2.push(`${desc.name} \u2192 ${targetPath} (${written.length} files extracted)`);
2655
+ }
2656
+ config.installed_skills[cacheKey] = buildSkillRecord(
2657
+ cacheKey,
2658
+ skill2,
2659
+ version2,
2660
+ resolvedSemver2,
2661
+ requestedRange,
2662
+ config.installed_skills[cacheKey]?.installations,
2663
+ installations2
2664
+ );
2665
+ saveConfig(config);
2666
+ for (const r of results2) {
2667
+ R2.success(r);
2668
+ }
2669
+ Le(`Done! Installed to ${installations2.length} target(s).`);
2670
+ return;
2671
+ }
2672
+ const { skill, content, version, semver: resolvedSemver } = resData;
2673
+ spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version)}`);
2384
2674
  store(cacheKey, content, skill, version);
2385
2675
  const contentType = skill.type ?? "skill";
2386
2676
  const installations = [];
@@ -2420,17 +2710,15 @@ var installCommand = new Command2("install").description("Install a skill locall
2420
2710
  const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
2421
2711
  results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
2422
2712
  }
2423
- const existing = config.installed_skills[cacheKey];
2424
- const skillRecord = {
2425
- slug: cacheKey,
2426
- name: skill.name,
2427
- type: contentType,
2428
- hash: skill.contentHash,
2713
+ config.installed_skills[cacheKey] = buildSkillRecord(
2714
+ cacheKey,
2715
+ skill,
2429
2716
  version,
2430
- cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
2431
- installations: existing ? [...existing.installations, ...installations] : installations
2432
- };
2433
- config.installed_skills[cacheKey] = skillRecord;
2717
+ resolvedSemver,
2718
+ requestedRange,
2719
+ config.installed_skills[cacheKey]?.installations,
2720
+ installations
2721
+ );
2434
2722
  saveConfig(config);
2435
2723
  for (const r of results) {
2436
2724
  R2.success(r);
@@ -2489,33 +2777,62 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
2489
2777
 
2490
2778
  // src/commands/list.ts
2491
2779
  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) => {
2780
+ 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
2781
  const client = new ApiClient();
2494
- if (!client.isAuthenticated()) {
2495
- console.error("Not authenticated. Run `localskills login` first.");
2782
+ if ((opts.tag || opts.search) && !opts.public) {
2783
+ console.error("The --tag and --search flags require --public.");
2496
2784
  process.exit(1);
2497
2785
  }
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(`
2786
+ if (opts.public) {
2787
+ const params = new URLSearchParams();
2788
+ if (opts.tag) params.set("tag", opts.tag);
2789
+ if (opts.search) params.set("q", opts.search);
2790
+ const qs = params.toString();
2791
+ const path = qs ? `/api/explore?${qs}` : "/api/explore";
2792
+ const res = await client.get(path);
2793
+ if (!res.success || !res.data) {
2794
+ console.error(`Error: ${res.error || "Failed to fetch skills"}`);
2795
+ process.exit(1);
2796
+ return;
2797
+ }
2798
+ if (res.data.length === 0) {
2799
+ console.log("No skills found.");
2800
+ return;
2801
+ }
2802
+ console.log("Public skills:\n");
2803
+ for (const skill of res.data) {
2804
+ const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
2805
+ const tags = skill.tags.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
2806
+ console.log(` ${skill.slug} ${ver}${tags} \u2014 ${skill.description || skill.name}`);
2807
+ }
2808
+ console.log(`
2515
2809
  ${res.data.length} skill(s) found.`);
2810
+ } else {
2811
+ requireAuth(client);
2812
+ const res = await client.get("/api/skills");
2813
+ if (!res.success || !res.data) {
2814
+ console.error(`Error: ${res.error || "Failed to fetch skills"}`);
2815
+ process.exit(1);
2816
+ return;
2817
+ }
2818
+ if (res.data.length === 0) {
2819
+ console.log("No skills found.");
2820
+ return;
2821
+ }
2822
+ console.log("Available skills:\n");
2823
+ for (const skill of res.data) {
2824
+ const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
2825
+ const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
2826
+ const tags = skill.tags?.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
2827
+ console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${skill.description || skill.name}`);
2828
+ }
2829
+ console.log(`
2830
+ ${res.data.length} skill(s) found.`);
2831
+ }
2516
2832
  });
2517
2833
 
2518
2834
  // src/commands/pull.ts
2835
+ import { mkdirSync as mkdirSync8 } from "fs";
2519
2836
  import { Command as Command5 } from "commander";
2520
2837
  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
2838
  const config = loadConfig();
@@ -2535,53 +2852,70 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
2535
2852
  R2.warn(`${slug} \u2014 not found in config, skipping.`);
2536
2853
  continue;
2537
2854
  }
2855
+ const versionQuery = buildVersionQuery(installed.semverRange);
2538
2856
  spinner.start(`Checking ${slug}...`);
2539
2857
  const res = await client.get(
2540
- `/api/skills/${encodeURIComponent(slug)}/content`
2858
+ `/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
2541
2859
  );
2542
2860
  if (!res.success || !res.data) {
2543
2861
  spinner.stop(`${slug} \u2014 failed: ${res.error || "not found"}`);
2544
2862
  continue;
2545
2863
  }
2546
- const { skill, content, version } = res.data;
2864
+ const resData = res.data;
2865
+ const format = resData.format ?? "text";
2866
+ const { skill, version } = resData;
2547
2867
  if (skill.contentHash === installed.hash) {
2548
2868
  spinner.stop(`${slug} \u2014 up to date`);
2549
2869
  skipped++;
2550
2870
  continue;
2551
2871
  }
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;
2872
+ if (format === "package") {
2873
+ const { downloadUrl, manifest } = resData;
2874
+ const zipBuffer = await client.fetchBinary(downloadUrl);
2875
+ storePackage(slug, zipBuffer, manifest, skill, version);
2876
+ for (const installation of installed.installations) {
2877
+ const adapter = getAdapter(installation.platform);
2878
+ const targetPath = adapter.resolvePath(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
2879
+ mkdirSync8(targetPath, { recursive: true });
2880
+ extractPackage(zipBuffer, targetPath);
2557
2881
  }
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}
2882
+ } else {
2883
+ const { content } = resData;
2884
+ store(slug, content, skill, version);
2885
+ for (const installation of installed.installations) {
2886
+ if (installation.method === "symlink") {
2887
+ getPlatformFile(slug, installation.platform, skill);
2888
+ continue;
2889
+ }
2890
+ const adapter = getAdapter(installation.platform);
2891
+ const transformed = adapter.transformContent(content, skill);
2892
+ if (installation.method === "section") {
2893
+ upsertSection(
2894
+ installation.path,
2895
+ slug,
2896
+ `## ${slug}
2565
2897
 
2566
2898
  ${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
- });
2899
+ );
2900
+ } else {
2901
+ const cachePath = getPlatformFile(slug, installation.platform, skill);
2902
+ adapter.install({
2903
+ slug,
2904
+ content: transformed,
2905
+ scope: installation.scope,
2906
+ method: "copy",
2907
+ cachePath,
2908
+ projectDir: installation.projectDir
2909
+ });
2910
+ }
2578
2911
  }
2579
2912
  }
2580
2913
  installed.hash = skill.contentHash;
2581
2914
  installed.version = version;
2915
+ installed.semver = resData.semver ?? null;
2582
2916
  installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
2583
2917
  updated++;
2584
- spinner.stop(`${slug} \u2014 updated to v${version}`);
2918
+ spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version)}`);
2585
2919
  }
2586
2920
  saveConfig(config);
2587
2921
  Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
@@ -2589,44 +2923,44 @@ ${transformed}`
2589
2923
 
2590
2924
  // src/commands/publish.ts
2591
2925
  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";
2926
+ import { readFileSync as readFileSync6, existsSync as existsSync15 } from "fs";
2927
+ import { resolve as resolve4, basename as basename2, extname as extname2 } from "path";
2594
2928
  import { homedir as homedir10 } from "os";
2595
2929
 
2596
2930
  // 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";
2931
+ import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
2932
+ import { join as join14, basename, extname } from "path";
2599
2933
  import { homedir as homedir9 } from "os";
2600
- import { readlinkSync as readlinkSync2, lstatSync as lstatSync2 } from "fs";
2934
+ import { readlinkSync, lstatSync as lstatSync2 } from "fs";
2601
2935
  function scanForSkills(projectDir) {
2602
2936
  const home = homedir9();
2603
2937
  const cwd = projectDir || process.cwd();
2604
2938
  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);
2939
+ scanDirectory(join14(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
2940
+ scanDirectory(join14(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
2941
+ scanClaudeSkills(join14(home, ".claude", "skills"), "global", results);
2942
+ scanClaudeSkills(join14(cwd, ".claude", "skills"), "project", results);
2943
+ scanDirectory(join14(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
2944
+ scanDirectory(join14(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
2945
+ scanSingleFile(join14(home, ".codex", "AGENTS.md"), "codex", "global", results);
2946
+ scanSingleFile(join14(cwd, "AGENTS.md"), "codex", "project", results);
2613
2947
  scanSingleFile(
2614
- join12(home, ".codeium", "windsurf", "memories", "global_rules.md"),
2948
+ join14(home, ".codeium", "windsurf", "memories", "global_rules.md"),
2615
2949
  "windsurf",
2616
2950
  "global",
2617
2951
  results
2618
2952
  );
2619
- scanDirectory(join12(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
2620
- scanDirectory(join12(cwd, ".clinerules"), ".md", "cline", "project", results);
2953
+ scanDirectory(join14(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
2954
+ scanDirectory(join14(cwd, ".clinerules"), ".md", "cline", "project", results);
2621
2955
  scanSingleFile(
2622
- join12(cwd, ".github", "copilot-instructions.md"),
2956
+ join14(cwd, ".github", "copilot-instructions.md"),
2623
2957
  "copilot",
2624
2958
  "project",
2625
2959
  results
2626
2960
  );
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);
2961
+ scanDirectory(join14(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
2962
+ scanDirectory(join14(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
2963
+ scanDirectory(join14(cwd, ".aider", "skills"), ".md", "aider", "project", results);
2630
2964
  return results;
2631
2965
  }
2632
2966
  function filterTracked(detected, config) {
@@ -2636,13 +2970,13 @@ function filterTracked(detected, config) {
2636
2970
  trackedPaths.add(inst.path);
2637
2971
  }
2638
2972
  }
2639
- const cacheDir = join12(homedir9(), ".localskills", "cache");
2973
+ const cacheDir = join14(homedir9(), ".localskills", "cache");
2640
2974
  return detected.filter((skill) => {
2641
2975
  if (trackedPaths.has(skill.filePath)) return false;
2642
2976
  try {
2643
2977
  const stat = lstatSync2(skill.filePath);
2644
2978
  if (stat.isSymbolicLink()) {
2645
- const target = readlinkSync2(skill.filePath);
2979
+ const target = readlinkSync(skill.filePath);
2646
2980
  if (target.startsWith(cacheDir)) return false;
2647
2981
  }
2648
2982
  } catch {
@@ -2653,11 +2987,9 @@ function filterTracked(detected, config) {
2653
2987
  function slugFromFilename(filename) {
2654
2988
  return basename(filename, extname(filename));
2655
2989
  }
2656
- function nameFromSlug(slug) {
2657
- return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2658
- }
2990
+ var nameFromSlug = titleFromSlug;
2659
2991
  function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
2660
- if (!existsSync13(dir)) return;
2992
+ if (!existsSync14(dir)) return;
2661
2993
  let entries;
2662
2994
  try {
2663
2995
  entries = readdirSync2(dir);
@@ -2666,7 +2998,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
2666
2998
  }
2667
2999
  for (const entry of entries) {
2668
3000
  if (!entry.endsWith(ext)) continue;
2669
- const filePath = join12(dir, entry);
3001
+ const filePath = join14(dir, entry);
2670
3002
  try {
2671
3003
  const raw = readFileSync5(filePath, "utf-8");
2672
3004
  const content = stripFrontmatter(raw).trim();
@@ -2686,7 +3018,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
2686
3018
  }
2687
3019
  }
2688
3020
  function scanClaudeSkills(skillsDir, scope, results) {
2689
- if (!existsSync13(skillsDir)) return;
3021
+ if (!existsSync14(skillsDir)) return;
2690
3022
  let entries;
2691
3023
  try {
2692
3024
  entries = readdirSync2(skillsDir);
@@ -2694,8 +3026,8 @@ function scanClaudeSkills(skillsDir, scope, results) {
2694
3026
  return;
2695
3027
  }
2696
3028
  for (const entry of entries) {
2697
- const skillFile = join12(skillsDir, entry, "SKILL.md");
2698
- if (!existsSync13(skillFile)) continue;
3029
+ const skillFile = join14(skillsDir, entry, "SKILL.md");
3030
+ if (!existsSync14(skillFile)) continue;
2699
3031
  try {
2700
3032
  const raw = readFileSync5(skillFile, "utf-8");
2701
3033
  const content = stripFrontmatter(raw).trim();
@@ -2714,7 +3046,7 @@ function scanClaudeSkills(skillsDir, scope, results) {
2714
3046
  }
2715
3047
  }
2716
3048
  function scanSingleFile(filePath, platform, scope, results) {
2717
- if (!existsSync13(filePath)) return;
3049
+ if (!existsSync14(filePath)) return;
2718
3050
  let raw;
2719
3051
  try {
2720
3052
  raw = readFileSync5(filePath, "utf-8");
@@ -2765,10 +3097,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2765
3097
  ).option("--type <type>", "Content type: skill or rule", "skill").option("-m, --message <message>", "Version message").action(
2766
3098
  async (fileArg, opts) => {
2767
3099
  const client = new ApiClient();
2768
- if (!client.isAuthenticated()) {
2769
- console.error("Not authenticated. Run `localskills login` first.");
2770
- process.exit(1);
2771
- }
3100
+ requireAuth(client);
2772
3101
  const teamsRes = await client.get("/api/tenants");
2773
3102
  if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
2774
3103
  console.error(
@@ -2779,8 +3108,8 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2779
3108
  }
2780
3109
  const teams = teamsRes.data;
2781
3110
  if (fileArg) {
2782
- const filePath = resolve3(fileArg);
2783
- if (!existsSync14(filePath)) {
3111
+ const filePath = resolve4(fileArg);
3112
+ if (!existsSync15(filePath)) {
2784
3113
  console.error(`File not found: ${filePath}`);
2785
3114
  process.exit(1);
2786
3115
  return;
@@ -2793,7 +3122,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2793
3122
  return;
2794
3123
  }
2795
3124
  const defaultSlug = basename2(filePath, extname2(filePath));
2796
- const defaultName = defaultSlug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3125
+ const defaultName = titleFromSlug(defaultSlug);
2797
3126
  const skillName = opts.name || defaultName;
2798
3127
  const contentType = validateContentType(opts.type || "skill");
2799
3128
  const visibility = validateVisibility(opts.visibility || "private");
@@ -2819,7 +3148,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2819
3148
  Le("Nothing to publish.");
2820
3149
  return;
2821
3150
  }
2822
- const selected = await je({
3151
+ const skills = cancelGuard(await je({
2823
3152
  message: "Select items to publish",
2824
3153
  options: detected.map((s) => ({
2825
3154
  value: s,
@@ -2827,28 +3156,19 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2827
3156
  hint: `${s.platform}/${s.scope}/${s.contentType} ${shortenPath(s.filePath)}`
2828
3157
  })),
2829
3158
  required: true
2830
- });
2831
- if (Ct(selected)) {
2832
- Ne("Cancelled.");
2833
- process.exit(0);
2834
- }
2835
- const skills = selected;
3159
+ }));
2836
3160
  const tenantId = await resolveTeam(teams, opts.team);
2837
3161
  for (const skill of skills) {
2838
3162
  R2.step(`Publishing ${skill.suggestedName}...`);
2839
- const name = await Ze({
3163
+ const name = cancelGuard(await Ze({
2840
3164
  message: "Skill name?",
2841
3165
  initialValue: skill.suggestedName,
2842
3166
  validate: (v) => {
2843
3167
  if (!v || v.length < 1) return "Name is required";
2844
3168
  if (v.length > 100) return "Name must be 100 characters or less";
2845
3169
  }
2846
- });
2847
- if (Ct(name)) {
2848
- Ne("Cancelled.");
2849
- process.exit(0);
2850
- }
2851
- const visibility = await Je({
3170
+ }));
3171
+ const visibility = cancelGuard(await Je({
2852
3172
  message: "Visibility?",
2853
3173
  options: [
2854
3174
  { value: "private", label: "Private", hint: "Only team members" },
@@ -2856,23 +3176,15 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
2856
3176
  { value: "unlisted", label: "Unlisted", hint: "Accessible via direct link" }
2857
3177
  ],
2858
3178
  initialValue: "private"
2859
- });
2860
- if (Ct(visibility)) {
2861
- Ne("Cancelled.");
2862
- process.exit(0);
2863
- }
2864
- const contentType = await Je({
3179
+ }));
3180
+ const contentType = cancelGuard(await Je({
2865
3181
  message: "Type?",
2866
3182
  options: [
2867
3183
  { value: "skill", label: "Skill", hint: "Reusable agent instructions" },
2868
3184
  { value: "rule", label: "Rule", hint: "Governance constraints" }
2869
3185
  ],
2870
3186
  initialValue: skill.contentType
2871
- });
2872
- if (Ct(contentType)) {
2873
- Ne("Cancelled.");
2874
- process.exit(0);
2875
- }
3187
+ }));
2876
3188
  await uploadSkill(client, {
2877
3189
  name,
2878
3190
  content: skill.content,
@@ -2897,19 +3209,14 @@ async function resolveTeam(teams, teamFlag) {
2897
3209
  if (teams.length === 1) {
2898
3210
  return teams[0].id;
2899
3211
  }
2900
- const selected = await Je({
3212
+ return cancelGuard(await Je({
2901
3213
  message: "Which team?",
2902
3214
  options: teams.map((t) => ({
2903
3215
  value: t.id,
2904
3216
  label: t.name,
2905
3217
  hint: t.slug
2906
3218
  }))
2907
- });
2908
- if (Ct(selected)) {
2909
- Ne("Cancelled.");
2910
- process.exit(0);
2911
- }
2912
- return selected;
3219
+ }));
2913
3220
  }
2914
3221
  async function uploadSkill(client, params) {
2915
3222
  const spinner = bt2();
@@ -2954,9 +3261,316 @@ function shortenPath(filePath) {
2954
3261
  return filePath;
2955
3262
  }
2956
3263
 
3264
+ // src/commands/push.ts
3265
+ import { Command as Command7 } from "commander";
3266
+ import { readFileSync as readFileSync7, existsSync as existsSync16 } from "fs";
3267
+ import { resolve as resolve5 } from "path";
3268
+ 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(
3269
+ async (fileArg, opts) => {
3270
+ const client = new ApiClient();
3271
+ requireAuth(client);
3272
+ const filePath = resolve5(fileArg);
3273
+ if (!existsSync16(filePath)) {
3274
+ console.error(`File not found: ${filePath}`);
3275
+ process.exit(1);
3276
+ return;
3277
+ }
3278
+ const raw = readFileSync7(filePath, "utf-8");
3279
+ const content = stripFrontmatter(raw).trim();
3280
+ if (!content) {
3281
+ console.error("File is empty after stripping frontmatter.");
3282
+ process.exit(1);
3283
+ return;
3284
+ }
3285
+ const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
3286
+ if (opts.version && bumpFlags.length > 0) {
3287
+ console.error("Cannot specify both --version and --patch/--minor/--major");
3288
+ process.exit(1);
3289
+ return;
3290
+ }
3291
+ let body = {
3292
+ content,
3293
+ message: opts.message
3294
+ };
3295
+ if (opts.version) {
3296
+ if (!isValidSemVer(opts.version)) {
3297
+ console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
3298
+ process.exit(1);
3299
+ return;
3300
+ }
3301
+ body.semver = opts.version;
3302
+ } else if (opts.major) {
3303
+ body.bump = "major";
3304
+ } else if (opts.minor) {
3305
+ body.bump = "minor";
3306
+ } else if (opts.patch) {
3307
+ body.bump = "patch";
3308
+ }
3309
+ const spinner = bt2();
3310
+ spinner.start("Pushing new version...");
3311
+ const res = await client.post(
3312
+ `/api/skills/${encodeURIComponent(opts.skill)}/versions`,
3313
+ body
3314
+ );
3315
+ if (!res.success || !res.data) {
3316
+ spinner.stop(`Failed: ${res.error || "Unknown error"}`);
3317
+ process.exit(1);
3318
+ return;
3319
+ }
3320
+ const v = res.data;
3321
+ spinner.stop(`Pushed ${formatVersionLabel(v.semver, v.version)}`);
3322
+ Le("Done!");
3323
+ }
3324
+ );
3325
+
3326
+ // src/commands/share.ts
3327
+ import { Command as Command8 } from "commander";
3328
+ import { readFileSync as readFileSync8, existsSync as existsSync17 } from "fs";
3329
+ import { resolve as resolve6, basename as basename3, extname as extname3 } from "path";
3330
+ import { generateKeyPairSync } from "crypto";
3331
+ 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) => {
3332
+ We("localskills share");
3333
+ await ensureAnonymousIdentity();
3334
+ const client = new ApiClient();
3335
+ if (fileArg) {
3336
+ const filePath = resolve6(fileArg);
3337
+ if (!existsSync17(filePath)) {
3338
+ R2.error(`File not found: ${filePath}`);
3339
+ process.exit(1);
3340
+ }
3341
+ const raw = readFileSync8(filePath, "utf-8");
3342
+ const content = stripFrontmatter(raw).trim();
3343
+ if (!content) {
3344
+ R2.error("File is empty after stripping frontmatter.");
3345
+ process.exit(1);
3346
+ }
3347
+ const defaultSlug = basename3(filePath, extname3(filePath));
3348
+ const defaultName = titleFromSlug(defaultSlug);
3349
+ const skillName = opts.name || defaultName;
3350
+ const contentType = opts.type === "rule" ? "rule" : "skill";
3351
+ await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
3352
+ } else {
3353
+ const spinner = bt2();
3354
+ spinner.start("Scanning for skills...");
3355
+ const config = loadConfig();
3356
+ const allDetected = scanForSkills();
3357
+ const detected = filterTracked(allDetected, config);
3358
+ spinner.stop(
3359
+ detected.length > 0 ? `Found ${detected.length} skill file${detected.length !== 1 ? "s" : ""}.` : "No skill files found."
3360
+ );
3361
+ if (detected.length === 0) {
3362
+ Le("Nothing to share. Pass a file path: localskills share <file>");
3363
+ return;
3364
+ }
3365
+ const selected = cancelGuard(
3366
+ await Je({
3367
+ message: "Select a skill to share",
3368
+ options: detected.map((s) => ({
3369
+ value: s,
3370
+ label: s.suggestedName,
3371
+ hint: `${s.platform} \xB7 ${s.contentType}`
3372
+ }))
3373
+ })
3374
+ );
3375
+ const name = cancelGuard(
3376
+ await Ze({
3377
+ message: "Skill name?",
3378
+ initialValue: selected.suggestedName,
3379
+ validate: (v) => {
3380
+ if (!v || v.length < 1) return "Name is required";
3381
+ if (v.length > 100) return "Name must be 100 characters or less";
3382
+ }
3383
+ })
3384
+ );
3385
+ await uploadAnonymousSkill(client, {
3386
+ name,
3387
+ content: selected.content,
3388
+ type: selected.contentType
3389
+ });
3390
+ }
3391
+ Le("Done!");
3392
+ });
3393
+ async function ensureAnonymousIdentity() {
3394
+ const config = loadConfig();
3395
+ if (config.token) {
3396
+ const client = new ApiClient();
3397
+ const res2 = await client.get("/api/cli/auth");
3398
+ if (res2.success) return;
3399
+ }
3400
+ let keyPair = getAnonymousKey();
3401
+ if (!keyPair) {
3402
+ const s2 = bt2();
3403
+ s2.start("Generating anonymous identity...");
3404
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
3405
+ publicKeyEncoding: { type: "spki", format: "der" },
3406
+ privateKeyEncoding: { type: "pkcs8", format: "der" }
3407
+ });
3408
+ const rawPubKey = publicKey.subarray(publicKey.length - 32);
3409
+ const rawPrivKey = privateKey.subarray(privateKey.length - 32);
3410
+ keyPair = {
3411
+ publicKey: rawPubKey.toString("base64"),
3412
+ privateKey: rawPrivKey.toString("base64")
3413
+ };
3414
+ setAnonymousKey(keyPair);
3415
+ s2.stop("Identity created.");
3416
+ }
3417
+ const s = bt2();
3418
+ s.start("Connecting to localskills.sh...");
3419
+ const tempClient = new ApiClient();
3420
+ const res = await tempClient.post("/api/cli/auth/anonymous", {
3421
+ publicKey: keyPair.publicKey,
3422
+ algorithm: "Ed25519"
3423
+ });
3424
+ if (!res.success || !res.data) {
3425
+ s.stop(`Registration failed: ${res.error || "Unknown error"}`);
3426
+ process.exit(1);
3427
+ }
3428
+ setToken(res.data.token);
3429
+ s.stop(`Connected as ${res.data.username}`);
3430
+ }
3431
+ async function uploadAnonymousSkill(client, params) {
3432
+ const s = bt2();
3433
+ s.start(`Sharing "${params.name}"...`);
3434
+ const teamsRes = await client.get("/api/tenants");
3435
+ if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
3436
+ s.stop("Failed to find your team. Try running `localskills share` again.");
3437
+ process.exit(1);
3438
+ }
3439
+ const tenantId = teamsRes.data[0].id;
3440
+ const res = await client.post("/api/skills", {
3441
+ name: params.name,
3442
+ content: params.content,
3443
+ tenantId,
3444
+ visibility: "unlisted",
3445
+ type: params.type
3446
+ });
3447
+ if (!res.success || !res.data) {
3448
+ s.stop(`Failed: ${res.error || "Unknown error"}`);
3449
+ return;
3450
+ }
3451
+ s.stop("Shared!");
3452
+ R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
3453
+ R2.info(`Install: localskills install ${res.data.publicId}`);
3454
+ }
3455
+
3456
+ // src/commands/profile.ts
3457
+ import { Command as Command9 } from "commander";
3458
+ var PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
3459
+ var MAX_PROFILE_NAME_LENGTH = 32;
3460
+ var profileCommand = new Command9("profile").description("Manage CLI profiles for multiple accounts");
3461
+ profileCommand.command("list").description("List all profiles").action(() => {
3462
+ const config = loadFullConfig();
3463
+ const persisted = config.active_profile;
3464
+ let resolved;
3465
+ try {
3466
+ resolved = getActiveProfileName();
3467
+ } catch (err) {
3468
+ if (err instanceof ProfileNotFoundError) {
3469
+ console.error(`Warning: ${err.message}
3470
+ `);
3471
+ resolved = persisted;
3472
+ } else {
3473
+ throw err;
3474
+ }
3475
+ }
3476
+ const names = Object.keys(config.profiles);
3477
+ console.log("Profiles:\n");
3478
+ for (const name of names) {
3479
+ const profile = config.profiles[name];
3480
+ let marker = "";
3481
+ if (name === resolved && name === persisted) {
3482
+ marker = " (active)";
3483
+ } else if (name === resolved) {
3484
+ marker = " (active via override)";
3485
+ } else if (name === persisted) {
3486
+ marker = " (default)";
3487
+ }
3488
+ const auth = profile.token ? "authenticated" : "not authenticated";
3489
+ const skillCount = Object.keys(profile.installed_skills).length;
3490
+ console.log(` ${name}${marker} \u2014 ${auth}, ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3491
+ }
3492
+ });
3493
+ profileCommand.command("create <name>").description("Create a new profile").action((name) => {
3494
+ if (!PROFILE_NAME_RE.test(name)) {
3495
+ console.error(
3496
+ `Error: Invalid profile name "${name}". Profile names must use lowercase letters, numbers, and hyphens, and must start with a letter or number.`
3497
+ );
3498
+ process.exit(1);
3499
+ }
3500
+ if (name.length > MAX_PROFILE_NAME_LENGTH) {
3501
+ console.error(`Error: Profile name must be ${MAX_PROFILE_NAME_LENGTH} characters or fewer.`);
3502
+ process.exit(1);
3503
+ }
3504
+ const config = loadFullConfig();
3505
+ if (config.profiles[name]) {
3506
+ console.error(`Error: Profile "${name}" already exists.`);
3507
+ process.exit(1);
3508
+ }
3509
+ config.profiles[name] = {
3510
+ token: null,
3511
+ installed_skills: {},
3512
+ defaults: {
3513
+ scope: "project",
3514
+ method: "symlink"
3515
+ },
3516
+ anonymous_key: null
3517
+ };
3518
+ saveFullConfig(config);
3519
+ console.log(`Profile "${name}" created.`);
3520
+ });
3521
+ profileCommand.command("switch <name>").description("Switch the active profile").action((name) => {
3522
+ const config = loadFullConfig();
3523
+ if (!config.profiles[name]) {
3524
+ console.error(`Error: Profile "${name}" does not exist.`);
3525
+ console.error(`Available profiles: ${Object.keys(config.profiles).join(", ")}`);
3526
+ process.exit(1);
3527
+ }
3528
+ config.active_profile = name;
3529
+ saveFullConfig(config);
3530
+ console.log(`Switched to profile "${name}".`);
3531
+ });
3532
+ profileCommand.command("delete <name>").description("Delete a profile").option("-f, --force", "Skip confirmation prompt").action(async (name, opts) => {
3533
+ if (name === DEFAULT_PROFILE_NAME) {
3534
+ console.error(`Error: Cannot delete the "${DEFAULT_PROFILE_NAME}" profile.`);
3535
+ process.exit(1);
3536
+ }
3537
+ const config = loadFullConfig();
3538
+ if (!config.profiles[name]) {
3539
+ console.error(`Error: Profile "${name}" does not exist.`);
3540
+ process.exit(1);
3541
+ }
3542
+ if (name === config.active_profile) {
3543
+ console.error(`Error: Cannot delete the active profile "${name}". Run \`localskills profile switch <other>\` first.`);
3544
+ process.exit(1);
3545
+ }
3546
+ if (!opts.force) {
3547
+ const profile = config.profiles[name];
3548
+ const skillCount = Object.keys(profile.installed_skills).length;
3549
+ const details = [
3550
+ profile.token ? "authenticated session" : null,
3551
+ skillCount > 0 ? `${skillCount} installed skill${skillCount !== 1 ? "s" : ""}` : null
3552
+ ].filter(Boolean);
3553
+ const detailStr = details.length > 0 ? ` (has ${details.join(", ")})` : "";
3554
+ const confirmed = await Re({
3555
+ message: `Delete profile "${name}"${detailStr}? This cannot be undone.`
3556
+ });
3557
+ if (Ct(confirmed) || !confirmed) {
3558
+ console.log("Cancelled.");
3559
+ return;
3560
+ }
3561
+ }
3562
+ delete config.profiles[name];
3563
+ saveFullConfig(config);
3564
+ console.log(`Profile "${name}" deleted.`);
3565
+ });
3566
+
2957
3567
  // src/index.ts
2958
- var program = new Command7();
2959
- program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0");
3568
+ var program = new Command10();
3569
+ program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0").option("--profile <name>", "Use a specific profile");
3570
+ program.hook("preAction", (thisCommand) => {
3571
+ const opts = thisCommand.opts();
3572
+ setProfileOverride(opts.profile);
3573
+ });
2960
3574
  program.addCommand(loginCommand);
2961
3575
  program.addCommand(logoutCommand);
2962
3576
  program.addCommand(whoamiCommand);
@@ -2965,4 +3579,14 @@ program.addCommand(uninstallCommand);
2965
3579
  program.addCommand(listCommand);
2966
3580
  program.addCommand(pullCommand);
2967
3581
  program.addCommand(publishCommand);
2968
- program.parse();
3582
+ program.addCommand(pushCommand);
3583
+ program.addCommand(shareCommand);
3584
+ program.addCommand(profileCommand);
3585
+ program.parseAsync().catch((err) => {
3586
+ if (err instanceof ProfileNotFoundError) {
3587
+ console.error(`Error: ${err.message}`);
3588
+ } else {
3589
+ console.error(err instanceof Error ? err.message : String(err));
3590
+ }
3591
+ process.exit(1);
3592
+ });