@localskills/cli 0.11.2 → 0.12.1

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 +288 -130
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1144,7 +1144,7 @@ ${l}
1144
1144
  } }).prompt();
1145
1145
 
1146
1146
  // src/lib/config.ts
1147
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from "fs";
1147
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, renameSync } from "fs";
1148
1148
  import { join } from "path";
1149
1149
  import { homedir } from "os";
1150
1150
  var CONFIG_DIR = join(homedir(), ".localskills");
@@ -1214,6 +1214,21 @@ function validateV3(config) {
1214
1214
  if (!config.profiles[DEFAULT_PROFILE_NAME]) {
1215
1215
  config.profiles[DEFAULT_PROFILE_NAME] = { ...DEFAULT_PROFILE, installed_skills: {} };
1216
1216
  }
1217
+ for (const name of Object.keys(config.profiles)) {
1218
+ const profile = config.profiles[name];
1219
+ if (!profile || typeof profile !== "object") {
1220
+ config.profiles[name] = { ...DEFAULT_PROFILE, installed_skills: {} };
1221
+ continue;
1222
+ }
1223
+ if (!profile.installed_skills || typeof profile.installed_skills !== "object") {
1224
+ profile.installed_skills = {};
1225
+ }
1226
+ if (!profile.defaults || typeof profile.defaults !== "object") {
1227
+ profile.defaults = { ...DEFAULT_PROFILE.defaults };
1228
+ }
1229
+ if (profile.token === void 0) profile.token = null;
1230
+ if (profile.anonymous_key === void 0) profile.anonymous_key = null;
1231
+ }
1217
1232
  if (!config.active_profile || !config.profiles[config.active_profile]) {
1218
1233
  config.active_profile = DEFAULT_PROFILE_NAME;
1219
1234
  }
@@ -1266,9 +1281,11 @@ function loadFullConfig() {
1266
1281
  }
1267
1282
  function saveFullConfig(config) {
1268
1283
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1269
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
1284
+ const tmpPath = `${CONFIG_PATH}.${process.pid}.tmp`;
1285
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", {
1270
1286
  mode: 384
1271
1287
  });
1288
+ renameSync(tmpPath, CONFIG_PATH);
1272
1289
  try {
1273
1290
  chmodSync(CONFIG_DIR, 448);
1274
1291
  chmodSync(CONFIG_PATH, 384);
@@ -1349,6 +1366,13 @@ function migrateV2toV3(v2) {
1349
1366
  }
1350
1367
  };
1351
1368
  }
1369
+ function resolveInstalledSkillKey(config, ref) {
1370
+ if (config.installed_skills[ref]) return ref;
1371
+ for (const [key, record] of Object.entries(config.installed_skills)) {
1372
+ if (record.slug === ref) return key;
1373
+ }
1374
+ return null;
1375
+ }
1352
1376
  function getToken() {
1353
1377
  return loadConfig().token;
1354
1378
  }
@@ -1572,6 +1596,11 @@ var loginCommand = new Command("login").description("Log in to localskills.sh").
1572
1596
  Le("Done!");
1573
1597
  return;
1574
1598
  }
1599
+ if (pollRes.data.status === "denied") {
1600
+ pollSpinner.stop("Authorization denied in the browser.");
1601
+ process.exit(1);
1602
+ return;
1603
+ }
1575
1604
  if (pollRes.data.status === "expired" || pollRes.data.status === "not_found") {
1576
1605
  pollSpinner.stop("Login expired. Please try again.");
1577
1606
  process.exit(1);
@@ -1611,7 +1640,7 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
1611
1640
  // src/commands/install.ts
1612
1641
  import { Command as Command2 } from "commander";
1613
1642
  import { mkdirSync as mkdirSync7, rmSync as rmSync4, cpSync } from "fs";
1614
- import { dirname as dirname5 } from "path";
1643
+ import { dirname as dirname5, resolve as resolvePathAbs } from "path";
1615
1644
 
1616
1645
  // ../../packages/shared/dist/utils/semver.js
1617
1646
  var SEMVER_RE = /^\d+\.\d+\.\d+$/;
@@ -1664,14 +1693,17 @@ function requireAuth(client) {
1664
1693
  }
1665
1694
  }
1666
1695
  function buildVersionQuery(range) {
1667
- if (!range) return "";
1696
+ if (!range || range === "latest") return "";
1668
1697
  if (isValidSemVer(range)) {
1669
1698
  return `?semver=${encodeURIComponent(range)}`;
1670
1699
  }
1671
1700
  if (isValidSemVerRange(range)) {
1672
1701
  return `?range=${encodeURIComponent(range)}`;
1673
1702
  }
1674
- console.error(`Invalid version specifier: ${range}`);
1703
+ console.error(
1704
+ `Invalid version specifier: ${range}
1705
+ Expected an exact version (1.2.3), a range (^1.2.3, ~1.2.3, >=1.2.3), "*", or "latest".`
1706
+ );
1675
1707
  process.exit(1);
1676
1708
  }
1677
1709
  function formatVersionLabel(semver, version2) {
@@ -1698,14 +1730,17 @@ import { join as join13, resolve as resolve3 } from "path";
1698
1730
  import { homedir as homedir8 } from "os";
1699
1731
 
1700
1732
  // src/lib/installers/cursor.ts
1701
- import { existsSync as existsSync4 } from "fs";
1733
+ import { existsSync as existsSync5 } from "fs";
1702
1734
  import { join as join3 } from "path";
1703
1735
  import { homedir as homedir2 } from "os";
1704
1736
 
1705
1737
  // src/lib/content-transform.ts
1706
1738
  function yamlEscape(value) {
1707
- if (value.includes(":") || value.includes("#") || value.includes('"') || value.includes("'") || value.startsWith("{") || value.startsWith("[")) {
1708
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1739
+ const needsQuoting = /[\r\n:#"']/.test(value) || // structural characters anywhere
1740
+ /^[\s\-?*&!|>%@`{[\]}]/.test(value) || // YAML indicator at the start
1741
+ /\s$/.test(value);
1742
+ if (needsQuoting) {
1743
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n")}"`;
1709
1744
  }
1710
1745
  return value;
1711
1746
  }
@@ -1740,7 +1775,7 @@ function stripFrontmatter(content) {
1740
1775
  }
1741
1776
 
1742
1777
  // src/lib/installers/common.ts
1743
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1778
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
1744
1779
  import { join as join2 } from "path";
1745
1780
 
1746
1781
  // src/lib/symlink.ts
@@ -1794,6 +1829,61 @@ function isSymlinkInto(linkPath, dir) {
1794
1829
  }
1795
1830
  }
1796
1831
 
1832
+ // src/lib/marked-sections.ts
1833
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
1834
+ import { dirname as dirname2 } from "path";
1835
+ var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
1836
+ var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
1837
+ function upsertSection(filePath, slug, content) {
1838
+ mkdirSync3(dirname2(filePath), { recursive: true });
1839
+ let existing = "";
1840
+ if (existsSync3(filePath)) {
1841
+ existing = readFileSync2(filePath, "utf-8");
1842
+ }
1843
+ const start = START_MARKER(slug);
1844
+ const end = END_MARKER(slug);
1845
+ const section = `${start}
1846
+ ${content}
1847
+ ${end}`;
1848
+ const startIdx = existing.indexOf(start);
1849
+ const endIdx = startIdx === -1 ? -1 : existing.indexOf(end, startIdx + start.length);
1850
+ let result;
1851
+ if (startIdx !== -1 && endIdx !== -1) {
1852
+ result = existing.slice(0, startIdx) + section + existing.slice(endIdx + end.length);
1853
+ } else {
1854
+ const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
1855
+ result = existing + separator + section + "\n";
1856
+ }
1857
+ writeFileSync2(filePath, result);
1858
+ }
1859
+ function removeSection(filePath, slug) {
1860
+ if (!existsSync3(filePath)) return false;
1861
+ const existing = readFileSync2(filePath, "utf-8");
1862
+ const start = START_MARKER(slug);
1863
+ const end = END_MARKER(slug);
1864
+ const startIdx = existing.indexOf(start);
1865
+ const endIdx = startIdx === -1 ? -1 : existing.indexOf(end, startIdx + start.length);
1866
+ if (startIdx === -1 || endIdx === -1) return false;
1867
+ let before = existing.slice(0, startIdx);
1868
+ let after = existing.slice(endIdx + end.length);
1869
+ while (before.endsWith("\n\n")) before = before.slice(0, -1);
1870
+ while (after.startsWith("\n\n")) after = after.slice(1);
1871
+ const result = (before + after).trim();
1872
+ writeFileSync2(filePath, result ? result + "\n" : "");
1873
+ return true;
1874
+ }
1875
+ function listSections(filePath) {
1876
+ if (!existsSync3(filePath)) return [];
1877
+ const content = readFileSync2(filePath, "utf-8");
1878
+ const regex = /<!-- localskills:start:(.+?) -->/g;
1879
+ const slugs = [];
1880
+ let match;
1881
+ while ((match = regex.exec(content)) !== null) {
1882
+ slugs.push(match[1]);
1883
+ }
1884
+ return slugs;
1885
+ }
1886
+
1797
1887
  // src/lib/installers/common.ts
1798
1888
  function safeSlugName(slug) {
1799
1889
  return slug.replace(/\//g, "-");
@@ -1803,18 +1893,26 @@ function installFileOrSymlink(opts, targetPath) {
1803
1893
  if (opts.method === "symlink") {
1804
1894
  createSymlink(opts.cachePath, targetPath);
1805
1895
  } else {
1806
- mkdirSync3(join2(targetPath, ".."), { recursive: true });
1807
- writeFileSync2(targetPath, opts.content);
1896
+ mkdirSync4(join2(targetPath, ".."), { recursive: true });
1897
+ removeSymlink(targetPath);
1898
+ writeFileSync3(targetPath, opts.content);
1808
1899
  }
1809
1900
  return targetPath;
1810
1901
  }
1811
1902
  function uninstallFile(installation) {
1812
1903
  if (installation.method === "symlink") {
1813
1904
  removeSymlink(installation.path);
1814
- } else if (existsSync3(installation.path)) {
1905
+ } else if (existsSync4(installation.path)) {
1815
1906
  unlinkSync2(installation.path);
1816
1907
  }
1817
1908
  }
1909
+ function installationIntact(installation, slug) {
1910
+ if (!existsSync4(installation.path)) return false;
1911
+ if (installation.method === "section") {
1912
+ return listSections(installation.path).includes(slug);
1913
+ }
1914
+ return true;
1915
+ }
1818
1916
  function defaultTransformContent(content) {
1819
1917
  return toPlainMD(content);
1820
1918
  }
@@ -1832,8 +1930,8 @@ function detect(projectDir) {
1832
1930
  const home = homedir2();
1833
1931
  const cwd = projectDir || process.cwd();
1834
1932
  return {
1835
- global: existsSync4(join3(home, ".cursor")),
1836
- project: existsSync4(join3(cwd, ".cursor"))
1933
+ global: existsSync5(join3(home, ".cursor")),
1934
+ project: existsSync5(join3(cwd, ".cursor"))
1837
1935
  };
1838
1936
  }
1839
1937
  function resolvePath(slug, scope, projectDir, _contentType) {
@@ -1868,7 +1966,7 @@ var cursorAdapter = {
1868
1966
  };
1869
1967
 
1870
1968
  // src/lib/installers/claude.ts
1871
- import { existsSync as existsSync5, rmSync as rmSync2, statSync, readdirSync } from "fs";
1969
+ import { existsSync as existsSync6, rmSync as rmSync2, statSync, readdirSync } from "fs";
1872
1970
  import { join as join4 } from "path";
1873
1971
  import { homedir as homedir3 } from "os";
1874
1972
  var descriptor2 = {
@@ -1883,8 +1981,8 @@ function detect2(projectDir) {
1883
1981
  const home = homedir3();
1884
1982
  const cwd = projectDir || process.cwd();
1885
1983
  return {
1886
- global: existsSync5(join4(home, ".claude")),
1887
- project: existsSync5(join4(cwd, ".claude"))
1984
+ global: existsSync6(join4(home, ".claude")),
1985
+ project: existsSync6(join4(cwd, ".claude"))
1888
1986
  };
1889
1987
  }
1890
1988
  function claudeBase(scope, projectDir) {
@@ -1915,7 +2013,7 @@ function uninstall2(installation, _slug) {
1915
2013
  const target = installation.path;
1916
2014
  if (isSymlink(target)) {
1917
2015
  removeSymlink(target);
1918
- } else if (existsSync5(target)) {
2016
+ } else if (existsSync6(target)) {
1919
2017
  if (statSync(target).isDirectory()) {
1920
2018
  rmSync2(target, { recursive: true, force: true });
1921
2019
  } else {
@@ -1924,7 +2022,7 @@ function uninstall2(installation, _slug) {
1924
2022
  }
1925
2023
  const parentDir = join4(target, "..");
1926
2024
  try {
1927
- if (existsSync5(parentDir) && readdirSync(parentDir).length === 0) {
2025
+ if (existsSync6(parentDir) && readdirSync(parentDir).length === 0) {
1928
2026
  rmSync2(parentDir, { recursive: true });
1929
2027
  }
1930
2028
  } catch {
@@ -1949,13 +2047,14 @@ import { join as join6 } from "path";
1949
2047
  import { homedir as homedir5 } from "os";
1950
2048
 
1951
2049
  // src/lib/detect.ts
1952
- import { existsSync as existsSync6 } from "fs";
2050
+ import { existsSync as existsSync7 } from "fs";
1953
2051
  import { execFileSync } from "child_process";
1954
2052
  import { homedir as homedir4 } from "os";
1955
2053
  import { join as join5 } from "path";
1956
2054
  function commandExists(cmd) {
1957
2055
  try {
1958
- execFileSync("which", [cmd], { stdio: "ignore" });
2056
+ const finder = process.platform === "win32" ? "where" : "which";
2057
+ execFileSync(finder, [cmd], { stdio: "ignore" });
1959
2058
  return true;
1960
2059
  } catch {
1961
2060
  return false;
@@ -1965,76 +2064,21 @@ function detectInstalledPlatforms(projectDir) {
1965
2064
  const detected = [];
1966
2065
  const home = homedir4();
1967
2066
  const cwd = projectDir || process.cwd();
1968
- if (existsSync6(join5(home, ".cursor")) || existsSync6(join5(cwd, ".cursor")))
2067
+ if (existsSync7(join5(home, ".cursor")) || existsSync7(join5(cwd, ".cursor")))
1969
2068
  detected.push("cursor");
1970
- if (existsSync6(join5(home, ".claude")) || commandExists("claude"))
2069
+ if (existsSync7(join5(home, ".claude")) || commandExists("claude"))
1971
2070
  detected.push("claude");
1972
2071
  if (commandExists("codex")) detected.push("codex");
1973
- if (existsSync6(join5(home, ".codeium")) || existsSync6(join5(cwd, ".windsurf")))
2072
+ if (existsSync7(join5(home, ".codeium")) || existsSync7(join5(cwd, ".windsurf")))
1974
2073
  detected.push("windsurf");
1975
- if (existsSync6(join5(cwd, ".clinerules"))) detected.push("cline");
1976
- if (existsSync6(join5(cwd, ".github"))) detected.push("copilot");
1977
- if (commandExists("opencode") || existsSync6(join5(cwd, ".opencode")))
2074
+ if (existsSync7(join5(cwd, ".clinerules"))) detected.push("cline");
2075
+ if (existsSync7(join5(cwd, ".github"))) detected.push("copilot");
2076
+ if (commandExists("opencode") || existsSync7(join5(cwd, ".opencode")))
1978
2077
  detected.push("opencode");
1979
2078
  if (commandExists("aider")) detected.push("aider");
1980
2079
  return detected;
1981
2080
  }
1982
2081
 
1983
- // src/lib/marked-sections.ts
1984
- import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1985
- import { dirname as dirname2 } from "path";
1986
- var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
1987
- var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
1988
- function upsertSection(filePath, slug, content) {
1989
- mkdirSync4(dirname2(filePath), { recursive: true });
1990
- let existing = "";
1991
- if (existsSync7(filePath)) {
1992
- existing = readFileSync2(filePath, "utf-8");
1993
- }
1994
- const start = START_MARKER(slug);
1995
- const end = END_MARKER(slug);
1996
- const section = `${start}
1997
- ${content}
1998
- ${end}`;
1999
- const startIdx = existing.indexOf(start);
2000
- const endIdx = existing.indexOf(end);
2001
- let result;
2002
- if (startIdx !== -1 && endIdx !== -1) {
2003
- result = existing.slice(0, startIdx) + section + existing.slice(endIdx + end.length);
2004
- } else {
2005
- const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
2006
- result = existing + separator + section + "\n";
2007
- }
2008
- writeFileSync3(filePath, result);
2009
- }
2010
- function removeSection(filePath, slug) {
2011
- if (!existsSync7(filePath)) return false;
2012
- const existing = readFileSync2(filePath, "utf-8");
2013
- const start = START_MARKER(slug);
2014
- const end = END_MARKER(slug);
2015
- const startIdx = existing.indexOf(start);
2016
- const endIdx = existing.indexOf(end);
2017
- if (startIdx === -1 || endIdx === -1) return false;
2018
- let before = existing.slice(0, startIdx);
2019
- let after = existing.slice(endIdx + end.length);
2020
- while (before.endsWith("\n\n")) before = before.slice(0, -1);
2021
- while (after.startsWith("\n\n")) after = after.slice(1);
2022
- const result = (before + after).trim();
2023
- writeFileSync3(filePath, result ? result + "\n" : "");
2024
- return true;
2025
- }
2026
- function listSections(filePath) {
2027
- if (!existsSync7(filePath)) return [];
2028
- const content = readFileSync2(filePath, "utf-8");
2029
- const regex = /<!-- localskills:start:(.+?) -->/g;
2030
- const slugs = [];
2031
- let match;
2032
- while ((match = regex.exec(content)) !== null) {
2033
- slugs.push(match[1]);
2034
- }
2035
- return slugs;
2036
- }
2037
-
2038
2082
  // src/lib/installers/codex.ts
2039
2083
  var descriptor3 = {
2040
2084
  id: "codex",
@@ -2469,6 +2513,16 @@ function getPlatformFile(slug, platform, skill) {
2469
2513
  writeFileSync6(filePath, transformed);
2470
2514
  return filePath;
2471
2515
  }
2516
+ function regenerateTextPlatformFiles(slug, installations, skill) {
2517
+ for (const inst of installations) {
2518
+ if ((inst.format ?? "text") !== "text") continue;
2519
+ if (inst.method !== "symlink") continue;
2520
+ try {
2521
+ getPlatformFile(slug, inst.platform, skill);
2522
+ } catch {
2523
+ }
2524
+ }
2525
+ }
2472
2526
  function getRawContent(slug) {
2473
2527
  const filePath = join13(getCacheDir(slug), "raw.md");
2474
2528
  if (!existsSync13(filePath)) return null;
@@ -2602,13 +2656,14 @@ async function interactiveTargets(detectedPlatforms) {
2602
2656
  }));
2603
2657
  return { platforms, scope, method };
2604
2658
  }
2605
- async function interactiveUninstall(installedSlugs) {
2659
+ async function interactiveUninstall(installedSkills) {
2606
2660
  We("localskills uninstall");
2607
2661
  return cancelGuard(await Je({
2608
2662
  message: "Which skill would you like to uninstall?",
2609
- options: installedSlugs.map((s) => ({
2610
- value: s,
2611
- label: s
2663
+ options: Object.entries(installedSkills).map(([key, record]) => ({
2664
+ value: key,
2665
+ label: record.name || record.slug || key,
2666
+ hint: record.slug && record.slug !== key ? record.slug : void 0
2612
2667
  }))
2613
2668
  }));
2614
2669
  }
@@ -2648,7 +2703,9 @@ function parsePlatforms(raw) {
2648
2703
  }
2649
2704
  function buildSkillRecord(cacheKey, skill, version2, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
2650
2705
  return {
2651
- slug: cacheKey,
2706
+ // The human slug (when known), so `uninstall`/`pull <slug>` can resolve
2707
+ // records that are keyed by publicId.
2708
+ slug: skill.slug ?? cacheKey,
2652
2709
  name: skill.name,
2653
2710
  type: skill.type ?? "skill",
2654
2711
  hash: skill.contentHash,
@@ -2696,7 +2753,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2696
2753
  let method;
2697
2754
  let projectDir;
2698
2755
  if (typeof opts.project === "string") {
2699
- projectDir = opts.project;
2756
+ projectDir = resolvePathAbs(opts.project);
2700
2757
  }
2701
2758
  const explicitPlatforms = parsePlatforms(opts.target);
2702
2759
  const explicitScope = opts.global ? "global" : opts.project !== void 0 ? "project" : null;
@@ -2713,7 +2770,12 @@ var installCommand = new Command2("install").description("Install a skill locall
2713
2770
  spinner2.start("Fetching available skills...");
2714
2771
  const res2 = await client.get("/api/skills");
2715
2772
  spinner2.stop("Fetched skills.");
2716
- if (!res2.success || !res2.data || res2.data.length === 0) {
2773
+ if (!res2.success || !res2.data) {
2774
+ console.error(`Error: ${res2.error || "Failed to fetch skills."}`);
2775
+ process.exit(1);
2776
+ return;
2777
+ }
2778
+ if (res2.data.length === 0) {
2717
2779
  console.error("No skills available.");
2718
2780
  process.exit(1);
2719
2781
  return;
@@ -2742,6 +2804,9 @@ var installCommand = new Command2("install").description("Install a skill locall
2742
2804
  const atIdx = slug.lastIndexOf("@");
2743
2805
  requestedRange = slug.substring(atIdx + 1);
2744
2806
  slug = slug.substring(0, atIdx);
2807
+ if (requestedRange === "" || requestedRange === "latest") {
2808
+ requestedRange = null;
2809
+ }
2745
2810
  }
2746
2811
  const versionQuery = buildVersionQuery(requestedRange);
2747
2812
  const spinner = bt2();
@@ -2861,6 +2926,11 @@ var installCommand = new Command2("install").description("Install a skill locall
2861
2926
  const { skill, content, version: version2, semver: resolvedSemver } = resData;
2862
2927
  spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version2)}`);
2863
2928
  store(cacheKey, content, skill, version2);
2929
+ regenerateTextPlatformFiles(
2930
+ cacheKey,
2931
+ config.installed_skills[cacheKey]?.installations ?? [],
2932
+ skill
2933
+ );
2864
2934
  const contentType = skill.type ?? "skill";
2865
2935
  const installations = [];
2866
2936
  const results = [];
@@ -2913,6 +2983,11 @@ var installCommand = new Command2("install").description("Install a skill locall
2913
2983
  const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
2914
2984
  results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
2915
2985
  }
2986
+ if (installations.length === 0) {
2987
+ R2.error("Nothing was installed \u2014 no selected platform supports this scope.");
2988
+ process.exit(1);
2989
+ return;
2990
+ }
2916
2991
  config.installed_skills[cacheKey] = buildSkillRecord(
2917
2992
  cacheKey,
2918
2993
  skill,
@@ -2934,25 +3009,26 @@ var installCommand = new Command2("install").description("Install a skill locall
2934
3009
  import { Command as Command3 } from "commander";
2935
3010
  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) => {
2936
3011
  const config = loadConfig();
2937
- const installedSlugs = Object.keys(config.installed_skills);
2938
- if (installedSlugs.length === 0) {
3012
+ if (Object.keys(config.installed_skills).length === 0) {
2939
3013
  console.log("No installed skills.");
2940
3014
  return;
2941
3015
  }
2942
3016
  let slug;
2943
3017
  if (slugArg) {
2944
- slug = slugArg;
3018
+ const resolved = resolveInstalledSkillKey(config, slugArg);
3019
+ if (!resolved) {
3020
+ console.error(`Skill "${slugArg}" is not installed.`);
3021
+ process.exit(1);
3022
+ return;
3023
+ }
3024
+ slug = resolved;
2945
3025
  } else {
2946
- slug = await interactiveUninstall(installedSlugs);
3026
+ slug = await interactiveUninstall(config.installed_skills);
2947
3027
  }
2948
3028
  const installed = config.installed_skills[slug];
2949
- if (!installed) {
2950
- console.error(`Skill "${slug}" is not installed.`);
2951
- process.exit(1);
2952
- return;
2953
- }
2954
3029
  We(`localskills uninstall ${slug}`);
2955
3030
  let removed = 0;
3031
+ const failed = [];
2956
3032
  for (const installation of installed.installations) {
2957
3033
  try {
2958
3034
  const adapter = getAdapter(installation.platform);
@@ -2965,13 +3041,26 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
2965
3041
  R2.warn(
2966
3042
  `${installation.platform} \u2014 failed to remove: ${err instanceof Error ? err.message : String(err)}`
2967
3043
  );
3044
+ failed.push(installation);
2968
3045
  }
2969
3046
  }
2970
- delete config.installed_skills[slug];
3047
+ if (failed.length === 0) {
3048
+ delete config.installed_skills[slug];
3049
+ } else {
3050
+ installed.installations = failed;
3051
+ R2.warn(
3052
+ `${failed.length} target(s) could not be removed and remain tracked. Fix the issue and run \`localskills uninstall ${slug}\` again.`
3053
+ );
3054
+ process.exitCode = 1;
3055
+ }
2971
3056
  saveConfig(config);
2972
3057
  if (opts?.purge) {
2973
- purge(slug);
2974
- R2.info("Cache purged.");
3058
+ if (failed.length === 0) {
3059
+ purge(slug);
3060
+ R2.info("Cache purged.");
3061
+ } else {
3062
+ R2.warn("Skipped cache purge because some targets could not be removed.");
3063
+ }
2975
3064
  }
2976
3065
  Le(
2977
3066
  `Uninstalled ${slug} from ${removed} target(s).`
@@ -2980,6 +3069,28 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
2980
3069
 
2981
3070
  // src/commands/list.ts
2982
3071
  import { Command as Command4 } from "commander";
3072
+ function toTagArray(tags) {
3073
+ if (Array.isArray(tags)) return tags.filter((t) => typeof t === "string");
3074
+ if (typeof tags === "string" && tags.trim()) {
3075
+ try {
3076
+ const parsed = JSON.parse(tags);
3077
+ if (Array.isArray(parsed)) return parsed.filter((t) => typeof t === "string");
3078
+ return [tags];
3079
+ } catch {
3080
+ return [tags];
3081
+ }
3082
+ }
3083
+ return [];
3084
+ }
3085
+ function formatTags(tags) {
3086
+ const list = toTagArray(tags);
3087
+ return list.length > 0 ? ` [${list.join(", ")}]` : "";
3088
+ }
3089
+ function oneLine(text, max = 100) {
3090
+ if (!text) return "";
3091
+ const flat = text.replace(/\s+/g, " ").trim();
3092
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
3093
+ }
2983
3094
  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) => {
2984
3095
  const client = new ApiClient();
2985
3096
  if ((opts.tag || opts.search) && !opts.public) {
@@ -3005,8 +3116,8 @@ var listCommand = new Command4("list").description("List available skills").opti
3005
3116
  console.log("Public skills:\n");
3006
3117
  for (const skill of res.data) {
3007
3118
  const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
3008
- const tags = skill.tags.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
3009
- console.log(` ${skill.slug} ${ver}${tags} \u2014 ${skill.description || skill.name}`);
3119
+ const tags = formatTags(skill.tags);
3120
+ console.log(` ${skill.slug} ${ver}${tags} \u2014 ${oneLine(skill.description) || skill.name}`);
3010
3121
  }
3011
3122
  console.log(`
3012
3123
  ${res.data.length} skill(s) found.`);
@@ -3026,8 +3137,8 @@ ${res.data.length} skill(s) found.`);
3026
3137
  for (const skill of res.data) {
3027
3138
  const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
3028
3139
  const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
3029
- const tags = skill.tags?.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
3030
- console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${skill.description || skill.name}`);
3140
+ const tags = formatTags(skill.tags);
3141
+ console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${oneLine(skill.description) || skill.name}`);
3031
3142
  }
3032
3143
  console.log(`
3033
3144
  ${res.data.length} skill(s) found.`);
@@ -3052,7 +3163,18 @@ function installedAsPackage(inst) {
3052
3163
  }
3053
3164
  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) => {
3054
3165
  const config = loadConfig();
3055
- const slugs = slugArg ? [slugArg] : Object.keys(config.installed_skills);
3166
+ let slugs;
3167
+ if (slugArg) {
3168
+ const resolved = resolveInstalledSkillKey(config, slugArg);
3169
+ if (!resolved) {
3170
+ console.error(`Skill "${slugArg}" is not installed.`);
3171
+ process.exit(1);
3172
+ return;
3173
+ }
3174
+ slugs = [resolved];
3175
+ } else {
3176
+ slugs = Object.keys(config.installed_skills);
3177
+ }
3056
3178
  if (slugs.length === 0) {
3057
3179
  console.log("No installed skills. Use `localskills install` first.");
3058
3180
  return;
@@ -3081,9 +3203,16 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
3081
3203
  const format = resData.format ?? "text";
3082
3204
  const { skill, version: version2 } = resData;
3083
3205
  if (skill.contentHash === installed.hash) {
3084
- spinner.stop(`${slug} \u2014 up to date`);
3085
- skipped++;
3086
- continue;
3206
+ const broken = installed.installations.some(
3207
+ (inst) => !installationIntact(inst, slug)
3208
+ );
3209
+ if (!broken) {
3210
+ spinner.stop(`${slug} \u2014 up to date`);
3211
+ skipped++;
3212
+ continue;
3213
+ }
3214
+ spinner.stop(`${slug} \u2014 content unchanged but installation broken, repairing...`);
3215
+ spinner.start(`Repairing ${slug}...`);
3087
3216
  }
3088
3217
  let allHandled = true;
3089
3218
  if (format === "package") {
@@ -3166,7 +3295,17 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
3166
3295
  continue;
3167
3296
  }
3168
3297
  if (installation.method === "symlink") {
3169
- getPlatformFile(slug, installation.platform, skill);
3298
+ const cachePath = getPlatformFile(slug, installation.platform, skill);
3299
+ const installedPath = adapter.install({
3300
+ slug,
3301
+ content: transformed,
3302
+ scope: installation.scope,
3303
+ method: "symlink",
3304
+ cachePath,
3305
+ projectDir: installation.projectDir,
3306
+ contentType: skill.type
3307
+ });
3308
+ installation.path = installedPath;
3170
3309
  installation.format = "text";
3171
3310
  continue;
3172
3311
  }
@@ -3587,7 +3726,12 @@ var publishCommand = new Command6("publish").description("Publish local skills t
3587
3726
  const client = new ApiClient();
3588
3727
  requireAuth(client);
3589
3728
  const teamsRes = await client.get("/api/tenants");
3590
- if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
3729
+ if (!teamsRes.success || !teamsRes.data) {
3730
+ console.error(`Error: ${teamsRes.error || "Failed to fetch your teams."}`);
3731
+ process.exit(1);
3732
+ return;
3733
+ }
3734
+ if (teamsRes.data.length === 0) {
3591
3735
  console.error(
3592
3736
  "No teams found. Create a team at localskills.sh first."
3593
3737
  );
@@ -3746,7 +3890,7 @@ async function uploadSkill(client, params) {
3746
3890
  return false;
3747
3891
  }
3748
3892
  spinner.stop(`Published!`);
3749
- R2.success(`\u2192 localskills.sh/s/${res.data.slug}`);
3893
+ R2.success(`\u2192 localskills.sh/s/${res.data.publicId}`);
3750
3894
  return true;
3751
3895
  }
3752
3896
  async function uploadPackage(client, params) {
@@ -3772,7 +3916,7 @@ async function uploadPackage(client, params) {
3772
3916
  return false;
3773
3917
  }
3774
3918
  spinner.stop(`Published! (${packed.fileCount} files)`);
3775
- R2.success(`\u2192 localskills.sh/s/${res.data.slug}`);
3919
+ R2.success(`\u2192 localskills.sh/s/${res.data.publicId}`);
3776
3920
  return true;
3777
3921
  }
3778
3922
  function validateContentType(value) {
@@ -3913,6 +4057,7 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3913
4057
  }
3914
4058
  await ensureAnonymousIdentity();
3915
4059
  const client = new ApiClient();
4060
+ const tenantId = await resolveShareTenant(client);
3916
4061
  if (fileArg) {
3917
4062
  const filePath = resolve6(fileArg);
3918
4063
  if (!existsSync17(filePath)) {
@@ -3925,7 +4070,8 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3925
4070
  const ok2 = await uploadAnonymousPackage(client, {
3926
4071
  name: skillName2,
3927
4072
  dir: filePath,
3928
- type: contentType2
4073
+ type: contentType2,
4074
+ tenantId
3929
4075
  });
3930
4076
  Le(ok2 ? "Done!" : "Share failed.");
3931
4077
  if (!ok2) process.exit(1);
@@ -3941,7 +4087,7 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3941
4087
  const defaultName = titleFromSlug(defaultSlug);
3942
4088
  const skillName = opts.name || defaultName;
3943
4089
  const contentType = opts.type === "rule" ? "rule" : "skill";
3944
- const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
4090
+ const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType, tenantId });
3945
4091
  Le(ok ? "Done!" : "Share failed.");
3946
4092
  if (!ok) process.exit(1);
3947
4093
  return;
@@ -3981,11 +4127,13 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3981
4127
  const ok = selected.format === "package" && selected.dir ? await uploadAnonymousPackage(client, {
3982
4128
  name,
3983
4129
  dir: selected.dir,
3984
- type: selected.contentType
4130
+ type: selected.contentType,
4131
+ tenantId
3985
4132
  }) : await uploadAnonymousSkill(client, {
3986
4133
  name,
3987
4134
  content: selected.content,
3988
- type: selected.contentType
4135
+ type: selected.contentType,
4136
+ tenantId
3989
4137
  });
3990
4138
  Le(ok ? "Done!" : "Share failed.");
3991
4139
  if (!ok) process.exit(1);
@@ -4033,19 +4181,36 @@ async function ensureAnonymousIdentity() {
4033
4181
  setToken(res.data.token);
4034
4182
  s.stop(`Connected as ${res.data.username}`);
4035
4183
  }
4036
- async function uploadAnonymousSkill(client, params) {
4184
+ async function resolveShareTenant(client) {
4037
4185
  const s = bt2();
4038
- s.start(`Sharing "${params.name}"...`);
4186
+ s.start("Looking up your team...");
4039
4187
  const teamsRes = await client.get("/api/tenants");
4040
4188
  if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
4041
- s.stop("Failed to find your team. Try running `localskills share` again.");
4189
+ s.stop(
4190
+ `Failed to find your team: ${teamsRes.error || "no teams"}. Try running \`localskills share\` again.`
4191
+ );
4042
4192
  process.exit(1);
4043
4193
  }
4044
- const tenantId = teamsRes.data[0].id;
4194
+ const teams = teamsRes.data;
4195
+ if (teams.length === 1) {
4196
+ s.stop("Team found.");
4197
+ return teams[0].id;
4198
+ }
4199
+ s.stop(`You belong to ${teams.length} teams.`);
4200
+ return cancelGuard(
4201
+ await Je({
4202
+ message: "Share under which team?",
4203
+ options: teams.map((t) => ({ value: t.id, label: t.name, hint: t.slug }))
4204
+ })
4205
+ );
4206
+ }
4207
+ async function uploadAnonymousSkill(client, params) {
4208
+ const s = bt2();
4209
+ s.start(`Sharing "${params.name}"...`);
4045
4210
  const res = await client.post("/api/skills", {
4046
4211
  name: params.name,
4047
4212
  content: params.content,
4048
- tenantId,
4213
+ tenantId: params.tenantId,
4049
4214
  visibility: "unlisted",
4050
4215
  type: params.type
4051
4216
  });
@@ -4069,16 +4234,9 @@ async function uploadAnonymousPackage(client, params) {
4069
4234
  }
4070
4235
  for (const w of packed.warnings) R2.warn(w);
4071
4236
  s.start(`Sharing "${params.name}" (${packed.fileCount} files)...`);
4072
- const teamsRes = await client.get("/api/tenants");
4073
- if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
4074
- s.stop("Failed to find your team. Try running `localskills share` again.");
4075
- process.exit(1);
4076
- return false;
4077
- }
4078
- const tenantId = teamsRes.data[0].id;
4079
4237
  const form = new FormData();
4080
4238
  form.append("name", params.name);
4081
- form.append("tenantId", tenantId);
4239
+ form.append("tenantId", params.tenantId);
4082
4240
  form.append("visibility", "unlisted");
4083
4241
  form.append("type", params.type);
4084
4242
  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.11.2",
3
+ "version": "0.12.1",
4
4
  "description": "CLI for localskills.sh — install agent skills locally",
5
5
  "type": "module",
6
6
  "bin": {