@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.
- package/dist/index.js +288 -130
- 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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1708
|
-
|
|
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
|
|
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
|
-
|
|
1807
|
-
|
|
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 (
|
|
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:
|
|
1836
|
-
project:
|
|
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
|
|
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:
|
|
1887
|
-
project:
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 (
|
|
2067
|
+
if (existsSync7(join5(home, ".cursor")) || existsSync7(join5(cwd, ".cursor")))
|
|
1969
2068
|
detected.push("cursor");
|
|
1970
|
-
if (
|
|
2069
|
+
if (existsSync7(join5(home, ".claude")) || commandExists("claude"))
|
|
1971
2070
|
detected.push("claude");
|
|
1972
2071
|
if (commandExists("codex")) detected.push("codex");
|
|
1973
|
-
if (
|
|
2072
|
+
if (existsSync7(join5(home, ".codeium")) || existsSync7(join5(cwd, ".windsurf")))
|
|
1974
2073
|
detected.push("windsurf");
|
|
1975
|
-
if (
|
|
1976
|
-
if (
|
|
1977
|
-
if (commandExists("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(
|
|
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:
|
|
2610
|
-
value:
|
|
2611
|
-
label:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2974
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
4184
|
+
async function resolveShareTenant(client) {
|
|
4037
4185
|
const s = bt2();
|
|
4038
|
-
s.start(
|
|
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(
|
|
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
|
|
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");
|