@localskills/cli 0.12.0 → 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 +255 -124
  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
  }
@@ -1616,7 +1640,7 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
1616
1640
  // src/commands/install.ts
1617
1641
  import { Command as Command2 } from "commander";
1618
1642
  import { mkdirSync as mkdirSync7, rmSync as rmSync4, cpSync } from "fs";
1619
- import { dirname as dirname5 } from "path";
1643
+ import { dirname as dirname5, resolve as resolvePathAbs } from "path";
1620
1644
 
1621
1645
  // ../../packages/shared/dist/utils/semver.js
1622
1646
  var SEMVER_RE = /^\d+\.\d+\.\d+$/;
@@ -1669,14 +1693,17 @@ function requireAuth(client) {
1669
1693
  }
1670
1694
  }
1671
1695
  function buildVersionQuery(range) {
1672
- if (!range) return "";
1696
+ if (!range || range === "latest") return "";
1673
1697
  if (isValidSemVer(range)) {
1674
1698
  return `?semver=${encodeURIComponent(range)}`;
1675
1699
  }
1676
1700
  if (isValidSemVerRange(range)) {
1677
1701
  return `?range=${encodeURIComponent(range)}`;
1678
1702
  }
1679
- 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
+ );
1680
1707
  process.exit(1);
1681
1708
  }
1682
1709
  function formatVersionLabel(semver, version2) {
@@ -1703,14 +1730,17 @@ import { join as join13, resolve as resolve3 } from "path";
1703
1730
  import { homedir as homedir8 } from "os";
1704
1731
 
1705
1732
  // src/lib/installers/cursor.ts
1706
- import { existsSync as existsSync4 } from "fs";
1733
+ import { existsSync as existsSync5 } from "fs";
1707
1734
  import { join as join3 } from "path";
1708
1735
  import { homedir as homedir2 } from "os";
1709
1736
 
1710
1737
  // src/lib/content-transform.ts
1711
1738
  function yamlEscape(value) {
1712
- if (value.includes(":") || value.includes("#") || value.includes('"') || value.includes("'") || value.startsWith("{") || value.startsWith("[")) {
1713
- 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")}"`;
1714
1744
  }
1715
1745
  return value;
1716
1746
  }
@@ -1745,7 +1775,7 @@ function stripFrontmatter(content) {
1745
1775
  }
1746
1776
 
1747
1777
  // src/lib/installers/common.ts
1748
- 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";
1749
1779
  import { join as join2 } from "path";
1750
1780
 
1751
1781
  // src/lib/symlink.ts
@@ -1799,6 +1829,61 @@ function isSymlinkInto(linkPath, dir) {
1799
1829
  }
1800
1830
  }
1801
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
+
1802
1887
  // src/lib/installers/common.ts
1803
1888
  function safeSlugName(slug) {
1804
1889
  return slug.replace(/\//g, "-");
@@ -1808,18 +1893,26 @@ function installFileOrSymlink(opts, targetPath) {
1808
1893
  if (opts.method === "symlink") {
1809
1894
  createSymlink(opts.cachePath, targetPath);
1810
1895
  } else {
1811
- mkdirSync3(join2(targetPath, ".."), { recursive: true });
1812
- writeFileSync2(targetPath, opts.content);
1896
+ mkdirSync4(join2(targetPath, ".."), { recursive: true });
1897
+ removeSymlink(targetPath);
1898
+ writeFileSync3(targetPath, opts.content);
1813
1899
  }
1814
1900
  return targetPath;
1815
1901
  }
1816
1902
  function uninstallFile(installation) {
1817
1903
  if (installation.method === "symlink") {
1818
1904
  removeSymlink(installation.path);
1819
- } else if (existsSync3(installation.path)) {
1905
+ } else if (existsSync4(installation.path)) {
1820
1906
  unlinkSync2(installation.path);
1821
1907
  }
1822
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
+ }
1823
1916
  function defaultTransformContent(content) {
1824
1917
  return toPlainMD(content);
1825
1918
  }
@@ -1837,8 +1930,8 @@ function detect(projectDir) {
1837
1930
  const home = homedir2();
1838
1931
  const cwd = projectDir || process.cwd();
1839
1932
  return {
1840
- global: existsSync4(join3(home, ".cursor")),
1841
- project: existsSync4(join3(cwd, ".cursor"))
1933
+ global: existsSync5(join3(home, ".cursor")),
1934
+ project: existsSync5(join3(cwd, ".cursor"))
1842
1935
  };
1843
1936
  }
1844
1937
  function resolvePath(slug, scope, projectDir, _contentType) {
@@ -1873,7 +1966,7 @@ var cursorAdapter = {
1873
1966
  };
1874
1967
 
1875
1968
  // src/lib/installers/claude.ts
1876
- import { existsSync as existsSync5, rmSync as rmSync2, statSync, readdirSync } from "fs";
1969
+ import { existsSync as existsSync6, rmSync as rmSync2, statSync, readdirSync } from "fs";
1877
1970
  import { join as join4 } from "path";
1878
1971
  import { homedir as homedir3 } from "os";
1879
1972
  var descriptor2 = {
@@ -1888,8 +1981,8 @@ function detect2(projectDir) {
1888
1981
  const home = homedir3();
1889
1982
  const cwd = projectDir || process.cwd();
1890
1983
  return {
1891
- global: existsSync5(join4(home, ".claude")),
1892
- project: existsSync5(join4(cwd, ".claude"))
1984
+ global: existsSync6(join4(home, ".claude")),
1985
+ project: existsSync6(join4(cwd, ".claude"))
1893
1986
  };
1894
1987
  }
1895
1988
  function claudeBase(scope, projectDir) {
@@ -1920,7 +2013,7 @@ function uninstall2(installation, _slug) {
1920
2013
  const target = installation.path;
1921
2014
  if (isSymlink(target)) {
1922
2015
  removeSymlink(target);
1923
- } else if (existsSync5(target)) {
2016
+ } else if (existsSync6(target)) {
1924
2017
  if (statSync(target).isDirectory()) {
1925
2018
  rmSync2(target, { recursive: true, force: true });
1926
2019
  } else {
@@ -1929,7 +2022,7 @@ function uninstall2(installation, _slug) {
1929
2022
  }
1930
2023
  const parentDir = join4(target, "..");
1931
2024
  try {
1932
- if (existsSync5(parentDir) && readdirSync(parentDir).length === 0) {
2025
+ if (existsSync6(parentDir) && readdirSync(parentDir).length === 0) {
1933
2026
  rmSync2(parentDir, { recursive: true });
1934
2027
  }
1935
2028
  } catch {
@@ -1954,13 +2047,14 @@ import { join as join6 } from "path";
1954
2047
  import { homedir as homedir5 } from "os";
1955
2048
 
1956
2049
  // src/lib/detect.ts
1957
- import { existsSync as existsSync6 } from "fs";
2050
+ import { existsSync as existsSync7 } from "fs";
1958
2051
  import { execFileSync } from "child_process";
1959
2052
  import { homedir as homedir4 } from "os";
1960
2053
  import { join as join5 } from "path";
1961
2054
  function commandExists(cmd) {
1962
2055
  try {
1963
- execFileSync("which", [cmd], { stdio: "ignore" });
2056
+ const finder = process.platform === "win32" ? "where" : "which";
2057
+ execFileSync(finder, [cmd], { stdio: "ignore" });
1964
2058
  return true;
1965
2059
  } catch {
1966
2060
  return false;
@@ -1970,76 +2064,21 @@ function detectInstalledPlatforms(projectDir) {
1970
2064
  const detected = [];
1971
2065
  const home = homedir4();
1972
2066
  const cwd = projectDir || process.cwd();
1973
- if (existsSync6(join5(home, ".cursor")) || existsSync6(join5(cwd, ".cursor")))
2067
+ if (existsSync7(join5(home, ".cursor")) || existsSync7(join5(cwd, ".cursor")))
1974
2068
  detected.push("cursor");
1975
- if (existsSync6(join5(home, ".claude")) || commandExists("claude"))
2069
+ if (existsSync7(join5(home, ".claude")) || commandExists("claude"))
1976
2070
  detected.push("claude");
1977
2071
  if (commandExists("codex")) detected.push("codex");
1978
- if (existsSync6(join5(home, ".codeium")) || existsSync6(join5(cwd, ".windsurf")))
2072
+ if (existsSync7(join5(home, ".codeium")) || existsSync7(join5(cwd, ".windsurf")))
1979
2073
  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")))
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")))
1983
2077
  detected.push("opencode");
1984
2078
  if (commandExists("aider")) detected.push("aider");
1985
2079
  return detected;
1986
2080
  }
1987
2081
 
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
2082
  // src/lib/installers/codex.ts
2044
2083
  var descriptor3 = {
2045
2084
  id: "codex",
@@ -2474,6 +2513,16 @@ function getPlatformFile(slug, platform, skill) {
2474
2513
  writeFileSync6(filePath, transformed);
2475
2514
  return filePath;
2476
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
+ }
2477
2526
  function getRawContent(slug) {
2478
2527
  const filePath = join13(getCacheDir(slug), "raw.md");
2479
2528
  if (!existsSync13(filePath)) return null;
@@ -2607,13 +2656,14 @@ async function interactiveTargets(detectedPlatforms) {
2607
2656
  }));
2608
2657
  return { platforms, scope, method };
2609
2658
  }
2610
- async function interactiveUninstall(installedSlugs) {
2659
+ async function interactiveUninstall(installedSkills) {
2611
2660
  We("localskills uninstall");
2612
2661
  return cancelGuard(await Je({
2613
2662
  message: "Which skill would you like to uninstall?",
2614
- options: installedSlugs.map((s) => ({
2615
- value: s,
2616
- 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
2617
2667
  }))
2618
2668
  }));
2619
2669
  }
@@ -2653,7 +2703,9 @@ function parsePlatforms(raw) {
2653
2703
  }
2654
2704
  function buildSkillRecord(cacheKey, skill, version2, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
2655
2705
  return {
2656
- 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,
2657
2709
  name: skill.name,
2658
2710
  type: skill.type ?? "skill",
2659
2711
  hash: skill.contentHash,
@@ -2701,7 +2753,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2701
2753
  let method;
2702
2754
  let projectDir;
2703
2755
  if (typeof opts.project === "string") {
2704
- projectDir = opts.project;
2756
+ projectDir = resolvePathAbs(opts.project);
2705
2757
  }
2706
2758
  const explicitPlatforms = parsePlatforms(opts.target);
2707
2759
  const explicitScope = opts.global ? "global" : opts.project !== void 0 ? "project" : null;
@@ -2718,7 +2770,12 @@ var installCommand = new Command2("install").description("Install a skill locall
2718
2770
  spinner2.start("Fetching available skills...");
2719
2771
  const res2 = await client.get("/api/skills");
2720
2772
  spinner2.stop("Fetched skills.");
2721
- 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) {
2722
2779
  console.error("No skills available.");
2723
2780
  process.exit(1);
2724
2781
  return;
@@ -2747,6 +2804,9 @@ var installCommand = new Command2("install").description("Install a skill locall
2747
2804
  const atIdx = slug.lastIndexOf("@");
2748
2805
  requestedRange = slug.substring(atIdx + 1);
2749
2806
  slug = slug.substring(0, atIdx);
2807
+ if (requestedRange === "" || requestedRange === "latest") {
2808
+ requestedRange = null;
2809
+ }
2750
2810
  }
2751
2811
  const versionQuery = buildVersionQuery(requestedRange);
2752
2812
  const spinner = bt2();
@@ -2866,6 +2926,11 @@ var installCommand = new Command2("install").description("Install a skill locall
2866
2926
  const { skill, content, version: version2, semver: resolvedSemver } = resData;
2867
2927
  spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version2)}`);
2868
2928
  store(cacheKey, content, skill, version2);
2929
+ regenerateTextPlatformFiles(
2930
+ cacheKey,
2931
+ config.installed_skills[cacheKey]?.installations ?? [],
2932
+ skill
2933
+ );
2869
2934
  const contentType = skill.type ?? "skill";
2870
2935
  const installations = [];
2871
2936
  const results = [];
@@ -2918,6 +2983,11 @@ var installCommand = new Command2("install").description("Install a skill locall
2918
2983
  const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
2919
2984
  results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
2920
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
+ }
2921
2991
  config.installed_skills[cacheKey] = buildSkillRecord(
2922
2992
  cacheKey,
2923
2993
  skill,
@@ -2939,25 +3009,26 @@ var installCommand = new Command2("install").description("Install a skill locall
2939
3009
  import { Command as Command3 } from "commander";
2940
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) => {
2941
3011
  const config = loadConfig();
2942
- const installedSlugs = Object.keys(config.installed_skills);
2943
- if (installedSlugs.length === 0) {
3012
+ if (Object.keys(config.installed_skills).length === 0) {
2944
3013
  console.log("No installed skills.");
2945
3014
  return;
2946
3015
  }
2947
3016
  let slug;
2948
3017
  if (slugArg) {
2949
- 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;
2950
3025
  } else {
2951
- slug = await interactiveUninstall(installedSlugs);
3026
+ slug = await interactiveUninstall(config.installed_skills);
2952
3027
  }
2953
3028
  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
3029
  We(`localskills uninstall ${slug}`);
2960
3030
  let removed = 0;
3031
+ const failed = [];
2961
3032
  for (const installation of installed.installations) {
2962
3033
  try {
2963
3034
  const adapter = getAdapter(installation.platform);
@@ -2970,13 +3041,26 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
2970
3041
  R2.warn(
2971
3042
  `${installation.platform} \u2014 failed to remove: ${err instanceof Error ? err.message : String(err)}`
2972
3043
  );
3044
+ failed.push(installation);
2973
3045
  }
2974
3046
  }
2975
- 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
+ }
2976
3056
  saveConfig(config);
2977
3057
  if (opts?.purge) {
2978
- purge(slug);
2979
- 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
+ }
2980
3064
  }
2981
3065
  Le(
2982
3066
  `Uninstalled ${slug} from ${removed} target(s).`
@@ -3079,7 +3163,18 @@ function installedAsPackage(inst) {
3079
3163
  }
3080
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) => {
3081
3165
  const config = loadConfig();
3082
- 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
+ }
3083
3178
  if (slugs.length === 0) {
3084
3179
  console.log("No installed skills. Use `localskills install` first.");
3085
3180
  return;
@@ -3108,9 +3203,16 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
3108
3203
  const format = resData.format ?? "text";
3109
3204
  const { skill, version: version2 } = resData;
3110
3205
  if (skill.contentHash === installed.hash) {
3111
- spinner.stop(`${slug} \u2014 up to date`);
3112
- skipped++;
3113
- 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}...`);
3114
3216
  }
3115
3217
  let allHandled = true;
3116
3218
  if (format === "package") {
@@ -3193,7 +3295,17 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
3193
3295
  continue;
3194
3296
  }
3195
3297
  if (installation.method === "symlink") {
3196
- 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;
3197
3309
  installation.format = "text";
3198
3310
  continue;
3199
3311
  }
@@ -3614,7 +3726,12 @@ var publishCommand = new Command6("publish").description("Publish local skills t
3614
3726
  const client = new ApiClient();
3615
3727
  requireAuth(client);
3616
3728
  const teamsRes = await client.get("/api/tenants");
3617
- 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) {
3618
3735
  console.error(
3619
3736
  "No teams found. Create a team at localskills.sh first."
3620
3737
  );
@@ -3940,6 +4057,7 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3940
4057
  }
3941
4058
  await ensureAnonymousIdentity();
3942
4059
  const client = new ApiClient();
4060
+ const tenantId = await resolveShareTenant(client);
3943
4061
  if (fileArg) {
3944
4062
  const filePath = resolve6(fileArg);
3945
4063
  if (!existsSync17(filePath)) {
@@ -3952,7 +4070,8 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3952
4070
  const ok2 = await uploadAnonymousPackage(client, {
3953
4071
  name: skillName2,
3954
4072
  dir: filePath,
3955
- type: contentType2
4073
+ type: contentType2,
4074
+ tenantId
3956
4075
  });
3957
4076
  Le(ok2 ? "Done!" : "Share failed.");
3958
4077
  if (!ok2) process.exit(1);
@@ -3968,7 +4087,7 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
3968
4087
  const defaultName = titleFromSlug(defaultSlug);
3969
4088
  const skillName = opts.name || defaultName;
3970
4089
  const contentType = opts.type === "rule" ? "rule" : "skill";
3971
- const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
4090
+ const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType, tenantId });
3972
4091
  Le(ok ? "Done!" : "Share failed.");
3973
4092
  if (!ok) process.exit(1);
3974
4093
  return;
@@ -4008,11 +4127,13 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
4008
4127
  const ok = selected.format === "package" && selected.dir ? await uploadAnonymousPackage(client, {
4009
4128
  name,
4010
4129
  dir: selected.dir,
4011
- type: selected.contentType
4130
+ type: selected.contentType,
4131
+ tenantId
4012
4132
  }) : await uploadAnonymousSkill(client, {
4013
4133
  name,
4014
4134
  content: selected.content,
4015
- type: selected.contentType
4135
+ type: selected.contentType,
4136
+ tenantId
4016
4137
  });
4017
4138
  Le(ok ? "Done!" : "Share failed.");
4018
4139
  if (!ok) process.exit(1);
@@ -4060,19 +4181,36 @@ async function ensureAnonymousIdentity() {
4060
4181
  setToken(res.data.token);
4061
4182
  s.stop(`Connected as ${res.data.username}`);
4062
4183
  }
4063
- async function uploadAnonymousSkill(client, params) {
4184
+ async function resolveShareTenant(client) {
4064
4185
  const s = bt2();
4065
- s.start(`Sharing "${params.name}"...`);
4186
+ s.start("Looking up your team...");
4066
4187
  const teamsRes = await client.get("/api/tenants");
4067
4188
  if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
4068
- 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
+ );
4069
4192
  process.exit(1);
4070
4193
  }
4071
- 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}"...`);
4072
4210
  const res = await client.post("/api/skills", {
4073
4211
  name: params.name,
4074
4212
  content: params.content,
4075
- tenantId,
4213
+ tenantId: params.tenantId,
4076
4214
  visibility: "unlisted",
4077
4215
  type: params.type
4078
4216
  });
@@ -4096,16 +4234,9 @@ async function uploadAnonymousPackage(client, params) {
4096
4234
  }
4097
4235
  for (const w of packed.warnings) R2.warn(w);
4098
4236
  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
4237
  const form = new FormData();
4107
4238
  form.append("name", params.name);
4108
- form.append("tenantId", tenantId);
4239
+ form.append("tenantId", params.tenantId);
4109
4240
  form.append("visibility", "unlisted");
4110
4241
  form.append("type", params.type);
4111
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.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "CLI for localskills.sh — install agent skills locally",
5
5
  "type": "module",
6
6
  "bin": {