@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.
- package/dist/index.js +262 -139
- 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
|
-
|
|
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"
|
|
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
|
|
1570
|
+
const pollInterval = Math.max(1, initRes.data.interval || 2) * 1e3;
|
|
1556
1571
|
while (Date.now() < expiresAt) {
|
|
1557
|
-
await sleep(
|
|
1572
|
+
await sleep(pollInterval);
|
|
1558
1573
|
try {
|
|
1559
|
-
const pollRes = await client.
|
|
1560
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1713
|
-
|
|
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
|
|
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
|
-
|
|
1812
|
-
|
|
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 (
|
|
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:
|
|
1841
|
-
project:
|
|
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
|
|
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:
|
|
1892
|
-
project:
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 (
|
|
2059
|
+
if (existsSync7(join5(home, ".cursor")) || existsSync7(join5(cwd, ".cursor")))
|
|
1974
2060
|
detected.push("cursor");
|
|
1975
|
-
if (
|
|
2061
|
+
if (existsSync7(join5(home, ".claude")) || commandExists("claude"))
|
|
1976
2062
|
detected.push("claude");
|
|
1977
2063
|
if (commandExists("codex")) detected.push("codex");
|
|
1978
|
-
if (
|
|
2064
|
+
if (existsSync7(join5(home, ".codeium")) || existsSync7(join5(cwd, ".windsurf")))
|
|
1979
2065
|
detected.push("windsurf");
|
|
1980
|
-
if (
|
|
1981
|
-
if (
|
|
1982
|
-
if (commandExists("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(
|
|
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:
|
|
2615
|
-
value:
|
|
2616
|
-
label:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2979
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
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
|
|
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
|
|
4176
|
+
async function resolveShareTenant(client) {
|
|
4064
4177
|
const s = bt2();
|
|
4065
|
-
s.start(
|
|
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(
|
|
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
|
|
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");
|