@localskills/cli 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +262 -139
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -164,7 +164,6 @@ import { Command as Command10 } from "commander";
164
164
 
165
165
  // src/commands/auth.ts
166
166
  import { Command } from "commander";
167
- import { randomBytes, createHash } from "crypto";
168
167
  import { spawn } from "child_process";
169
168
 
170
169
  // ../../node_modules/.pnpm/@clack+core@1.0.1/node_modules/@clack/core/dist/index.mjs
@@ -1144,7 +1143,7 @@ ${l}
1144
1143
  } }).prompt();
1145
1144
 
1146
1145
  // src/lib/config.ts
1147
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from "fs";
1146
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, renameSync } from "fs";
1148
1147
  import { join } from "path";
1149
1148
  import { homedir } from "os";
1150
1149
  var CONFIG_DIR = join(homedir(), ".localskills");
@@ -1214,6 +1213,21 @@ function validateV3(config) {
1214
1213
  if (!config.profiles[DEFAULT_PROFILE_NAME]) {
1215
1214
  config.profiles[DEFAULT_PROFILE_NAME] = { ...DEFAULT_PROFILE, installed_skills: {} };
1216
1215
  }
1216
+ for (const name of Object.keys(config.profiles)) {
1217
+ const profile = config.profiles[name];
1218
+ if (!profile || typeof profile !== "object") {
1219
+ config.profiles[name] = { ...DEFAULT_PROFILE, installed_skills: {} };
1220
+ continue;
1221
+ }
1222
+ if (!profile.installed_skills || typeof profile.installed_skills !== "object") {
1223
+ profile.installed_skills = {};
1224
+ }
1225
+ if (!profile.defaults || typeof profile.defaults !== "object") {
1226
+ profile.defaults = { ...DEFAULT_PROFILE.defaults };
1227
+ }
1228
+ if (profile.token === void 0) profile.token = null;
1229
+ if (profile.anonymous_key === void 0) profile.anonymous_key = null;
1230
+ }
1217
1231
  if (!config.active_profile || !config.profiles[config.active_profile]) {
1218
1232
  config.active_profile = DEFAULT_PROFILE_NAME;
1219
1233
  }
@@ -1266,9 +1280,11 @@ function loadFullConfig() {
1266
1280
  }
1267
1281
  function saveFullConfig(config) {
1268
1282
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1269
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
1283
+ const tmpPath = `${CONFIG_PATH}.${process.pid}.tmp`;
1284
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", {
1270
1285
  mode: 384
1271
1286
  });
1287
+ renameSync(tmpPath, CONFIG_PATH);
1272
1288
  try {
1273
1289
  chmodSync(CONFIG_DIR, 448);
1274
1290
  chmodSync(CONFIG_PATH, 384);
@@ -1349,6 +1365,13 @@ function migrateV2toV3(v2) {
1349
1365
  }
1350
1366
  };
1351
1367
  }
1368
+ function resolveInstalledSkillKey(config, ref) {
1369
+ if (config.installed_skills[ref]) return ref;
1370
+ for (const [key, record] of Object.entries(config.installed_skills)) {
1371
+ if (record.slug === ref) return key;
1372
+ }
1373
+ return null;
1374
+ }
1352
1375
  function getToken() {
1353
1376
  return loadConfig().token;
1354
1377
  }
@@ -1463,11 +1486,6 @@ var ApiClient = class {
1463
1486
  };
1464
1487
 
1465
1488
  // src/commands/auth.ts
1466
- var USER_CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
1467
- function generateUserCode(length = 8) {
1468
- const bytes = randomBytes(length);
1469
- return Array.from(bytes).map((b) => USER_CODE_CHARS[b % USER_CODE_CHARS.length]).join("");
1470
- }
1471
1489
  function openBrowser(url) {
1472
1490
  try {
1473
1491
  const platform = process.platform;
@@ -1533,18 +1551,15 @@ var loginCommand = new Command("login").description("Log in to localskills.sh").
1533
1551
  We("localskills login");
1534
1552
  const spinner = bt2();
1535
1553
  spinner.start("Initializing...");
1536
- const deviceCode = randomBytes(32);
1537
- const codeHash = createHash("sha256").update(deviceCode).digest("hex");
1538
- const userCode = generateUserCode();
1539
1554
  const client = new ApiClient();
1540
- const initRes = await client.post("/api/cli/auth/device", { codeHash, userCode });
1555
+ const initRes = await client.post("/api/cli/auth/device");
1541
1556
  if (!initRes.success || !initRes.data) {
1542
1557
  spinner.stop(`Failed: ${initRes.error || "Could not start login"}`);
1543
1558
  process.exit(1);
1544
1559
  return;
1545
1560
  }
1546
1561
  spinner.stop("Ready!");
1547
- const { verificationUrl } = initRes.data;
1562
+ const { deviceCode, verificationUrl, userCode } = initRes.data;
1548
1563
  R2.info(`Your verification code: ${userCode}`);
1549
1564
  R2.message(`Opening browser to ${verificationUrl}`);
1550
1565
  R2.message("If the browser doesn't open, visit the URL above manually.");
@@ -1552,12 +1567,13 @@ var loginCommand = new Command("login").description("Log in to localskills.sh").
1552
1567
  const pollSpinner = bt2();
1553
1568
  pollSpinner.start("Waiting for authorization...");
1554
1569
  const expiresAt = new Date(initRes.data.expiresAt).getTime();
1555
- const POLL_INTERVAL = 2e3;
1570
+ const pollInterval = Math.max(1, initRes.data.interval || 2) * 1e3;
1556
1571
  while (Date.now() < expiresAt) {
1557
- await sleep(POLL_INTERVAL);
1572
+ await sleep(pollInterval);
1558
1573
  try {
1559
- const pollRes = await client.get(
1560
- `/api/cli/auth/poll?code_hash=${codeHash}`
1574
+ const pollRes = await client.post(
1575
+ "/api/cli/auth/poll",
1576
+ { deviceCode }
1561
1577
  );
1562
1578
  if (!pollRes.success || !pollRes.data) continue;
1563
1579
  if (pollRes.data.status === "approved" && pollRes.data.token) {
@@ -1616,7 +1632,7 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
1616
1632
  // src/commands/install.ts
1617
1633
  import { Command as Command2 } from "commander";
1618
1634
  import { mkdirSync as mkdirSync7, rmSync as rmSync4, cpSync } from "fs";
1619
- import { dirname as dirname5 } from "path";
1635
+ import { dirname as dirname5, resolve as resolvePathAbs } from "path";
1620
1636
 
1621
1637
  // ../../packages/shared/dist/utils/semver.js
1622
1638
  var SEMVER_RE = /^\d+\.\d+\.\d+$/;
@@ -1669,14 +1685,17 @@ function requireAuth(client) {
1669
1685
  }
1670
1686
  }
1671
1687
  function buildVersionQuery(range) {
1672
- if (!range) return "";
1688
+ if (!range || range === "latest") return "";
1673
1689
  if (isValidSemVer(range)) {
1674
1690
  return `?semver=${encodeURIComponent(range)}`;
1675
1691
  }
1676
1692
  if (isValidSemVerRange(range)) {
1677
1693
  return `?range=${encodeURIComponent(range)}`;
1678
1694
  }
1679
- console.error(`Invalid version specifier: ${range}`);
1695
+ console.error(
1696
+ `Invalid version specifier: ${range}
1697
+ Expected an exact version (1.2.3), a range (^1.2.3, ~1.2.3, >=1.2.3), "*", or "latest".`
1698
+ );
1680
1699
  process.exit(1);
1681
1700
  }
1682
1701
  function formatVersionLabel(semver, version2) {
@@ -1703,14 +1722,17 @@ import { join as join13, resolve as resolve3 } from "path";
1703
1722
  import { homedir as homedir8 } from "os";
1704
1723
 
1705
1724
  // src/lib/installers/cursor.ts
1706
- import { existsSync as existsSync4 } from "fs";
1725
+ import { existsSync as existsSync5 } from "fs";
1707
1726
  import { join as join3 } from "path";
1708
1727
  import { homedir as homedir2 } from "os";
1709
1728
 
1710
1729
  // src/lib/content-transform.ts
1711
1730
  function yamlEscape(value) {
1712
- if (value.includes(":") || value.includes("#") || value.includes('"') || value.includes("'") || value.startsWith("{") || value.startsWith("[")) {
1713
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1731
+ const needsQuoting = /[\r\n:#"']/.test(value) || // structural characters anywhere
1732
+ /^[\s\-?*&!|>%@`{[\]}]/.test(value) || // YAML indicator at the start
1733
+ /\s$/.test(value);
1734
+ if (needsQuoting) {
1735
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n")}"`;
1714
1736
  }
1715
1737
  return value;
1716
1738
  }
@@ -1745,7 +1767,7 @@ function stripFrontmatter(content) {
1745
1767
  }
1746
1768
 
1747
1769
  // src/lib/installers/common.ts
1748
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1770
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
1749
1771
  import { join as join2 } from "path";
1750
1772
 
1751
1773
  // src/lib/symlink.ts
@@ -1799,6 +1821,61 @@ function isSymlinkInto(linkPath, dir) {
1799
1821
  }
1800
1822
  }
1801
1823
 
1824
+ // src/lib/marked-sections.ts
1825
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
1826
+ import { dirname as dirname2 } from "path";
1827
+ var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
1828
+ var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
1829
+ function upsertSection(filePath, slug, content) {
1830
+ mkdirSync3(dirname2(filePath), { recursive: true });
1831
+ let existing = "";
1832
+ if (existsSync3(filePath)) {
1833
+ existing = readFileSync2(filePath, "utf-8");
1834
+ }
1835
+ const start = START_MARKER(slug);
1836
+ const end = END_MARKER(slug);
1837
+ const section = `${start}
1838
+ ${content}
1839
+ ${end}`;
1840
+ const startIdx = existing.indexOf(start);
1841
+ const endIdx = startIdx === -1 ? -1 : existing.indexOf(end, startIdx + start.length);
1842
+ let result;
1843
+ if (startIdx !== -1 && endIdx !== -1) {
1844
+ result = existing.slice(0, startIdx) + section + existing.slice(endIdx + end.length);
1845
+ } else {
1846
+ const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
1847
+ result = existing + separator + section + "\n";
1848
+ }
1849
+ writeFileSync2(filePath, result);
1850
+ }
1851
+ function removeSection(filePath, slug) {
1852
+ if (!existsSync3(filePath)) return false;
1853
+ const existing = readFileSync2(filePath, "utf-8");
1854
+ const start = START_MARKER(slug);
1855
+ const end = END_MARKER(slug);
1856
+ const startIdx = existing.indexOf(start);
1857
+ const endIdx = startIdx === -1 ? -1 : existing.indexOf(end, startIdx + start.length);
1858
+ if (startIdx === -1 || endIdx === -1) return false;
1859
+ let before = existing.slice(0, startIdx);
1860
+ let after = existing.slice(endIdx + end.length);
1861
+ while (before.endsWith("\n\n")) before = before.slice(0, -1);
1862
+ while (after.startsWith("\n\n")) after = after.slice(1);
1863
+ const result = (before + after).trim();
1864
+ writeFileSync2(filePath, result ? result + "\n" : "");
1865
+ return true;
1866
+ }
1867
+ function listSections(filePath) {
1868
+ if (!existsSync3(filePath)) return [];
1869
+ const content = readFileSync2(filePath, "utf-8");
1870
+ const regex = /<!-- localskills:start:(.+?) -->/g;
1871
+ const slugs = [];
1872
+ let match;
1873
+ while ((match = regex.exec(content)) !== null) {
1874
+ slugs.push(match[1]);
1875
+ }
1876
+ return slugs;
1877
+ }
1878
+
1802
1879
  // src/lib/installers/common.ts
1803
1880
  function safeSlugName(slug) {
1804
1881
  return slug.replace(/\//g, "-");
@@ -1808,18 +1885,26 @@ function installFileOrSymlink(opts, targetPath) {
1808
1885
  if (opts.method === "symlink") {
1809
1886
  createSymlink(opts.cachePath, targetPath);
1810
1887
  } else {
1811
- mkdirSync3(join2(targetPath, ".."), { recursive: true });
1812
- writeFileSync2(targetPath, opts.content);
1888
+ mkdirSync4(join2(targetPath, ".."), { recursive: true });
1889
+ removeSymlink(targetPath);
1890
+ writeFileSync3(targetPath, opts.content);
1813
1891
  }
1814
1892
  return targetPath;
1815
1893
  }
1816
1894
  function uninstallFile(installation) {
1817
1895
  if (installation.method === "symlink") {
1818
1896
  removeSymlink(installation.path);
1819
- } else if (existsSync3(installation.path)) {
1897
+ } else if (existsSync4(installation.path)) {
1820
1898
  unlinkSync2(installation.path);
1821
1899
  }
1822
1900
  }
1901
+ function installationIntact(installation, slug) {
1902
+ if (!existsSync4(installation.path)) return false;
1903
+ if (installation.method === "section") {
1904
+ return listSections(installation.path).includes(slug);
1905
+ }
1906
+ return true;
1907
+ }
1823
1908
  function defaultTransformContent(content) {
1824
1909
  return toPlainMD(content);
1825
1910
  }
@@ -1837,8 +1922,8 @@ function detect(projectDir) {
1837
1922
  const home = homedir2();
1838
1923
  const cwd = projectDir || process.cwd();
1839
1924
  return {
1840
- global: existsSync4(join3(home, ".cursor")),
1841
- project: existsSync4(join3(cwd, ".cursor"))
1925
+ global: existsSync5(join3(home, ".cursor")),
1926
+ project: existsSync5(join3(cwd, ".cursor"))
1842
1927
  };
1843
1928
  }
1844
1929
  function resolvePath(slug, scope, projectDir, _contentType) {
@@ -1873,7 +1958,7 @@ var cursorAdapter = {
1873
1958
  };
1874
1959
 
1875
1960
  // src/lib/installers/claude.ts
1876
- import { existsSync as existsSync5, rmSync as rmSync2, statSync, readdirSync } from "fs";
1961
+ import { existsSync as existsSync6, rmSync as rmSync2, statSync, readdirSync } from "fs";
1877
1962
  import { join as join4 } from "path";
1878
1963
  import { homedir as homedir3 } from "os";
1879
1964
  var descriptor2 = {
@@ -1888,8 +1973,8 @@ function detect2(projectDir) {
1888
1973
  const home = homedir3();
1889
1974
  const cwd = projectDir || process.cwd();
1890
1975
  return {
1891
- global: existsSync5(join4(home, ".claude")),
1892
- project: existsSync5(join4(cwd, ".claude"))
1976
+ global: existsSync6(join4(home, ".claude")),
1977
+ project: existsSync6(join4(cwd, ".claude"))
1893
1978
  };
1894
1979
  }
1895
1980
  function claudeBase(scope, projectDir) {
@@ -1920,7 +2005,7 @@ function uninstall2(installation, _slug) {
1920
2005
  const target = installation.path;
1921
2006
  if (isSymlink(target)) {
1922
2007
  removeSymlink(target);
1923
- } else if (existsSync5(target)) {
2008
+ } else if (existsSync6(target)) {
1924
2009
  if (statSync(target).isDirectory()) {
1925
2010
  rmSync2(target, { recursive: true, force: true });
1926
2011
  } else {
@@ -1929,7 +2014,7 @@ function uninstall2(installation, _slug) {
1929
2014
  }
1930
2015
  const parentDir = join4(target, "..");
1931
2016
  try {
1932
- if (existsSync5(parentDir) && readdirSync(parentDir).length === 0) {
2017
+ if (existsSync6(parentDir) && readdirSync(parentDir).length === 0) {
1933
2018
  rmSync2(parentDir, { recursive: true });
1934
2019
  }
1935
2020
  } catch {
@@ -1954,13 +2039,14 @@ import { join as join6 } from "path";
1954
2039
  import { homedir as homedir5 } from "os";
1955
2040
 
1956
2041
  // src/lib/detect.ts
1957
- import { existsSync as existsSync6 } from "fs";
2042
+ import { existsSync as existsSync7 } from "fs";
1958
2043
  import { execFileSync } from "child_process";
1959
2044
  import { homedir as homedir4 } from "os";
1960
2045
  import { join as join5 } from "path";
1961
2046
  function commandExists(cmd) {
1962
2047
  try {
1963
- execFileSync("which", [cmd], { stdio: "ignore" });
2048
+ const finder = process.platform === "win32" ? "where" : "which";
2049
+ execFileSync(finder, [cmd], { stdio: "ignore" });
1964
2050
  return true;
1965
2051
  } catch {
1966
2052
  return false;
@@ -1970,76 +2056,21 @@ function detectInstalledPlatforms(projectDir) {
1970
2056
  const detected = [];
1971
2057
  const home = homedir4();
1972
2058
  const cwd = projectDir || process.cwd();
1973
- if (existsSync6(join5(home, ".cursor")) || existsSync6(join5(cwd, ".cursor")))
2059
+ if (existsSync7(join5(home, ".cursor")) || existsSync7(join5(cwd, ".cursor")))
1974
2060
  detected.push("cursor");
1975
- if (existsSync6(join5(home, ".claude")) || commandExists("claude"))
2061
+ if (existsSync7(join5(home, ".claude")) || commandExists("claude"))
1976
2062
  detected.push("claude");
1977
2063
  if (commandExists("codex")) detected.push("codex");
1978
- if (existsSync6(join5(home, ".codeium")) || existsSync6(join5(cwd, ".windsurf")))
2064
+ if (existsSync7(join5(home, ".codeium")) || existsSync7(join5(cwd, ".windsurf")))
1979
2065
  detected.push("windsurf");
1980
- if (existsSync6(join5(cwd, ".clinerules"))) detected.push("cline");
1981
- if (existsSync6(join5(cwd, ".github"))) detected.push("copilot");
1982
- if (commandExists("opencode") || existsSync6(join5(cwd, ".opencode")))
2066
+ if (existsSync7(join5(cwd, ".clinerules"))) detected.push("cline");
2067
+ if (existsSync7(join5(cwd, ".github"))) detected.push("copilot");
2068
+ if (commandExists("opencode") || existsSync7(join5(cwd, ".opencode")))
1983
2069
  detected.push("opencode");
1984
2070
  if (commandExists("aider")) detected.push("aider");
1985
2071
  return detected;
1986
2072
  }
1987
2073
 
1988
- // src/lib/marked-sections.ts
1989
- import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1990
- import { dirname as dirname2 } from "path";
1991
- var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
1992
- var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
1993
- function upsertSection(filePath, slug, content) {
1994
- mkdirSync4(dirname2(filePath), { recursive: true });
1995
- let existing = "";
1996
- if (existsSync7(filePath)) {
1997
- existing = readFileSync2(filePath, "utf-8");
1998
- }
1999
- const start = START_MARKER(slug);
2000
- const end = END_MARKER(slug);
2001
- const section = `${start}
2002
- ${content}
2003
- ${end}`;
2004
- const startIdx = existing.indexOf(start);
2005
- const endIdx = existing.indexOf(end);
2006
- let result;
2007
- if (startIdx !== -1 && endIdx !== -1) {
2008
- result = existing.slice(0, startIdx) + section + existing.slice(endIdx + end.length);
2009
- } else {
2010
- const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
2011
- result = existing + separator + section + "\n";
2012
- }
2013
- writeFileSync3(filePath, result);
2014
- }
2015
- function removeSection(filePath, slug) {
2016
- if (!existsSync7(filePath)) return false;
2017
- const existing = readFileSync2(filePath, "utf-8");
2018
- const start = START_MARKER(slug);
2019
- const end = END_MARKER(slug);
2020
- const startIdx = existing.indexOf(start);
2021
- const endIdx = existing.indexOf(end);
2022
- if (startIdx === -1 || endIdx === -1) return false;
2023
- let before = existing.slice(0, startIdx);
2024
- let after = existing.slice(endIdx + end.length);
2025
- while (before.endsWith("\n\n")) before = before.slice(0, -1);
2026
- while (after.startsWith("\n\n")) after = after.slice(1);
2027
- const result = (before + after).trim();
2028
- writeFileSync3(filePath, result ? result + "\n" : "");
2029
- return true;
2030
- }
2031
- function listSections(filePath) {
2032
- if (!existsSync7(filePath)) return [];
2033
- const content = readFileSync2(filePath, "utf-8");
2034
- const regex = /<!-- localskills:start:(.+?) -->/g;
2035
- const slugs = [];
2036
- let match;
2037
- while ((match = regex.exec(content)) !== null) {
2038
- slugs.push(match[1]);
2039
- }
2040
- return slugs;
2041
- }
2042
-
2043
2074
  // src/lib/installers/codex.ts
2044
2075
  var descriptor3 = {
2045
2076
  id: "codex",
@@ -2474,6 +2505,16 @@ function getPlatformFile(slug, platform, skill) {
2474
2505
  writeFileSync6(filePath, transformed);
2475
2506
  return filePath;
2476
2507
  }
2508
+ function regenerateTextPlatformFiles(slug, installations, skill) {
2509
+ for (const inst of installations) {
2510
+ if ((inst.format ?? "text") !== "text") continue;
2511
+ if (inst.method !== "symlink") continue;
2512
+ try {
2513
+ getPlatformFile(slug, inst.platform, skill);
2514
+ } catch {
2515
+ }
2516
+ }
2517
+ }
2477
2518
  function getRawContent(slug) {
2478
2519
  const filePath = join13(getCacheDir(slug), "raw.md");
2479
2520
  if (!existsSync13(filePath)) return null;
@@ -2607,13 +2648,14 @@ async function interactiveTargets(detectedPlatforms) {
2607
2648
  }));
2608
2649
  return { platforms, scope, method };
2609
2650
  }
2610
- async function interactiveUninstall(installedSlugs) {
2651
+ async function interactiveUninstall(installedSkills) {
2611
2652
  We("localskills uninstall");
2612
2653
  return cancelGuard(await Je({
2613
2654
  message: "Which skill would you like to uninstall?",
2614
- options: installedSlugs.map((s) => ({
2615
- value: s,
2616
- label: s
2655
+ options: Object.entries(installedSkills).map(([key, record]) => ({
2656
+ value: key,
2657
+ label: record.name || record.slug || key,
2658
+ hint: record.slug && record.slug !== key ? record.slug : void 0
2617
2659
  }))
2618
2660
  }));
2619
2661
  }
@@ -2653,7 +2695,9 @@ function parsePlatforms(raw) {
2653
2695
  }
2654
2696
  function buildSkillRecord(cacheKey, skill, version2, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
2655
2697
  return {
2656
- slug: cacheKey,
2698
+ // The human slug (when known), so `uninstall`/`pull <slug>` can resolve
2699
+ // records that are keyed by publicId.
2700
+ slug: skill.slug ?? cacheKey,
2657
2701
  name: skill.name,
2658
2702
  type: skill.type ?? "skill",
2659
2703
  hash: skill.contentHash,
@@ -2701,7 +2745,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2701
2745
  let method;
2702
2746
  let projectDir;
2703
2747
  if (typeof opts.project === "string") {
2704
- projectDir = opts.project;
2748
+ projectDir = resolvePathAbs(opts.project);
2705
2749
  }
2706
2750
  const explicitPlatforms = parsePlatforms(opts.target);
2707
2751
  const explicitScope = opts.global ? "global" : opts.project !== void 0 ? "project" : null;
@@ -2718,7 +2762,12 @@ var installCommand = new Command2("install").description("Install a skill locall
2718
2762
  spinner2.start("Fetching available skills...");
2719
2763
  const res2 = await client.get("/api/skills");
2720
2764
  spinner2.stop("Fetched skills.");
2721
- if (!res2.success || !res2.data || res2.data.length === 0) {
2765
+ if (!res2.success || !res2.data) {
2766
+ console.error(`Error: ${res2.error || "Failed to fetch skills."}`);
2767
+ process.exit(1);
2768
+ return;
2769
+ }
2770
+ if (res2.data.length === 0) {
2722
2771
  console.error("No skills available.");
2723
2772
  process.exit(1);
2724
2773
  return;
@@ -2747,6 +2796,9 @@ var installCommand = new Command2("install").description("Install a skill locall
2747
2796
  const atIdx = slug.lastIndexOf("@");
2748
2797
  requestedRange = slug.substring(atIdx + 1);
2749
2798
  slug = slug.substring(0, atIdx);
2799
+ if (requestedRange === "" || requestedRange === "latest") {
2800
+ requestedRange = null;
2801
+ }
2750
2802
  }
2751
2803
  const versionQuery = buildVersionQuery(requestedRange);
2752
2804
  const spinner = bt2();
@@ -2866,6 +2918,11 @@ var installCommand = new Command2("install").description("Install a skill locall
2866
2918
  const { skill, content, version: version2, semver: resolvedSemver } = resData;
2867
2919
  spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version2)}`);
2868
2920
  store(cacheKey, content, skill, version2);
2921
+ regenerateTextPlatformFiles(
2922
+ cacheKey,
2923
+ config.installed_skills[cacheKey]?.installations ?? [],
2924
+ skill
2925
+ );
2869
2926
  const contentType = skill.type ?? "skill";
2870
2927
  const installations = [];
2871
2928
  const results = [];
@@ -2918,6 +2975,11 @@ var installCommand = new Command2("install").description("Install a skill locall
2918
2975
  const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
2919
2976
  results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
2920
2977
  }
2978
+ if (installations.length === 0) {
2979
+ R2.error("Nothing was installed \u2014 no selected platform supports this scope.");
2980
+ process.exit(1);
2981
+ return;
2982
+ }
2921
2983
  config.installed_skills[cacheKey] = buildSkillRecord(
2922
2984
  cacheKey,
2923
2985
  skill,
@@ -2939,25 +3001,26 @@ var installCommand = new Command2("install").description("Install a skill locall
2939
3001
  import { Command as Command3 } from "commander";
2940
3002
  var uninstallCommand = new Command3("uninstall").description("Uninstall a skill").argument("[slug]", "Skill slug (omit for interactive selection)").option("--purge", "Also remove from cache").action(async (slugArg, opts) => {
2941
3003
  const config = loadConfig();
2942
- const installedSlugs = Object.keys(config.installed_skills);
2943
- if (installedSlugs.length === 0) {
3004
+ if (Object.keys(config.installed_skills).length === 0) {
2944
3005
  console.log("No installed skills.");
2945
3006
  return;
2946
3007
  }
2947
3008
  let slug;
2948
3009
  if (slugArg) {
2949
- slug = slugArg;
3010
+ const resolved = resolveInstalledSkillKey(config, slugArg);
3011
+ if (!resolved) {
3012
+ console.error(`Skill "${slugArg}" is not installed.`);
3013
+ process.exit(1);
3014
+ return;
3015
+ }
3016
+ slug = resolved;
2950
3017
  } else {
2951
- slug = await interactiveUninstall(installedSlugs);
3018
+ slug = await interactiveUninstall(config.installed_skills);
2952
3019
  }
2953
3020
  const installed = config.installed_skills[slug];
2954
- if (!installed) {
2955
- console.error(`Skill "${slug}" is not installed.`);
2956
- process.exit(1);
2957
- return;
2958
- }
2959
3021
  We(`localskills uninstall ${slug}`);
2960
3022
  let removed = 0;
3023
+ const failed = [];
2961
3024
  for (const installation of installed.installations) {
2962
3025
  try {
2963
3026
  const adapter = getAdapter(installation.platform);
@@ -2970,13 +3033,26 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
2970
3033
  R2.warn(
2971
3034
  `${installation.platform} \u2014 failed to remove: ${err instanceof Error ? err.message : String(err)}`
2972
3035
  );
3036
+ failed.push(installation);
2973
3037
  }
2974
3038
  }
2975
- delete config.installed_skills[slug];
3039
+ if (failed.length === 0) {
3040
+ delete config.installed_skills[slug];
3041
+ } else {
3042
+ installed.installations = failed;
3043
+ R2.warn(
3044
+ `${failed.length} target(s) could not be removed and remain tracked. Fix the issue and run \`localskills uninstall ${slug}\` again.`
3045
+ );
3046
+ process.exitCode = 1;
3047
+ }
2976
3048
  saveConfig(config);
2977
3049
  if (opts?.purge) {
2978
- purge(slug);
2979
- R2.info("Cache purged.");
3050
+ if (failed.length === 0) {
3051
+ purge(slug);
3052
+ R2.info("Cache purged.");
3053
+ } else {
3054
+ R2.warn("Skipped cache purge because some targets could not be removed.");
3055
+ }
2980
3056
  }
2981
3057
  Le(
2982
3058
  `Uninstalled ${slug} from ${removed} target(s).`
@@ -3079,7 +3155,18 @@ function installedAsPackage(inst) {
3079
3155
  }
3080
3156
  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) => {
3081
3157
  const config = loadConfig();
3082
- const slugs = slugArg ? [slugArg] : Object.keys(config.installed_skills);
3158
+ let slugs;
3159
+ if (slugArg) {
3160
+ const resolved = resolveInstalledSkillKey(config, slugArg);
3161
+ if (!resolved) {
3162
+ console.error(`Skill "${slugArg}" is not installed.`);
3163
+ process.exit(1);
3164
+ return;
3165
+ }
3166
+ slugs = [resolved];
3167
+ } else {
3168
+ slugs = Object.keys(config.installed_skills);
3169
+ }
3083
3170
  if (slugs.length === 0) {
3084
3171
  console.log("No installed skills. Use `localskills install` first.");
3085
3172
  return;
@@ -3108,9 +3195,16 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
3108
3195
  const format = resData.format ?? "text";
3109
3196
  const { skill, version: version2 } = resData;
3110
3197
  if (skill.contentHash === installed.hash) {
3111
- spinner.stop(`${slug} \u2014 up to date`);
3112
- skipped++;
3113
- continue;
3198
+ const broken = installed.installations.some(
3199
+ (inst) => !installationIntact(inst, slug)
3200
+ );
3201
+ if (!broken) {
3202
+ spinner.stop(`${slug} \u2014 up to date`);
3203
+ skipped++;
3204
+ continue;
3205
+ }
3206
+ spinner.stop(`${slug} \u2014 content unchanged but installation broken, repairing...`);
3207
+ spinner.start(`Repairing ${slug}...`);
3114
3208
  }
3115
3209
  let allHandled = true;
3116
3210
  if (format === "package") {
@@ -3193,7 +3287,17 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
3193
3287
  continue;
3194
3288
  }
3195
3289
  if (installation.method === "symlink") {
3196
- getPlatformFile(slug, installation.platform, skill);
3290
+ const cachePath = getPlatformFile(slug, installation.platform, skill);
3291
+ const installedPath = adapter.install({
3292
+ slug,
3293
+ content: transformed,
3294
+ scope: installation.scope,
3295
+ method: "symlink",
3296
+ cachePath,
3297
+ projectDir: installation.projectDir,
3298
+ contentType: skill.type
3299
+ });
3300
+ installation.path = installedPath;
3197
3301
  installation.format = "text";
3198
3302
  continue;
3199
3303
  }
@@ -3614,7 +3718,12 @@ var publishCommand = new Command6("publish").description("Publish local skills t
3614
3718
  const client = new ApiClient();
3615
3719
  requireAuth(client);
3616
3720
  const teamsRes = await client.get("/api/tenants");
3617
- if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
3721
+ if (!teamsRes.success || !teamsRes.data) {
3722
+ console.error(`Error: ${teamsRes.error || "Failed to fetch your teams."}`);
3723
+ process.exit(1);
3724
+ return;
3725
+ }
3726
+ if (teamsRes.data.length === 0) {
3618
3727
  console.error(
3619
3728
  "No teams found. Create a team at localskills.sh first."
3620
3729
  );
@@ -3940,6 +4049,7 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3940
4049
  }
3941
4050
  await ensureAnonymousIdentity();
3942
4051
  const client = new ApiClient();
4052
+ const tenantId = await resolveShareTenant(client);
3943
4053
  if (fileArg) {
3944
4054
  const filePath = resolve6(fileArg);
3945
4055
  if (!existsSync17(filePath)) {
@@ -3952,7 +4062,8 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3952
4062
  const ok2 = await uploadAnonymousPackage(client, {
3953
4063
  name: skillName2,
3954
4064
  dir: filePath,
3955
- type: contentType2
4065
+ type: contentType2,
4066
+ tenantId
3956
4067
  });
3957
4068
  Le(ok2 ? "Done!" : "Share failed.");
3958
4069
  if (!ok2) process.exit(1);
@@ -3968,7 +4079,7 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3968
4079
  const defaultName = titleFromSlug(defaultSlug);
3969
4080
  const skillName = opts.name || defaultName;
3970
4081
  const contentType = opts.type === "rule" ? "rule" : "skill";
3971
- const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
4082
+ const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType, tenantId });
3972
4083
  Le(ok ? "Done!" : "Share failed.");
3973
4084
  if (!ok) process.exit(1);
3974
4085
  return;
@@ -4008,11 +4119,13 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
4008
4119
  const ok = selected.format === "package" && selected.dir ? await uploadAnonymousPackage(client, {
4009
4120
  name,
4010
4121
  dir: selected.dir,
4011
- type: selected.contentType
4122
+ type: selected.contentType,
4123
+ tenantId
4012
4124
  }) : await uploadAnonymousSkill(client, {
4013
4125
  name,
4014
4126
  content: selected.content,
4015
- type: selected.contentType
4127
+ type: selected.contentType,
4128
+ tenantId
4016
4129
  });
4017
4130
  Le(ok ? "Done!" : "Share failed.");
4018
4131
  if (!ok) process.exit(1);
@@ -4060,19 +4173,36 @@ async function ensureAnonymousIdentity() {
4060
4173
  setToken(res.data.token);
4061
4174
  s.stop(`Connected as ${res.data.username}`);
4062
4175
  }
4063
- async function uploadAnonymousSkill(client, params) {
4176
+ async function resolveShareTenant(client) {
4064
4177
  const s = bt2();
4065
- s.start(`Sharing "${params.name}"...`);
4178
+ s.start("Looking up your team...");
4066
4179
  const teamsRes = await client.get("/api/tenants");
4067
4180
  if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
4068
- s.stop("Failed to find your team. Try running `localskills share` again.");
4181
+ s.stop(
4182
+ `Failed to find your team: ${teamsRes.error || "no teams"}. Try running \`localskills share\` again.`
4183
+ );
4069
4184
  process.exit(1);
4070
4185
  }
4071
- const tenantId = teamsRes.data[0].id;
4186
+ const teams = teamsRes.data;
4187
+ if (teams.length === 1) {
4188
+ s.stop("Team found.");
4189
+ return teams[0].id;
4190
+ }
4191
+ s.stop(`You belong to ${teams.length} teams.`);
4192
+ return cancelGuard(
4193
+ await Je({
4194
+ message: "Share under which team?",
4195
+ options: teams.map((t) => ({ value: t.id, label: t.name, hint: t.slug }))
4196
+ })
4197
+ );
4198
+ }
4199
+ async function uploadAnonymousSkill(client, params) {
4200
+ const s = bt2();
4201
+ s.start(`Sharing "${params.name}"...`);
4072
4202
  const res = await client.post("/api/skills", {
4073
4203
  name: params.name,
4074
4204
  content: params.content,
4075
- tenantId,
4205
+ tenantId: params.tenantId,
4076
4206
  visibility: "unlisted",
4077
4207
  type: params.type
4078
4208
  });
@@ -4096,16 +4226,9 @@ async function uploadAnonymousPackage(client, params) {
4096
4226
  }
4097
4227
  for (const w of packed.warnings) R2.warn(w);
4098
4228
  s.start(`Sharing "${params.name}" (${packed.fileCount} files)...`);
4099
- const teamsRes = await client.get("/api/tenants");
4100
- if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
4101
- s.stop("Failed to find your team. Try running `localskills share` again.");
4102
- process.exit(1);
4103
- return false;
4104
- }
4105
- const tenantId = teamsRes.data[0].id;
4106
4229
  const form = new FormData();
4107
4230
  form.append("name", params.name);
4108
- form.append("tenantId", tenantId);
4231
+ form.append("tenantId", params.tenantId);
4109
4232
  form.append("visibility", "unlisted");
4110
4233
  form.append("type", params.type);
4111
4234
  form.append("file", zipBlob(packed.zip), "skill.zip");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localskills/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "CLI for localskills.sh — install agent skills locally",
5
5
  "type": "module",
6
6
  "bin": {