@localskills/cli 0.11.1 → 0.12.0
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/README.md +27 -6
- package/dist/index.js +831 -193
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,13 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var
|
|
9
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
-
}) : x3)(function(x3) {
|
|
11
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
-
throw Error('Dynamic require of "' + x3 + '" is not supported');
|
|
13
|
-
});
|
|
14
|
-
var __commonJS = (cb, mod) => function __require2() {
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
15
9
|
try {
|
|
16
10
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
17
11
|
} catch (e2) {
|
|
@@ -1400,6 +1394,9 @@ var ApiClient = class {
|
|
|
1400
1394
|
}
|
|
1401
1395
|
return h;
|
|
1402
1396
|
}
|
|
1397
|
+
authHeader() {
|
|
1398
|
+
return this.token ? { Authorization: `Bearer ${this.token}` } : {};
|
|
1399
|
+
}
|
|
1403
1400
|
async handleResponse(res) {
|
|
1404
1401
|
if (res.status === 401) {
|
|
1405
1402
|
return {
|
|
@@ -1409,19 +1406,45 @@ var ApiClient = class {
|
|
|
1409
1406
|
}
|
|
1410
1407
|
return res.json();
|
|
1411
1408
|
}
|
|
1409
|
+
/** Turn a transport-level rejection (offline, DNS, TLS, reset, bad body) into a failure response. */
|
|
1410
|
+
transportError(err) {
|
|
1411
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1412
|
+
return { success: false, error: `Network error: ${detail}`, networkError: true };
|
|
1413
|
+
}
|
|
1412
1414
|
async get(path) {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1415
|
+
try {
|
|
1416
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
1417
|
+
headers: this.headers()
|
|
1418
|
+
});
|
|
1419
|
+
return await this.handleResponse(res);
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
return this.transportError(err);
|
|
1422
|
+
}
|
|
1417
1423
|
}
|
|
1418
1424
|
async post(path, body) {
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
+
try {
|
|
1426
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
1427
|
+
method: "POST",
|
|
1428
|
+
headers: this.headers(),
|
|
1429
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1430
|
+
});
|
|
1431
|
+
return await this.handleResponse(res);
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
return this.transportError(err);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async postForm(path, form) {
|
|
1437
|
+
try {
|
|
1438
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
1439
|
+
method: "POST",
|
|
1440
|
+
// No Content-Type — fetch sets the multipart/form-data boundary.
|
|
1441
|
+
headers: this.authHeader(),
|
|
1442
|
+
body: form
|
|
1443
|
+
});
|
|
1444
|
+
return await this.handleResponse(res);
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
return this.transportError(err);
|
|
1447
|
+
}
|
|
1425
1448
|
}
|
|
1426
1449
|
async fetchBinary(url) {
|
|
1427
1450
|
const headers = {};
|
|
@@ -1549,6 +1572,11 @@ var loginCommand = new Command("login").description("Log in to localskills.sh").
|
|
|
1549
1572
|
Le("Done!");
|
|
1550
1573
|
return;
|
|
1551
1574
|
}
|
|
1575
|
+
if (pollRes.data.status === "denied") {
|
|
1576
|
+
pollSpinner.stop("Authorization denied in the browser.");
|
|
1577
|
+
process.exit(1);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1552
1580
|
if (pollRes.data.status === "expired" || pollRes.data.status === "not_found") {
|
|
1553
1581
|
pollSpinner.stop("Login expired. Please try again.");
|
|
1554
1582
|
process.exit(1);
|
|
@@ -1587,7 +1615,8 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
|
|
|
1587
1615
|
|
|
1588
1616
|
// src/commands/install.ts
|
|
1589
1617
|
import { Command as Command2 } from "commander";
|
|
1590
|
-
import { mkdirSync as mkdirSync7 } from "fs";
|
|
1618
|
+
import { mkdirSync as mkdirSync7, rmSync as rmSync4, cpSync } from "fs";
|
|
1619
|
+
import { dirname as dirname5 } from "path";
|
|
1591
1620
|
|
|
1592
1621
|
// ../../packages/shared/dist/utils/semver.js
|
|
1593
1622
|
var SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
@@ -1624,6 +1653,10 @@ function isGreaterThan(next, prev) {
|
|
|
1624
1653
|
}
|
|
1625
1654
|
|
|
1626
1655
|
// ../../packages/shared/dist/utils/index.js
|
|
1656
|
+
var PACKAGE_MAX_COMPRESSED_BYTES = 1 * 1024 * 1024;
|
|
1657
|
+
var PACKAGE_MAX_DECOMPRESSED_BYTES = 1 * 1024 * 1024;
|
|
1658
|
+
var PACKAGE_MAX_FILE_COUNT = 500;
|
|
1659
|
+
var PACKAGE_PREVIEW_BYTES = 2048;
|
|
1627
1660
|
function titleFromSlug(slug) {
|
|
1628
1661
|
return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1629
1662
|
}
|
|
@@ -1660,13 +1693,13 @@ function cancelGuard(value) {
|
|
|
1660
1693
|
// src/lib/cache.ts
|
|
1661
1694
|
import {
|
|
1662
1695
|
existsSync as existsSync13,
|
|
1663
|
-
mkdirSync as
|
|
1696
|
+
mkdirSync as mkdirSync6,
|
|
1664
1697
|
readFileSync as readFileSync4,
|
|
1665
|
-
readdirSync,
|
|
1666
|
-
writeFileSync as
|
|
1698
|
+
readdirSync as readdirSync2,
|
|
1699
|
+
writeFileSync as writeFileSync6,
|
|
1667
1700
|
rmSync as rmSync3
|
|
1668
1701
|
} from "fs";
|
|
1669
|
-
import { join as
|
|
1702
|
+
import { join as join13, resolve as resolve3 } from "path";
|
|
1670
1703
|
import { homedir as homedir8 } from "os";
|
|
1671
1704
|
|
|
1672
1705
|
// src/lib/installers/cursor.ts
|
|
@@ -1722,9 +1755,10 @@ import {
|
|
|
1722
1755
|
lstatSync,
|
|
1723
1756
|
existsSync as existsSync2,
|
|
1724
1757
|
mkdirSync as mkdirSync2,
|
|
1725
|
-
rmSync
|
|
1758
|
+
rmSync,
|
|
1759
|
+
readlinkSync
|
|
1726
1760
|
} from "fs";
|
|
1727
|
-
import { dirname, resolve } from "path";
|
|
1761
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
1728
1762
|
function createSymlink(targetPath, linkPath) {
|
|
1729
1763
|
const absTarget = resolve(targetPath);
|
|
1730
1764
|
const absLink = resolve(linkPath);
|
|
@@ -1753,6 +1787,17 @@ function isSymlink(path) {
|
|
|
1753
1787
|
return false;
|
|
1754
1788
|
}
|
|
1755
1789
|
}
|
|
1790
|
+
function isSymlinkInto(linkPath, dir) {
|
|
1791
|
+
try {
|
|
1792
|
+
if (!lstatSync(linkPath).isSymbolicLink()) return false;
|
|
1793
|
+
const target = resolve(dirname(linkPath), readlinkSync(linkPath));
|
|
1794
|
+
const base = resolve(dir);
|
|
1795
|
+
const rel = relative(base, target);
|
|
1796
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
1797
|
+
} catch {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1756
1801
|
|
|
1757
1802
|
// src/lib/installers/common.ts
|
|
1758
1803
|
function safeSlugName(slug) {
|
|
@@ -1828,7 +1873,7 @@ var cursorAdapter = {
|
|
|
1828
1873
|
};
|
|
1829
1874
|
|
|
1830
1875
|
// src/lib/installers/claude.ts
|
|
1831
|
-
import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
|
|
1876
|
+
import { existsSync as existsSync5, rmSync as rmSync2, statSync, readdirSync } from "fs";
|
|
1832
1877
|
import { join as join4 } from "path";
|
|
1833
1878
|
import { homedir as homedir3 } from "os";
|
|
1834
1879
|
var descriptor2 = {
|
|
@@ -1847,14 +1892,20 @@ function detect2(projectDir) {
|
|
|
1847
1892
|
project: existsSync5(join4(cwd, ".claude"))
|
|
1848
1893
|
};
|
|
1849
1894
|
}
|
|
1895
|
+
function claudeBase(scope, projectDir) {
|
|
1896
|
+
return scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
|
|
1897
|
+
}
|
|
1850
1898
|
function resolvePath2(slug, scope, projectDir, contentType) {
|
|
1851
1899
|
const safeName = safeSlugName(slug);
|
|
1852
|
-
const base = scope
|
|
1900
|
+
const base = claudeBase(scope, projectDir);
|
|
1853
1901
|
if (contentType === "rule") {
|
|
1854
1902
|
return join4(base, "rules", `${safeName}.md`);
|
|
1855
1903
|
}
|
|
1856
1904
|
return join4(base, "skills", safeName, "SKILL.md");
|
|
1857
1905
|
}
|
|
1906
|
+
function resolvePackageDir(slug, scope, projectDir, _contentType) {
|
|
1907
|
+
return join4(claudeBase(scope, projectDir), "skills", safeSlugName(slug));
|
|
1908
|
+
}
|
|
1858
1909
|
function transformContent2(content, skill) {
|
|
1859
1910
|
if (skill.type === "rule") {
|
|
1860
1911
|
return toPlainMD(content);
|
|
@@ -1866,11 +1917,19 @@ function install2(opts) {
|
|
|
1866
1917
|
return installFileOrSymlink(opts, targetPath);
|
|
1867
1918
|
}
|
|
1868
1919
|
function uninstall2(installation, _slug) {
|
|
1869
|
-
|
|
1870
|
-
|
|
1920
|
+
const target = installation.path;
|
|
1921
|
+
if (isSymlink(target)) {
|
|
1922
|
+
removeSymlink(target);
|
|
1923
|
+
} else if (existsSync5(target)) {
|
|
1924
|
+
if (statSync(target).isDirectory()) {
|
|
1925
|
+
rmSync2(target, { recursive: true, force: true });
|
|
1926
|
+
} else {
|
|
1927
|
+
rmSync2(target, { force: true });
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const parentDir = join4(target, "..");
|
|
1871
1931
|
try {
|
|
1872
|
-
|
|
1873
|
-
if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
|
|
1932
|
+
if (existsSync5(parentDir) && readdirSync(parentDir).length === 0) {
|
|
1874
1933
|
rmSync2(parentDir, { recursive: true });
|
|
1875
1934
|
}
|
|
1876
1935
|
} catch {
|
|
@@ -1883,6 +1942,7 @@ var claudeAdapter = {
|
|
|
1883
1942
|
descriptor: descriptor2,
|
|
1884
1943
|
detect: detect2,
|
|
1885
1944
|
resolvePath: resolvePath2,
|
|
1945
|
+
resolvePackageDir,
|
|
1886
1946
|
transformContent: transformContent2,
|
|
1887
1947
|
install: install2,
|
|
1888
1948
|
uninstall: uninstall2,
|
|
@@ -2334,8 +2394,32 @@ function getAllAdapters() {
|
|
|
2334
2394
|
return [...adapters.values()];
|
|
2335
2395
|
}
|
|
2336
2396
|
|
|
2397
|
+
// src/lib/extract.ts
|
|
2398
|
+
import { unzipSync } from "fflate";
|
|
2399
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
2400
|
+
import { join as join12, dirname as dirname3, resolve as resolve2 } from "path";
|
|
2401
|
+
function extractPackage(zipBuffer, targetDir) {
|
|
2402
|
+
const resolvedTarget = resolve2(targetDir);
|
|
2403
|
+
const extracted = unzipSync(new Uint8Array(zipBuffer));
|
|
2404
|
+
const writtenFiles = [];
|
|
2405
|
+
for (const [path, data] of Object.entries(extracted)) {
|
|
2406
|
+
if (path.endsWith("/")) continue;
|
|
2407
|
+
if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
const fullPath = resolve2(join12(targetDir, path));
|
|
2411
|
+
if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
mkdirSync5(dirname3(fullPath), { recursive: true });
|
|
2415
|
+
writeFileSync5(fullPath, Buffer.from(data));
|
|
2416
|
+
writtenFiles.push(path);
|
|
2417
|
+
}
|
|
2418
|
+
return writtenFiles;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2337
2421
|
// src/lib/cache.ts
|
|
2338
|
-
var CACHE_DIR =
|
|
2422
|
+
var CACHE_DIR = join13(homedir8(), ".localskills", "cache");
|
|
2339
2423
|
function slugToDir(slug) {
|
|
2340
2424
|
if (slug.includes("..") || slug.includes("\0")) {
|
|
2341
2425
|
throw new Error("Invalid slug: contains forbidden characters");
|
|
@@ -2343,16 +2427,16 @@ function slugToDir(slug) {
|
|
|
2343
2427
|
return slug.replace(/\//g, "--");
|
|
2344
2428
|
}
|
|
2345
2429
|
function getCacheDir(slug) {
|
|
2346
|
-
const dir =
|
|
2347
|
-
if (!dir.startsWith(
|
|
2430
|
+
const dir = resolve3(join13(CACHE_DIR, slugToDir(slug)));
|
|
2431
|
+
if (!dir.startsWith(resolve3(CACHE_DIR) + "/") && dir !== resolve3(CACHE_DIR)) {
|
|
2348
2432
|
throw new Error("Invalid slug: path traversal detected");
|
|
2349
2433
|
}
|
|
2350
2434
|
return dir;
|
|
2351
2435
|
}
|
|
2352
2436
|
function store(slug, content, skill, version2) {
|
|
2353
2437
|
const dir = getCacheDir(slug);
|
|
2354
|
-
|
|
2355
|
-
|
|
2438
|
+
mkdirSync6(dir, { recursive: true });
|
|
2439
|
+
writeFileSync6(join13(dir, "raw.md"), content);
|
|
2356
2440
|
const meta = {
|
|
2357
2441
|
hash: skill.contentHash,
|
|
2358
2442
|
version: version2,
|
|
@@ -2362,7 +2446,7 @@ function store(slug, content, skill, version2) {
|
|
|
2362
2446
|
type: skill.type ?? "skill",
|
|
2363
2447
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2364
2448
|
};
|
|
2365
|
-
|
|
2449
|
+
writeFileSync6(join13(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2366
2450
|
clearPlatformFiles(slug);
|
|
2367
2451
|
}
|
|
2368
2452
|
function getPlatformFile(slug, platform, skill) {
|
|
@@ -2373,25 +2457,25 @@ function getPlatformFile(slug, platform, skill) {
|
|
|
2373
2457
|
const transformed = adapter.transformContent(raw, skill);
|
|
2374
2458
|
if (platform === "claude") {
|
|
2375
2459
|
if (skill.type === "rule") {
|
|
2376
|
-
const claudeRuleDir =
|
|
2377
|
-
|
|
2378
|
-
const filePath3 =
|
|
2379
|
-
|
|
2460
|
+
const claudeRuleDir = join13(dir, "claude-rule");
|
|
2461
|
+
mkdirSync6(claudeRuleDir, { recursive: true });
|
|
2462
|
+
const filePath3 = join13(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
|
|
2463
|
+
writeFileSync6(filePath3, transformed);
|
|
2380
2464
|
return filePath3;
|
|
2381
2465
|
}
|
|
2382
|
-
const claudeDir =
|
|
2383
|
-
|
|
2384
|
-
const filePath2 =
|
|
2385
|
-
|
|
2466
|
+
const claudeDir = join13(dir, "claude");
|
|
2467
|
+
mkdirSync6(claudeDir, { recursive: true });
|
|
2468
|
+
const filePath2 = join13(claudeDir, "SKILL.md");
|
|
2469
|
+
writeFileSync6(filePath2, transformed);
|
|
2386
2470
|
return filePath2;
|
|
2387
2471
|
}
|
|
2388
2472
|
const ext = adapter.descriptor.fileExtension;
|
|
2389
|
-
const filePath =
|
|
2390
|
-
|
|
2473
|
+
const filePath = join13(dir, `${platform}${ext}`);
|
|
2474
|
+
writeFileSync6(filePath, transformed);
|
|
2391
2475
|
return filePath;
|
|
2392
2476
|
}
|
|
2393
2477
|
function getRawContent(slug) {
|
|
2394
|
-
const filePath =
|
|
2478
|
+
const filePath = join13(getCacheDir(slug), "raw.md");
|
|
2395
2479
|
if (!existsSync13(filePath)) return null;
|
|
2396
2480
|
return readFileSync4(filePath, "utf-8");
|
|
2397
2481
|
}
|
|
@@ -2401,11 +2485,18 @@ function purge(slug) {
|
|
|
2401
2485
|
rmSync3(dir, { recursive: true, force: true });
|
|
2402
2486
|
}
|
|
2403
2487
|
}
|
|
2488
|
+
function getPackageDir(slug) {
|
|
2489
|
+
return join13(getCacheDir(slug), "pkg");
|
|
2490
|
+
}
|
|
2404
2491
|
function storePackage(slug, zipBuffer, manifest, skill, version2) {
|
|
2405
2492
|
const dir = getCacheDir(slug);
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2493
|
+
mkdirSync6(dir, { recursive: true });
|
|
2494
|
+
const pkgDir = join13(dir, "pkg");
|
|
2495
|
+
rmSync3(pkgDir, { recursive: true, force: true });
|
|
2496
|
+
mkdirSync6(pkgDir, { recursive: true });
|
|
2497
|
+
extractPackage(zipBuffer, pkgDir);
|
|
2498
|
+
rmSync3(join13(dir, "package.zip"), { force: true });
|
|
2499
|
+
writeFileSync6(join13(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
2409
2500
|
const meta = {
|
|
2410
2501
|
hash: skill.contentHash,
|
|
2411
2502
|
version: version2,
|
|
@@ -2416,41 +2507,44 @@ function storePackage(slug, zipBuffer, manifest, skill, version2) {
|
|
|
2416
2507
|
format: "package",
|
|
2417
2508
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2418
2509
|
};
|
|
2419
|
-
|
|
2510
|
+
writeFileSync6(join13(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2420
2511
|
}
|
|
2421
2512
|
function clearPlatformFiles(slug) {
|
|
2422
2513
|
const dir = getCacheDir(slug);
|
|
2423
2514
|
if (!existsSync13(dir)) return;
|
|
2424
|
-
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "
|
|
2425
|
-
for (const entry of
|
|
2515
|
+
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "manifest.json", "pkg"]);
|
|
2516
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2426
2517
|
if (!keep.has(entry.name)) {
|
|
2427
|
-
rmSync3(
|
|
2518
|
+
rmSync3(join13(dir, entry.name), { recursive: true, force: true });
|
|
2428
2519
|
}
|
|
2429
2520
|
}
|
|
2430
2521
|
}
|
|
2431
2522
|
|
|
2432
|
-
// src/lib/
|
|
2433
|
-
import {
|
|
2434
|
-
import {
|
|
2435
|
-
import {
|
|
2436
|
-
function
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
if (path.endsWith("/")) continue;
|
|
2442
|
-
if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
|
|
2443
|
-
continue;
|
|
2444
|
-
}
|
|
2445
|
-
const fullPath = resolve3(join13(targetDir, path));
|
|
2446
|
-
if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
|
|
2447
|
-
continue;
|
|
2448
|
-
}
|
|
2449
|
-
mkdirSync6(dirname3(fullPath), { recursive: true });
|
|
2450
|
-
writeFileSync6(fullPath, Buffer.from(data));
|
|
2451
|
-
writtenFiles.push(path);
|
|
2523
|
+
// src/lib/package-target.ts
|
|
2524
|
+
import { lstatSync as lstatSync2, readdirSync as readdirSync3 } from "fs";
|
|
2525
|
+
import { basename, dirname as dirname4, join as join14 } from "path";
|
|
2526
|
+
import { homedir as homedir9 } from "os";
|
|
2527
|
+
function packageTargetConflict(targetDir, ownedByConfig) {
|
|
2528
|
+
try {
|
|
2529
|
+
lstatSync2(targetDir);
|
|
2530
|
+
} catch {
|
|
2531
|
+
return null;
|
|
2452
2532
|
}
|
|
2453
|
-
return
|
|
2533
|
+
if (ownedByConfig) return null;
|
|
2534
|
+
if (isSymlinkInto(targetDir, join14(homedir9(), ".localskills", "cache"))) {
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2537
|
+
return `${targetDir} already exists and isn't managed by localskills \u2014 move or remove it first`;
|
|
2538
|
+
}
|
|
2539
|
+
function soleTrackedOccupant(targetDir, installedPath) {
|
|
2540
|
+
if (dirname4(installedPath) !== targetDir) return false;
|
|
2541
|
+
let entries;
|
|
2542
|
+
try {
|
|
2543
|
+
entries = readdirSync3(targetDir);
|
|
2544
|
+
} catch {
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2547
|
+
return entries.length === 1 && entries[0] === basename(installedPath);
|
|
2454
2548
|
}
|
|
2455
2549
|
|
|
2456
2550
|
// src/lib/interactive.ts
|
|
@@ -2570,6 +2664,32 @@ function buildSkillRecord(cacheKey, skill, version2, resolvedSemver, requestedRa
|
|
|
2570
2664
|
installations: existingInstallations ? [...existingInstallations, ...newInstallations] : newInstallations
|
|
2571
2665
|
};
|
|
2572
2666
|
}
|
|
2667
|
+
function reconcilePlatformInstalls(existing, slug, platformId, scope, projectDir, newPath, newFormat) {
|
|
2668
|
+
const remaining = [];
|
|
2669
|
+
const stale = [];
|
|
2670
|
+
let ownedTarget = false;
|
|
2671
|
+
for (const inst of existing ?? []) {
|
|
2672
|
+
const sameTarget = inst.platform === platformId && inst.scope === scope && (inst.projectDir ?? null) === (projectDir ?? null);
|
|
2673
|
+
if (!sameTarget) {
|
|
2674
|
+
remaining.push(inst);
|
|
2675
|
+
continue;
|
|
2676
|
+
}
|
|
2677
|
+
if (inst.path === newPath && (inst.format ?? "text") === newFormat) {
|
|
2678
|
+
ownedTarget = true;
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
stale.push(inst);
|
|
2682
|
+
}
|
|
2683
|
+
return { remaining, stale, ownedTarget };
|
|
2684
|
+
}
|
|
2685
|
+
function removeStaleInstalls(stale, slug) {
|
|
2686
|
+
for (const inst of stale) {
|
|
2687
|
+
try {
|
|
2688
|
+
getAdapter(inst.platform).uninstall(inst, slug);
|
|
2689
|
+
} catch {
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2573
2693
|
var installCommand = new Command2("install").description("Install a skill locally").argument("[slug]", "Skill slug (omit for interactive search)").option("-t, --target <targets...>", "Target platforms (cursor, claude, codex, ...)").option("-g, --global", "Install globally").option("-p, --project [dir]", "Install in project directory").option("--symlink", "Use symlink (default)").option("--copy", "Copy instead of symlink").action(
|
|
2574
2694
|
async (slugArg, opts) => {
|
|
2575
2695
|
const client = new ApiClient();
|
|
@@ -2652,11 +2772,20 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2652
2772
|
spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version3)} (package, ${manifest.files.length} files)`);
|
|
2653
2773
|
const dlSpinner = bt2();
|
|
2654
2774
|
dlSpinner.start("Downloading package...");
|
|
2655
|
-
|
|
2775
|
+
let zipBuffer;
|
|
2776
|
+
try {
|
|
2777
|
+
zipBuffer = await client.fetchBinary(downloadUrl);
|
|
2778
|
+
} catch (err) {
|
|
2779
|
+
dlSpinner.stop(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2780
|
+
process.exit(1);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2656
2783
|
dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
|
|
2657
2784
|
storePackage(cacheKey, zipBuffer, manifest, skill2, version3);
|
|
2785
|
+
const pkgDir = getPackageDir(cacheKey);
|
|
2658
2786
|
const installations2 = [];
|
|
2659
2787
|
const results2 = [];
|
|
2788
|
+
let existingInstallations2 = config.installed_skills[cacheKey]?.installations;
|
|
2660
2789
|
for (const platformId of platforms) {
|
|
2661
2790
|
const adapter = getAdapter(platformId);
|
|
2662
2791
|
const desc = adapter.descriptor;
|
|
@@ -2668,19 +2797,55 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2668
2797
|
R2.warn(`${desc.name} does not support project \u2014 skipping.`);
|
|
2669
2798
|
continue;
|
|
2670
2799
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2800
|
+
if (!adapter.resolvePackageDir) {
|
|
2801
|
+
R2.warn(`${desc.name} does not support multi-file (folder) skills \u2014 skipping.`);
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
const targetDir = adapter.resolvePackageDir(cacheKey, scope, projectDir, skill2.type ?? "skill");
|
|
2805
|
+
const reconciled = reconcilePlatformInstalls(
|
|
2806
|
+
existingInstallations2,
|
|
2807
|
+
cacheKey,
|
|
2808
|
+
platformId,
|
|
2809
|
+
scope,
|
|
2810
|
+
projectDir,
|
|
2811
|
+
targetDir,
|
|
2812
|
+
"package"
|
|
2813
|
+
);
|
|
2814
|
+
const owned = reconciled.ownedTarget || reconciled.stale.some((inst) => soleTrackedOccupant(targetDir, inst.path));
|
|
2815
|
+
const conflict = packageTargetConflict(targetDir, owned);
|
|
2816
|
+
if (conflict) {
|
|
2817
|
+
R2.warn(`${desc.name} \u2014 ${conflict}. Skipping.`);
|
|
2818
|
+
existingInstallations2 = [...reconciled.remaining, ...reconciled.stale];
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
removeStaleInstalls(reconciled.stale, cacheKey);
|
|
2822
|
+
existingInstallations2 = reconciled.remaining;
|
|
2823
|
+
const actualMethod = method === "copy" ? "copy" : "symlink";
|
|
2824
|
+
if (actualMethod === "symlink") {
|
|
2825
|
+
createSymlink(pkgDir, targetDir);
|
|
2826
|
+
} else {
|
|
2827
|
+
rmSync4(targetDir, { recursive: true, force: true });
|
|
2828
|
+
mkdirSync7(dirname5(targetDir), { recursive: true });
|
|
2829
|
+
cpSync(pkgDir, targetDir, { recursive: true });
|
|
2830
|
+
}
|
|
2674
2831
|
const installation = {
|
|
2675
2832
|
platform: platformId,
|
|
2676
2833
|
scope,
|
|
2677
|
-
method:
|
|
2678
|
-
path:
|
|
2834
|
+
method: actualMethod,
|
|
2835
|
+
path: targetDir,
|
|
2679
2836
|
projectDir,
|
|
2680
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2837
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2838
|
+
format: "package"
|
|
2681
2839
|
};
|
|
2682
2840
|
installations2.push(installation);
|
|
2683
|
-
|
|
2841
|
+
const methodLabel = actualMethod === "symlink" ? "symlinked" : "copied";
|
|
2842
|
+
results2.push(`${desc.name} \u2192 ${targetDir} (${methodLabel}, ${manifest.files.length} files)`);
|
|
2843
|
+
}
|
|
2844
|
+
if (installations2.length === 0) {
|
|
2845
|
+
R2.error("No compatible targets for this folder skill.");
|
|
2846
|
+
R2.info("Folder (package) skills currently install to Claude Code only.");
|
|
2847
|
+
process.exit(1);
|
|
2848
|
+
return;
|
|
2684
2849
|
}
|
|
2685
2850
|
config.installed_skills[cacheKey] = buildSkillRecord(
|
|
2686
2851
|
cacheKey,
|
|
@@ -2688,7 +2853,7 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2688
2853
|
version3,
|
|
2689
2854
|
resolvedSemver2,
|
|
2690
2855
|
requestedRange,
|
|
2691
|
-
|
|
2856
|
+
existingInstallations2,
|
|
2692
2857
|
installations2
|
|
2693
2858
|
);
|
|
2694
2859
|
saveConfig(config);
|
|
@@ -2704,6 +2869,7 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2704
2869
|
const contentType = skill.type ?? "skill";
|
|
2705
2870
|
const installations = [];
|
|
2706
2871
|
const results = [];
|
|
2872
|
+
let existingInstallations = config.installed_skills[cacheKey]?.installations;
|
|
2707
2873
|
for (const platformId of platforms) {
|
|
2708
2874
|
const adapter = getAdapter(platformId);
|
|
2709
2875
|
const desc = adapter.descriptor;
|
|
@@ -2716,6 +2882,18 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2716
2882
|
continue;
|
|
2717
2883
|
}
|
|
2718
2884
|
const actualMethod = adapter.defaultMethod(scope) === "section" ? "section" : method;
|
|
2885
|
+
const newPath = adapter.resolvePath(cacheKey, scope, projectDir, contentType);
|
|
2886
|
+
const reconciledText = reconcilePlatformInstalls(
|
|
2887
|
+
existingInstallations,
|
|
2888
|
+
cacheKey,
|
|
2889
|
+
platformId,
|
|
2890
|
+
scope,
|
|
2891
|
+
projectDir,
|
|
2892
|
+
newPath,
|
|
2893
|
+
"text"
|
|
2894
|
+
);
|
|
2895
|
+
removeStaleInstalls(reconciledText.stale, cacheKey);
|
|
2896
|
+
existingInstallations = reconciledText.remaining;
|
|
2719
2897
|
const cachePath = getPlatformFile(cacheKey, platformId, skill);
|
|
2720
2898
|
const transformed = adapter.transformContent(content, skill);
|
|
2721
2899
|
const installedPath = adapter.install({
|
|
@@ -2733,7 +2911,8 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2733
2911
|
method: actualMethod,
|
|
2734
2912
|
path: installedPath,
|
|
2735
2913
|
projectDir,
|
|
2736
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2914
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2915
|
+
format: "text"
|
|
2737
2916
|
};
|
|
2738
2917
|
installations.push(installation);
|
|
2739
2918
|
const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
|
|
@@ -2745,7 +2924,7 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2745
2924
|
version2,
|
|
2746
2925
|
resolvedSemver,
|
|
2747
2926
|
requestedRange,
|
|
2748
|
-
|
|
2927
|
+
existingInstallations,
|
|
2749
2928
|
installations
|
|
2750
2929
|
);
|
|
2751
2930
|
saveConfig(config);
|
|
@@ -2806,6 +2985,28 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
|
|
|
2806
2985
|
|
|
2807
2986
|
// src/commands/list.ts
|
|
2808
2987
|
import { Command as Command4 } from "commander";
|
|
2988
|
+
function toTagArray(tags) {
|
|
2989
|
+
if (Array.isArray(tags)) return tags.filter((t) => typeof t === "string");
|
|
2990
|
+
if (typeof tags === "string" && tags.trim()) {
|
|
2991
|
+
try {
|
|
2992
|
+
const parsed = JSON.parse(tags);
|
|
2993
|
+
if (Array.isArray(parsed)) return parsed.filter((t) => typeof t === "string");
|
|
2994
|
+
return [tags];
|
|
2995
|
+
} catch {
|
|
2996
|
+
return [tags];
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
return [];
|
|
3000
|
+
}
|
|
3001
|
+
function formatTags(tags) {
|
|
3002
|
+
const list = toTagArray(tags);
|
|
3003
|
+
return list.length > 0 ? ` [${list.join(", ")}]` : "";
|
|
3004
|
+
}
|
|
3005
|
+
function oneLine(text, max = 100) {
|
|
3006
|
+
if (!text) return "";
|
|
3007
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
3008
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
3009
|
+
}
|
|
2809
3010
|
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) => {
|
|
2810
3011
|
const client = new ApiClient();
|
|
2811
3012
|
if ((opts.tag || opts.search) && !opts.public) {
|
|
@@ -2831,8 +3032,8 @@ var listCommand = new Command4("list").description("List available skills").opti
|
|
|
2831
3032
|
console.log("Public skills:\n");
|
|
2832
3033
|
for (const skill of res.data) {
|
|
2833
3034
|
const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
|
|
2834
|
-
const tags = skill.tags
|
|
2835
|
-
console.log(` ${skill.slug} ${ver}${tags} \u2014 ${skill.description || skill.name}`);
|
|
3035
|
+
const tags = formatTags(skill.tags);
|
|
3036
|
+
console.log(` ${skill.slug} ${ver}${tags} \u2014 ${oneLine(skill.description) || skill.name}`);
|
|
2836
3037
|
}
|
|
2837
3038
|
console.log(`
|
|
2838
3039
|
${res.data.length} skill(s) found.`);
|
|
@@ -2852,8 +3053,8 @@ ${res.data.length} skill(s) found.`);
|
|
|
2852
3053
|
for (const skill of res.data) {
|
|
2853
3054
|
const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
|
|
2854
3055
|
const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
|
|
2855
|
-
const tags = skill.tags
|
|
2856
|
-
console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${skill.description || skill.name}`);
|
|
3056
|
+
const tags = formatTags(skill.tags);
|
|
3057
|
+
console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${oneLine(skill.description) || skill.name}`);
|
|
2857
3058
|
}
|
|
2858
3059
|
console.log(`
|
|
2859
3060
|
${res.data.length} skill(s) found.`);
|
|
@@ -2861,8 +3062,21 @@ ${res.data.length} skill(s) found.`);
|
|
|
2861
3062
|
});
|
|
2862
3063
|
|
|
2863
3064
|
// src/commands/pull.ts
|
|
2864
|
-
import { mkdirSync as mkdirSync8 } from "fs";
|
|
3065
|
+
import { mkdirSync as mkdirSync8, rmSync as rmSync5, cpSync as cpSync2, statSync as statSync2 } from "fs";
|
|
3066
|
+
import { dirname as dirname6 } from "path";
|
|
2865
3067
|
import { Command as Command5 } from "commander";
|
|
3068
|
+
function errMsg(err) {
|
|
3069
|
+
return err instanceof Error ? err.message : String(err);
|
|
3070
|
+
}
|
|
3071
|
+
function installedAsPackage(inst) {
|
|
3072
|
+
if (inst.format === "package") return true;
|
|
3073
|
+
if (inst.format === "text") return false;
|
|
3074
|
+
try {
|
|
3075
|
+
return statSync2(inst.path).isDirectory();
|
|
3076
|
+
} catch {
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
2866
3080
|
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) => {
|
|
2867
3081
|
const config = loadConfig();
|
|
2868
3082
|
const slugs = slugArg ? [slugArg] : Object.keys(config.installed_skills);
|
|
@@ -2898,53 +3112,129 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
|
|
|
2898
3112
|
skipped++;
|
|
2899
3113
|
continue;
|
|
2900
3114
|
}
|
|
3115
|
+
let allHandled = true;
|
|
2901
3116
|
if (format === "package") {
|
|
2902
3117
|
const { downloadUrl, manifest } = resData;
|
|
2903
|
-
|
|
3118
|
+
let zipBuffer;
|
|
3119
|
+
try {
|
|
3120
|
+
zipBuffer = await client.fetchBinary(downloadUrl);
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
spinner.stop(`${slug} \u2014 download failed: ${errMsg(err)}`);
|
|
3123
|
+
continue;
|
|
3124
|
+
}
|
|
2904
3125
|
storePackage(slug, zipBuffer, manifest, skill, version2);
|
|
3126
|
+
const pkgDir = getPackageDir(slug);
|
|
3127
|
+
const kept = [];
|
|
2905
3128
|
for (const installation of installed.installations) {
|
|
2906
3129
|
const adapter = getAdapter(installation.platform);
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
|
|
3130
|
+
const wasPackage = installedAsPackage(installation);
|
|
3131
|
+
if (!adapter.resolvePackageDir) {
|
|
3132
|
+
R2.warn(
|
|
3133
|
+
`${adapter.descriptor.name} can't hold a folder skill \u2014 keeping the existing install; it won't receive this update.`
|
|
3134
|
+
);
|
|
3135
|
+
kept.push(installation);
|
|
3136
|
+
allHandled = false;
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
try {
|
|
3140
|
+
const targetDir = adapter.resolvePackageDir(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
|
|
3141
|
+
const owned = wasPackage && installation.path === targetDir || !wasPackage && soleTrackedOccupant(targetDir, installation.path);
|
|
3142
|
+
const conflict = packageTargetConflict(targetDir, owned);
|
|
3143
|
+
if (conflict) {
|
|
3144
|
+
R2.warn(
|
|
3145
|
+
`${adapter.descriptor.name} \u2014 ${conflict}. Skipping; reinstall with \`localskills install ${slug}\`.`
|
|
3146
|
+
);
|
|
3147
|
+
kept.push(installation);
|
|
3148
|
+
allHandled = false;
|
|
3149
|
+
continue;
|
|
3150
|
+
}
|
|
3151
|
+
if (!wasPackage) {
|
|
3152
|
+
adapter.uninstall(installation, slug);
|
|
3153
|
+
}
|
|
3154
|
+
const useMethod = installation.method === "copy" ? "copy" : "symlink";
|
|
3155
|
+
if (useMethod === "symlink") {
|
|
3156
|
+
createSymlink(pkgDir, targetDir);
|
|
3157
|
+
} else {
|
|
3158
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
3159
|
+
mkdirSync8(dirname6(targetDir), { recursive: true });
|
|
3160
|
+
cpSync2(pkgDir, targetDir, { recursive: true });
|
|
3161
|
+
}
|
|
3162
|
+
kept.push({ ...installation, method: useMethod, path: targetDir, format: "package" });
|
|
3163
|
+
} catch (err) {
|
|
3164
|
+
R2.warn(`${adapter.descriptor.name} \u2014 failed to update: ${errMsg(err)}`);
|
|
3165
|
+
kept.push(installation);
|
|
3166
|
+
allHandled = false;
|
|
3167
|
+
}
|
|
2910
3168
|
}
|
|
3169
|
+
installed.installations = kept;
|
|
2911
3170
|
} else {
|
|
2912
3171
|
const { content } = resData;
|
|
2913
3172
|
store(slug, content, skill, version2);
|
|
2914
3173
|
for (const installation of installed.installations) {
|
|
2915
|
-
if (installation.method === "symlink") {
|
|
2916
|
-
getPlatformFile(slug, installation.platform, skill);
|
|
2917
|
-
continue;
|
|
2918
|
-
}
|
|
2919
3174
|
const adapter = getAdapter(installation.platform);
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
installation
|
|
2924
|
-
|
|
2925
|
-
|
|
3175
|
+
try {
|
|
3176
|
+
const transformed = adapter.transformContent(content, skill);
|
|
3177
|
+
if (installedAsPackage(installation)) {
|
|
3178
|
+
adapter.uninstall(installation, slug);
|
|
3179
|
+
const method = installation.method === "section" ? "copy" : installation.method;
|
|
3180
|
+
const cachePath = getPlatformFile(slug, installation.platform, skill);
|
|
3181
|
+
const installedPath = adapter.install({
|
|
3182
|
+
slug,
|
|
3183
|
+
content: transformed,
|
|
3184
|
+
scope: installation.scope,
|
|
3185
|
+
method,
|
|
3186
|
+
cachePath,
|
|
3187
|
+
projectDir: installation.projectDir,
|
|
3188
|
+
contentType: skill.type
|
|
3189
|
+
});
|
|
3190
|
+
installation.path = installedPath;
|
|
3191
|
+
installation.method = method;
|
|
3192
|
+
installation.format = "text";
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
if (installation.method === "symlink") {
|
|
3196
|
+
getPlatformFile(slug, installation.platform, skill);
|
|
3197
|
+
installation.format = "text";
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
if (installation.method === "section") {
|
|
3201
|
+
upsertSection(
|
|
3202
|
+
installation.path,
|
|
3203
|
+
slug,
|
|
3204
|
+
`## ${slug}
|
|
2926
3205
|
|
|
2927
3206
|
${transformed}`
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3207
|
+
);
|
|
3208
|
+
} else {
|
|
3209
|
+
const cachePath = getPlatformFile(slug, installation.platform, skill);
|
|
3210
|
+
adapter.install({
|
|
3211
|
+
slug,
|
|
3212
|
+
content: transformed,
|
|
3213
|
+
scope: installation.scope,
|
|
3214
|
+
method: "copy",
|
|
3215
|
+
cachePath,
|
|
3216
|
+
projectDir: installation.projectDir,
|
|
3217
|
+
contentType: skill.type
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
installation.format = "text";
|
|
3221
|
+
} catch (err) {
|
|
3222
|
+
R2.warn(`${adapter.descriptor.name} \u2014 failed to update: ${errMsg(err)}`);
|
|
3223
|
+
allHandled = false;
|
|
2939
3224
|
}
|
|
2940
3225
|
}
|
|
2941
3226
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3227
|
+
if (allHandled) {
|
|
3228
|
+
installed.hash = skill.contentHash;
|
|
3229
|
+
installed.version = version2;
|
|
3230
|
+
installed.semver = resData.semver ?? null;
|
|
3231
|
+
installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3232
|
+
updated++;
|
|
3233
|
+
spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version2)}`);
|
|
3234
|
+
} else {
|
|
3235
|
+
updated++;
|
|
3236
|
+
spinner.stop(`${slug} \u2014 partially updated to ${formatVersionLabel(res.data.semver, version2)} (see warnings above)`);
|
|
3237
|
+
}
|
|
2948
3238
|
}
|
|
2949
3239
|
saveConfig(config);
|
|
2950
3240
|
Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
|
|
@@ -2952,44 +3242,43 @@ ${transformed}`
|
|
|
2952
3242
|
|
|
2953
3243
|
// src/commands/publish.ts
|
|
2954
3244
|
import { Command as Command6 } from "commander";
|
|
2955
|
-
import { readFileSync as
|
|
2956
|
-
import { resolve as resolve4, basename as
|
|
2957
|
-
import { homedir as
|
|
3245
|
+
import { readFileSync as readFileSync7, existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
3246
|
+
import { resolve as resolve4, basename as basename3, extname as extname2 } from "path";
|
|
3247
|
+
import { homedir as homedir11 } from "os";
|
|
2958
3248
|
|
|
2959
3249
|
// src/lib/scanner.ts
|
|
2960
|
-
import { existsSync as existsSync14, readdirSync as
|
|
2961
|
-
import { join as
|
|
2962
|
-
import { homedir as
|
|
2963
|
-
import { readlinkSync, lstatSync as lstatSync2 } from "fs";
|
|
3250
|
+
import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "fs";
|
|
3251
|
+
import { join as join15, basename as basename2, extname, dirname as dirname7 } from "path";
|
|
3252
|
+
import { homedir as homedir10 } from "os";
|
|
2964
3253
|
function scanForSkills(projectDir) {
|
|
2965
|
-
const home =
|
|
3254
|
+
const home = homedir10();
|
|
2966
3255
|
const cwd = projectDir || process.cwd();
|
|
2967
3256
|
const results = [];
|
|
2968
|
-
scanDirectory(
|
|
2969
|
-
scanDirectory(
|
|
2970
|
-
scanClaudeSkills(
|
|
2971
|
-
scanClaudeSkills(
|
|
2972
|
-
scanDirectory(
|
|
2973
|
-
scanDirectory(
|
|
2974
|
-
scanSingleFile(
|
|
2975
|
-
scanSingleFile(
|
|
3257
|
+
scanDirectory(join15(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
|
|
3258
|
+
scanDirectory(join15(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
|
|
3259
|
+
scanClaudeSkills(join15(home, ".claude", "skills"), "global", results);
|
|
3260
|
+
scanClaudeSkills(join15(cwd, ".claude", "skills"), "project", results);
|
|
3261
|
+
scanDirectory(join15(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
|
|
3262
|
+
scanDirectory(join15(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
|
|
3263
|
+
scanSingleFile(join15(home, ".codex", "AGENTS.md"), "codex", "global", results);
|
|
3264
|
+
scanSingleFile(join15(cwd, "AGENTS.md"), "codex", "project", results);
|
|
2976
3265
|
scanSingleFile(
|
|
2977
|
-
|
|
3266
|
+
join15(home, ".codeium", "windsurf", "memories", "global_rules.md"),
|
|
2978
3267
|
"windsurf",
|
|
2979
3268
|
"global",
|
|
2980
3269
|
results
|
|
2981
3270
|
);
|
|
2982
|
-
scanDirectory(
|
|
2983
|
-
scanDirectory(
|
|
3271
|
+
scanDirectory(join15(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
|
|
3272
|
+
scanDirectory(join15(cwd, ".clinerules"), ".md", "cline", "project", results);
|
|
2984
3273
|
scanSingleFile(
|
|
2985
|
-
|
|
3274
|
+
join15(cwd, ".github", "copilot-instructions.md"),
|
|
2986
3275
|
"copilot",
|
|
2987
3276
|
"project",
|
|
2988
3277
|
results
|
|
2989
3278
|
);
|
|
2990
|
-
scanDirectory(
|
|
2991
|
-
scanDirectory(
|
|
2992
|
-
scanDirectory(
|
|
3279
|
+
scanDirectory(join15(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
|
|
3280
|
+
scanDirectory(join15(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
|
|
3281
|
+
scanDirectory(join15(cwd, ".aider", "skills"), ".md", "aider", "project", results);
|
|
2993
3282
|
return results;
|
|
2994
3283
|
}
|
|
2995
3284
|
function filterTracked(detected, config) {
|
|
@@ -2999,35 +3288,32 @@ function filterTracked(detected, config) {
|
|
|
2999
3288
|
trackedPaths.add(inst.path);
|
|
3000
3289
|
}
|
|
3001
3290
|
}
|
|
3002
|
-
const cacheDir =
|
|
3291
|
+
const cacheDir = join15(homedir10(), ".localskills", "cache");
|
|
3003
3292
|
return detected.filter((skill) => {
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
if (
|
|
3008
|
-
|
|
3009
|
-
if (target.startsWith(cacheDir)) return false;
|
|
3010
|
-
}
|
|
3011
|
-
} catch {
|
|
3293
|
+
const candidates = [skill.filePath, dirname7(skill.filePath)];
|
|
3294
|
+
if (skill.dir) candidates.push(skill.dir);
|
|
3295
|
+
for (const path of candidates) {
|
|
3296
|
+
if (trackedPaths.has(path)) return false;
|
|
3297
|
+
if (isSymlinkInto(path, cacheDir)) return false;
|
|
3012
3298
|
}
|
|
3013
3299
|
return true;
|
|
3014
3300
|
});
|
|
3015
3301
|
}
|
|
3016
3302
|
function slugFromFilename(filename) {
|
|
3017
|
-
return
|
|
3303
|
+
return basename2(filename, extname(filename));
|
|
3018
3304
|
}
|
|
3019
3305
|
var nameFromSlug = titleFromSlug;
|
|
3020
3306
|
function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
|
|
3021
3307
|
if (!existsSync14(dir)) return;
|
|
3022
3308
|
let entries;
|
|
3023
3309
|
try {
|
|
3024
|
-
entries =
|
|
3310
|
+
entries = readdirSync4(dir);
|
|
3025
3311
|
} catch {
|
|
3026
3312
|
return;
|
|
3027
3313
|
}
|
|
3028
3314
|
for (const entry of entries) {
|
|
3029
3315
|
if (!entry.endsWith(ext)) continue;
|
|
3030
|
-
const filePath =
|
|
3316
|
+
const filePath = join15(dir, entry);
|
|
3031
3317
|
try {
|
|
3032
3318
|
const raw = readFileSync5(filePath, "utf-8");
|
|
3033
3319
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -3035,6 +3321,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
3035
3321
|
const slug = slugFromFilename(entry);
|
|
3036
3322
|
results.push({
|
|
3037
3323
|
filePath,
|
|
3324
|
+
format: "text",
|
|
3038
3325
|
platform,
|
|
3039
3326
|
scope,
|
|
3040
3327
|
suggestedName: nameFromSlug(slug),
|
|
@@ -3046,28 +3333,56 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
3046
3333
|
}
|
|
3047
3334
|
}
|
|
3048
3335
|
}
|
|
3336
|
+
var SCAN_JUNK = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".git", "node_modules", ".localskills"]);
|
|
3337
|
+
function dirHasFilesBesides(dir, excludeRootFile) {
|
|
3338
|
+
const stack = [dir];
|
|
3339
|
+
while (stack.length > 0) {
|
|
3340
|
+
const current = stack.pop();
|
|
3341
|
+
let entries;
|
|
3342
|
+
try {
|
|
3343
|
+
entries = readdirSync4(current, { withFileTypes: true });
|
|
3344
|
+
} catch {
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
3347
|
+
for (const entry of entries) {
|
|
3348
|
+
if (SCAN_JUNK.has(entry.name)) continue;
|
|
3349
|
+
if (entry.isDirectory()) {
|
|
3350
|
+
stack.push(join15(current, entry.name));
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
if (current === dir && entry.name === excludeRootFile) continue;
|
|
3354
|
+
return true;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
return false;
|
|
3358
|
+
}
|
|
3049
3359
|
function scanClaudeSkills(skillsDir, scope, results) {
|
|
3050
3360
|
if (!existsSync14(skillsDir)) return;
|
|
3051
3361
|
let entries;
|
|
3052
3362
|
try {
|
|
3053
|
-
entries =
|
|
3363
|
+
entries = readdirSync4(skillsDir);
|
|
3054
3364
|
} catch {
|
|
3055
3365
|
return;
|
|
3056
3366
|
}
|
|
3057
3367
|
for (const entry of entries) {
|
|
3058
|
-
const
|
|
3368
|
+
const skillDir = join15(skillsDir, entry);
|
|
3369
|
+
const skillFile = join15(skillDir, "SKILL.md");
|
|
3059
3370
|
if (!existsSync14(skillFile)) continue;
|
|
3060
3371
|
try {
|
|
3061
3372
|
const raw = readFileSync5(skillFile, "utf-8");
|
|
3062
3373
|
const content = stripFrontmatter(raw).trim();
|
|
3063
|
-
|
|
3374
|
+
const isPackage = dirHasFilesBesides(skillDir, "SKILL.md");
|
|
3375
|
+
if (!isPackage && !content) continue;
|
|
3064
3376
|
results.push({
|
|
3065
3377
|
filePath: skillFile,
|
|
3378
|
+
...isPackage ? { dir: skillDir } : {},
|
|
3379
|
+
format: isPackage ? "package" : "text",
|
|
3066
3380
|
platform: "claude",
|
|
3067
3381
|
scope,
|
|
3068
3382
|
suggestedName: nameFromSlug(entry),
|
|
3069
3383
|
suggestedSlug: entry,
|
|
3070
3384
|
content,
|
|
3385
|
+
// for packages: SKILL.md body, used only for the hint/preview
|
|
3071
3386
|
contentType: "skill"
|
|
3072
3387
|
});
|
|
3073
3388
|
} catch {
|
|
@@ -3094,6 +3409,7 @@ function scanSingleFile(filePath, platform, scope, results) {
|
|
|
3094
3409
|
if (!content2) continue;
|
|
3095
3410
|
results.push({
|
|
3096
3411
|
filePath,
|
|
3412
|
+
format: "text",
|
|
3097
3413
|
platform,
|
|
3098
3414
|
scope,
|
|
3099
3415
|
suggestedName: nameFromSlug(slug2),
|
|
@@ -3109,6 +3425,7 @@ function scanSingleFile(filePath, platform, scope, results) {
|
|
|
3109
3425
|
const slug = slugFromFilename(filePath);
|
|
3110
3426
|
results.push({
|
|
3111
3427
|
filePath,
|
|
3428
|
+
format: "text",
|
|
3112
3429
|
platform,
|
|
3113
3430
|
scope,
|
|
3114
3431
|
suggestedName: nameFromSlug(slug),
|
|
@@ -3118,8 +3435,177 @@ function scanSingleFile(filePath, platform, scope, results) {
|
|
|
3118
3435
|
});
|
|
3119
3436
|
}
|
|
3120
3437
|
|
|
3438
|
+
// src/lib/pack.ts
|
|
3439
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
3440
|
+
import { join as join16, relative as relative2, sep as sep2 } from "path";
|
|
3441
|
+
import { zipSync } from "fflate";
|
|
3442
|
+
var PackError = class extends Error {
|
|
3443
|
+
constructor(message) {
|
|
3444
|
+
super(message);
|
|
3445
|
+
this.name = "PackError";
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
function zipBlob(zip) {
|
|
3449
|
+
return new Blob([new Uint8Array(zip)], { type: "application/zip" });
|
|
3450
|
+
}
|
|
3451
|
+
var SKIP = /* @__PURE__ */ new Set([
|
|
3452
|
+
".DS_Store",
|
|
3453
|
+
".git",
|
|
3454
|
+
"node_modules",
|
|
3455
|
+
".localskills",
|
|
3456
|
+
"Thumbs.db"
|
|
3457
|
+
]);
|
|
3458
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3459
|
+
".png",
|
|
3460
|
+
".jpg",
|
|
3461
|
+
".jpeg",
|
|
3462
|
+
".gif",
|
|
3463
|
+
".webp",
|
|
3464
|
+
".ico",
|
|
3465
|
+
".bmp",
|
|
3466
|
+
".tiff",
|
|
3467
|
+
".woff",
|
|
3468
|
+
".woff2",
|
|
3469
|
+
".ttf",
|
|
3470
|
+
".eot",
|
|
3471
|
+
".otf",
|
|
3472
|
+
".pdf",
|
|
3473
|
+
".zip",
|
|
3474
|
+
".tar",
|
|
3475
|
+
".gz",
|
|
3476
|
+
".bz2",
|
|
3477
|
+
".7z",
|
|
3478
|
+
".rar",
|
|
3479
|
+
".exe",
|
|
3480
|
+
".dll",
|
|
3481
|
+
".so",
|
|
3482
|
+
".dylib",
|
|
3483
|
+
".bin",
|
|
3484
|
+
".wasm",
|
|
3485
|
+
".pyc",
|
|
3486
|
+
".class",
|
|
3487
|
+
".o",
|
|
3488
|
+
".obj",
|
|
3489
|
+
".mp3",
|
|
3490
|
+
".mp4",
|
|
3491
|
+
".wav",
|
|
3492
|
+
".avi",
|
|
3493
|
+
".mov",
|
|
3494
|
+
".mkv",
|
|
3495
|
+
".sqlite",
|
|
3496
|
+
".db"
|
|
3497
|
+
]);
|
|
3498
|
+
function getExtension(path) {
|
|
3499
|
+
const dot = path.lastIndexOf(".");
|
|
3500
|
+
return dot === -1 ? "" : path.substring(dot).toLowerCase();
|
|
3501
|
+
}
|
|
3502
|
+
function isBinaryByContent(data) {
|
|
3503
|
+
const sample = data.subarray(0, 8192);
|
|
3504
|
+
for (let i = 0; i < sample.length; i++) if (sample[i] === 0) return true;
|
|
3505
|
+
return false;
|
|
3506
|
+
}
|
|
3507
|
+
function validatePath(path) {
|
|
3508
|
+
if (path.startsWith("/") || path.startsWith("\\")) return false;
|
|
3509
|
+
if (path.includes("..")) return false;
|
|
3510
|
+
if (path.includes("\0")) return false;
|
|
3511
|
+
if (path.includes("\\")) return false;
|
|
3512
|
+
if (path === "__proto__") return false;
|
|
3513
|
+
return true;
|
|
3514
|
+
}
|
|
3515
|
+
function walk(root, current, acc) {
|
|
3516
|
+
for (const entry of readdirSync5(current, { withFileTypes: true })) {
|
|
3517
|
+
if (SKIP.has(entry.name)) continue;
|
|
3518
|
+
const abs = join16(current, entry.name);
|
|
3519
|
+
const rel = relative2(root, abs).split(sep2).join("/");
|
|
3520
|
+
let isFile = entry.isFile();
|
|
3521
|
+
let size;
|
|
3522
|
+
if (entry.isDirectory()) {
|
|
3523
|
+
walk(root, abs, acc);
|
|
3524
|
+
continue;
|
|
3525
|
+
}
|
|
3526
|
+
if (entry.isSymbolicLink()) {
|
|
3527
|
+
let st3;
|
|
3528
|
+
try {
|
|
3529
|
+
st3 = statSync3(abs);
|
|
3530
|
+
} catch {
|
|
3531
|
+
acc.warnings.push(`Skipped broken symlink: ${rel}`);
|
|
3532
|
+
continue;
|
|
3533
|
+
}
|
|
3534
|
+
if (st3.isDirectory()) {
|
|
3535
|
+
acc.warnings.push(`Skipped symlinked directory: ${rel}/`);
|
|
3536
|
+
continue;
|
|
3537
|
+
}
|
|
3538
|
+
isFile = st3.isFile();
|
|
3539
|
+
size = st3.size;
|
|
3540
|
+
}
|
|
3541
|
+
if (!isFile) continue;
|
|
3542
|
+
acc.count++;
|
|
3543
|
+
if (acc.count > PACKAGE_MAX_FILE_COUNT) {
|
|
3544
|
+
throw new PackError(
|
|
3545
|
+
`Folder contains more than ${PACKAGE_MAX_FILE_COUNT} files`
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
size ??= statSync3(abs).size;
|
|
3549
|
+
if (acc.bytes + size > PACKAGE_MAX_DECOMPRESSED_BYTES) {
|
|
3550
|
+
throw new PackError(
|
|
3551
|
+
`Folder exceeds ${PACKAGE_MAX_DECOMPRESSED_BYTES / 1024 / 1024} MB uncompressed limit`
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
const data = new Uint8Array(readFileSync6(abs));
|
|
3555
|
+
acc.bytes += data.length;
|
|
3556
|
+
if (acc.bytes > PACKAGE_MAX_DECOMPRESSED_BYTES) {
|
|
3557
|
+
throw new PackError(
|
|
3558
|
+
`Folder exceeds ${PACKAGE_MAX_DECOMPRESSED_BYTES / 1024 / 1024} MB uncompressed limit`
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
acc.files.set(rel, data);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
function packFolder(dir, opts = {}) {
|
|
3565
|
+
const requireSkillMd = opts.requireSkillMd ?? true;
|
|
3566
|
+
const stat = statSync3(dir);
|
|
3567
|
+
if (!stat.isDirectory()) throw new PackError(`Not a directory: ${dir}`);
|
|
3568
|
+
const acc = { files: /* @__PURE__ */ new Map(), count: 0, bytes: 0, warnings: [] };
|
|
3569
|
+
walk(dir, dir, acc);
|
|
3570
|
+
const entries = acc.files;
|
|
3571
|
+
if (entries.size === 0) throw new PackError("Folder contains no files");
|
|
3572
|
+
if (requireSkillMd && !entries.has("SKILL.md")) {
|
|
3573
|
+
throw new PackError(
|
|
3574
|
+
"No SKILL.md at the folder root. A skill folder must contain a SKILL.md file."
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
let totalSize = 0;
|
|
3578
|
+
const files = [];
|
|
3579
|
+
for (const [path, data] of entries) {
|
|
3580
|
+
if (!validatePath(path)) throw new PackError(`Invalid file path: ${path}`);
|
|
3581
|
+
totalSize += data.length;
|
|
3582
|
+
const binary = BINARY_EXTENSIONS.has(getExtension(path)) || isBinaryByContent(data);
|
|
3583
|
+
const file = { path, size: data.length, isBinary: binary };
|
|
3584
|
+
if (!binary && data.length > 0) {
|
|
3585
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(data);
|
|
3586
|
+
file.preview = text.length > PACKAGE_PREVIEW_BYTES ? text.substring(0, PACKAGE_PREVIEW_BYTES) : text;
|
|
3587
|
+
}
|
|
3588
|
+
files.push(file);
|
|
3589
|
+
}
|
|
3590
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
3591
|
+
const zipInput = /* @__PURE__ */ Object.create(null);
|
|
3592
|
+
for (const [path, data] of entries) zipInput[path] = data;
|
|
3593
|
+
const zip = Buffer.from(zipSync(zipInput));
|
|
3594
|
+
if (zip.length > PACKAGE_MAX_COMPRESSED_BYTES) {
|
|
3595
|
+
throw new PackError(
|
|
3596
|
+
`Archive exceeds ${PACKAGE_MAX_COMPRESSED_BYTES / 1024 / 1024} MB compressed limit`
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
return {
|
|
3600
|
+
zip,
|
|
3601
|
+
manifest: { version: 1, totalSize, archiveSize: zip.length, files },
|
|
3602
|
+
fileCount: files.length,
|
|
3603
|
+
warnings: acc.warnings
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3121
3607
|
// src/commands/publish.ts
|
|
3122
|
-
var publishCommand = new Command6("publish").description("Publish local
|
|
3608
|
+
var publishCommand = new Command6("publish").description("Publish local skills to localskills.sh").argument("[path]", "Path to a skill file, or a skill folder (uploaded as a package)").option("-t, --team <id>", "Team ID to publish to").option("-n, --name <name>", "Skill name").option(
|
|
3123
3609
|
"--visibility <visibility>",
|
|
3124
3610
|
"Visibility: public, private, or unlisted",
|
|
3125
3611
|
"private"
|
|
@@ -3139,30 +3625,46 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
3139
3625
|
if (fileArg) {
|
|
3140
3626
|
const filePath = resolve4(fileArg);
|
|
3141
3627
|
if (!existsSync15(filePath)) {
|
|
3142
|
-
console.error(`
|
|
3628
|
+
console.error(`Path not found: ${filePath}`);
|
|
3143
3629
|
process.exit(1);
|
|
3144
3630
|
return;
|
|
3145
3631
|
}
|
|
3146
|
-
|
|
3632
|
+
if (statSync4(filePath).isDirectory()) {
|
|
3633
|
+
const contentType2 = validateContentType(opts.type || "skill");
|
|
3634
|
+
const visibility2 = validateVisibility(opts.visibility || "private");
|
|
3635
|
+
const tenantId2 = await resolveTeam(teams, opts.team);
|
|
3636
|
+
const skillName2 = opts.name || titleFromSlug(basename3(filePath));
|
|
3637
|
+
const ok2 = await uploadPackage(client, {
|
|
3638
|
+
name: skillName2,
|
|
3639
|
+
dir: filePath,
|
|
3640
|
+
tenantId: tenantId2,
|
|
3641
|
+
visibility: visibility2,
|
|
3642
|
+
type: contentType2
|
|
3643
|
+
});
|
|
3644
|
+
if (!ok2) process.exit(1);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3147
3648
|
const content = stripFrontmatter(raw).trim();
|
|
3148
3649
|
if (!content) {
|
|
3149
3650
|
console.error("File is empty after stripping frontmatter.");
|
|
3150
3651
|
process.exit(1);
|
|
3151
3652
|
return;
|
|
3152
3653
|
}
|
|
3153
|
-
const defaultSlug =
|
|
3654
|
+
const defaultSlug = basename3(filePath, extname2(filePath));
|
|
3154
3655
|
const defaultName = titleFromSlug(defaultSlug);
|
|
3155
3656
|
const skillName = opts.name || defaultName;
|
|
3156
3657
|
const contentType = validateContentType(opts.type || "skill");
|
|
3157
3658
|
const visibility = validateVisibility(opts.visibility || "private");
|
|
3158
3659
|
const tenantId = await resolveTeam(teams, opts.team);
|
|
3159
|
-
await uploadSkill(client, {
|
|
3660
|
+
const ok = await uploadSkill(client, {
|
|
3160
3661
|
name: skillName,
|
|
3161
3662
|
content,
|
|
3162
3663
|
tenantId,
|
|
3163
3664
|
visibility,
|
|
3164
3665
|
type: contentType
|
|
3165
3666
|
});
|
|
3667
|
+
if (!ok) process.exit(1);
|
|
3166
3668
|
} else {
|
|
3167
3669
|
We("localskills publish");
|
|
3168
3670
|
const spinner = bt2();
|
|
@@ -3187,6 +3689,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
3187
3689
|
required: true
|
|
3188
3690
|
}));
|
|
3189
3691
|
const tenantId = await resolveTeam(teams, opts.team);
|
|
3692
|
+
let failures = 0;
|
|
3190
3693
|
for (const skill of skills) {
|
|
3191
3694
|
R2.step(`Publishing ${skill.suggestedName}...`);
|
|
3192
3695
|
const name = cancelGuard(await Ze({
|
|
@@ -3214,15 +3717,23 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
3214
3717
|
],
|
|
3215
3718
|
initialValue: skill.contentType
|
|
3216
3719
|
}));
|
|
3217
|
-
await
|
|
3720
|
+
const ok = skill.format === "package" && skill.dir ? await uploadPackage(client, {
|
|
3721
|
+
name,
|
|
3722
|
+
dir: skill.dir,
|
|
3723
|
+
tenantId,
|
|
3724
|
+
visibility,
|
|
3725
|
+
type: contentType
|
|
3726
|
+
}) : await uploadSkill(client, {
|
|
3218
3727
|
name,
|
|
3219
3728
|
content: skill.content,
|
|
3220
3729
|
tenantId,
|
|
3221
3730
|
visibility,
|
|
3222
3731
|
type: contentType
|
|
3223
3732
|
});
|
|
3733
|
+
if (!ok) failures++;
|
|
3224
3734
|
}
|
|
3225
|
-
Le("Done!");
|
|
3735
|
+
Le(failures > 0 ? `Done, with ${failures} failure(s).` : "Done!");
|
|
3736
|
+
if (failures > 0) process.exit(1);
|
|
3226
3737
|
}
|
|
3227
3738
|
}
|
|
3228
3739
|
);
|
|
@@ -3259,10 +3770,37 @@ async function uploadSkill(client, params) {
|
|
|
3259
3770
|
});
|
|
3260
3771
|
if (!res.success || !res.data) {
|
|
3261
3772
|
spinner.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3262
|
-
return;
|
|
3773
|
+
return false;
|
|
3263
3774
|
}
|
|
3264
3775
|
spinner.stop(`Published!`);
|
|
3265
|
-
R2.success(`\u2192 localskills.sh/s/${res.data.
|
|
3776
|
+
R2.success(`\u2192 localskills.sh/s/${res.data.publicId}`);
|
|
3777
|
+
return true;
|
|
3778
|
+
}
|
|
3779
|
+
async function uploadPackage(client, params) {
|
|
3780
|
+
const spinner = bt2();
|
|
3781
|
+
let packed;
|
|
3782
|
+
try {
|
|
3783
|
+
packed = packFolder(params.dir, { requireSkillMd: params.type === "skill" });
|
|
3784
|
+
} catch (e2) {
|
|
3785
|
+
R2.error(e2 instanceof PackError ? e2.message : String(e2));
|
|
3786
|
+
return false;
|
|
3787
|
+
}
|
|
3788
|
+
for (const w of packed.warnings) R2.warn(w);
|
|
3789
|
+
spinner.start(`Uploading ${params.name} (${packed.fileCount} files)...`);
|
|
3790
|
+
const form = new FormData();
|
|
3791
|
+
form.append("name", params.name);
|
|
3792
|
+
form.append("tenantId", params.tenantId);
|
|
3793
|
+
form.append("visibility", params.visibility);
|
|
3794
|
+
form.append("type", params.type);
|
|
3795
|
+
form.append("file", zipBlob(packed.zip), "skill.zip");
|
|
3796
|
+
const res = await client.postForm("/api/skills", form);
|
|
3797
|
+
if (!res.success || !res.data) {
|
|
3798
|
+
spinner.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3799
|
+
return false;
|
|
3800
|
+
}
|
|
3801
|
+
spinner.stop(`Published! (${packed.fileCount} files)`);
|
|
3802
|
+
R2.success(`\u2192 localskills.sh/s/${res.data.publicId}`);
|
|
3803
|
+
return true;
|
|
3266
3804
|
}
|
|
3267
3805
|
function validateContentType(value) {
|
|
3268
3806
|
if (value === "skill" || value === "rule") {
|
|
@@ -3279,7 +3817,7 @@ function validateVisibility(value) {
|
|
|
3279
3817
|
process.exit(1);
|
|
3280
3818
|
}
|
|
3281
3819
|
function shortenPath(filePath) {
|
|
3282
|
-
const home =
|
|
3820
|
+
const home = homedir11();
|
|
3283
3821
|
if (filePath.startsWith(home)) {
|
|
3284
3822
|
return "~" + filePath.slice(home.length);
|
|
3285
3823
|
}
|
|
@@ -3292,41 +3830,78 @@ function shortenPath(filePath) {
|
|
|
3292
3830
|
|
|
3293
3831
|
// src/commands/push.ts
|
|
3294
3832
|
import { Command as Command7 } from "commander";
|
|
3295
|
-
import { readFileSync as
|
|
3833
|
+
import { readFileSync as readFileSync8, existsSync as existsSync16, statSync as statSync5 } from "fs";
|
|
3296
3834
|
import { resolve as resolve5 } from "path";
|
|
3297
|
-
var pushCommand = new Command7("push").description("Push a new version of an existing skill").argument("<
|
|
3835
|
+
var pushCommand = new Command7("push").description("Push a new version of an existing skill").argument("<path>", "Path to the skill file, or a skill folder (uploaded as a package)").requiredOption("-s, --skill <id>", "Skill ID or slug").option("--version <semver>", "Explicit semver (e.g., 1.1.0)").option("--patch", "Bump patch version").option("--minor", "Bump minor version").option("--major", "Bump major version").option("-m, --message <message>", "Version message").action(
|
|
3298
3836
|
async (fileArg, opts) => {
|
|
3299
3837
|
const client = new ApiClient();
|
|
3300
3838
|
requireAuth(client);
|
|
3301
3839
|
const filePath = resolve5(fileArg);
|
|
3302
3840
|
if (!existsSync16(filePath)) {
|
|
3303
|
-
console.error(`
|
|
3841
|
+
console.error(`Path not found: ${filePath}`);
|
|
3304
3842
|
process.exit(1);
|
|
3305
3843
|
return;
|
|
3306
3844
|
}
|
|
3307
|
-
const
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
console.error("File is empty after stripping frontmatter.");
|
|
3845
|
+
const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
|
|
3846
|
+
if (bumpFlags.length > 1) {
|
|
3847
|
+
console.error("Specify only one of --patch, --minor, or --major");
|
|
3311
3848
|
process.exit(1);
|
|
3312
3849
|
return;
|
|
3313
3850
|
}
|
|
3314
|
-
const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
|
|
3315
3851
|
if (opts.version && bumpFlags.length > 0) {
|
|
3316
3852
|
console.error("Cannot specify both --version and --patch/--minor/--major");
|
|
3317
3853
|
process.exit(1);
|
|
3318
3854
|
return;
|
|
3319
3855
|
}
|
|
3856
|
+
if (opts.version && !isValidSemVer(opts.version)) {
|
|
3857
|
+
console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
|
|
3858
|
+
process.exit(1);
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
if (statSync5(filePath).isDirectory()) {
|
|
3862
|
+
let packed;
|
|
3863
|
+
try {
|
|
3864
|
+
packed = packFolder(filePath, { requireSkillMd: false });
|
|
3865
|
+
} catch (e2) {
|
|
3866
|
+
console.error(e2 instanceof PackError ? e2.message : String(e2));
|
|
3867
|
+
process.exit(1);
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
for (const w of packed.warnings) R2.warn(w);
|
|
3871
|
+
const form = new FormData();
|
|
3872
|
+
form.append("file", zipBlob(packed.zip), "skill.zip");
|
|
3873
|
+
if (opts.message) form.append("message", opts.message);
|
|
3874
|
+
if (opts.version) form.append("semver", opts.version);
|
|
3875
|
+
else if (opts.major) form.append("bump", "major");
|
|
3876
|
+
else if (opts.minor) form.append("bump", "minor");
|
|
3877
|
+
else if (opts.patch) form.append("bump", "patch");
|
|
3878
|
+
const spinner2 = bt2();
|
|
3879
|
+
spinner2.start(`Pushing new version (${packed.fileCount} files)...`);
|
|
3880
|
+
const res2 = await client.postForm(
|
|
3881
|
+
`/api/skills/${encodeURIComponent(opts.skill)}/versions`,
|
|
3882
|
+
form
|
|
3883
|
+
);
|
|
3884
|
+
if (!res2.success || !res2.data) {
|
|
3885
|
+
spinner2.stop(`Failed: ${res2.error || "Unknown error"}`);
|
|
3886
|
+
process.exit(1);
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
spinner2.stop(`Pushed ${formatVersionLabel(res2.data.semver, res2.data.version)}`);
|
|
3890
|
+
Le("Done!");
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
3894
|
+
const content = stripFrontmatter(raw).trim();
|
|
3895
|
+
if (!content) {
|
|
3896
|
+
console.error("File is empty after stripping frontmatter.");
|
|
3897
|
+
process.exit(1);
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3320
3900
|
let body = {
|
|
3321
3901
|
content,
|
|
3322
3902
|
message: opts.message
|
|
3323
3903
|
};
|
|
3324
3904
|
if (opts.version) {
|
|
3325
|
-
if (!isValidSemVer(opts.version)) {
|
|
3326
|
-
console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
|
|
3327
|
-
process.exit(1);
|
|
3328
|
-
return;
|
|
3329
|
-
}
|
|
3330
3905
|
body.semver = opts.version;
|
|
3331
3906
|
} else if (opts.major) {
|
|
3332
3907
|
body.bump = "major";
|
|
@@ -3354,30 +3929,49 @@ var pushCommand = new Command7("push").description("Push a new version of an exi
|
|
|
3354
3929
|
|
|
3355
3930
|
// src/commands/share.ts
|
|
3356
3931
|
import { Command as Command8 } from "commander";
|
|
3357
|
-
import { readFileSync as
|
|
3358
|
-
import { resolve as resolve6, basename as
|
|
3932
|
+
import { readFileSync as readFileSync9, existsSync as existsSync17, statSync as statSync6 } from "fs";
|
|
3933
|
+
import { resolve as resolve6, basename as basename4, extname as extname3 } from "path";
|
|
3359
3934
|
import { generateKeyPairSync } from "crypto";
|
|
3360
|
-
var shareCommand = new Command8("share").description("Share a skill anonymously (no login required)").argument("[
|
|
3935
|
+
var shareCommand = new Command8("share").description("Share a skill anonymously (no login required)").argument("[path]", "Path to a skill file, or a skill folder (uploaded as a package)").option("-n, --name <name>", "Skill name").option("--type <type>", "Content type: skill or rule", "skill").action(async (fileArg, opts) => {
|
|
3361
3936
|
We("localskills share");
|
|
3937
|
+
if (opts.type && opts.type !== "skill" && opts.type !== "rule") {
|
|
3938
|
+
R2.error(`Invalid type: ${opts.type}. Use skill or rule.`);
|
|
3939
|
+
process.exit(1);
|
|
3940
|
+
}
|
|
3362
3941
|
await ensureAnonymousIdentity();
|
|
3363
3942
|
const client = new ApiClient();
|
|
3364
3943
|
if (fileArg) {
|
|
3365
3944
|
const filePath = resolve6(fileArg);
|
|
3366
3945
|
if (!existsSync17(filePath)) {
|
|
3367
|
-
R2.error(`
|
|
3946
|
+
R2.error(`Path not found: ${filePath}`);
|
|
3368
3947
|
process.exit(1);
|
|
3369
3948
|
}
|
|
3370
|
-
|
|
3949
|
+
if (statSync6(filePath).isDirectory()) {
|
|
3950
|
+
const contentType2 = opts.type === "rule" ? "rule" : "skill";
|
|
3951
|
+
const skillName2 = opts.name || titleFromSlug(basename4(filePath));
|
|
3952
|
+
const ok2 = await uploadAnonymousPackage(client, {
|
|
3953
|
+
name: skillName2,
|
|
3954
|
+
dir: filePath,
|
|
3955
|
+
type: contentType2
|
|
3956
|
+
});
|
|
3957
|
+
Le(ok2 ? "Done!" : "Share failed.");
|
|
3958
|
+
if (!ok2) process.exit(1);
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
const raw = readFileSync9(filePath, "utf-8");
|
|
3371
3962
|
const content = stripFrontmatter(raw).trim();
|
|
3372
3963
|
if (!content) {
|
|
3373
3964
|
R2.error("File is empty after stripping frontmatter.");
|
|
3374
3965
|
process.exit(1);
|
|
3375
3966
|
}
|
|
3376
|
-
const defaultSlug =
|
|
3967
|
+
const defaultSlug = basename4(filePath, extname3(filePath));
|
|
3377
3968
|
const defaultName = titleFromSlug(defaultSlug);
|
|
3378
3969
|
const skillName = opts.name || defaultName;
|
|
3379
3970
|
const contentType = opts.type === "rule" ? "rule" : "skill";
|
|
3380
|
-
await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
|
|
3971
|
+
const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
|
|
3972
|
+
Le(ok ? "Done!" : "Share failed.");
|
|
3973
|
+
if (!ok) process.exit(1);
|
|
3974
|
+
return;
|
|
3381
3975
|
} else {
|
|
3382
3976
|
const spinner = bt2();
|
|
3383
3977
|
spinner.start("Scanning for skills...");
|
|
@@ -3411,13 +4005,18 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
|
|
|
3411
4005
|
}
|
|
3412
4006
|
})
|
|
3413
4007
|
);
|
|
3414
|
-
await
|
|
4008
|
+
const ok = selected.format === "package" && selected.dir ? await uploadAnonymousPackage(client, {
|
|
4009
|
+
name,
|
|
4010
|
+
dir: selected.dir,
|
|
4011
|
+
type: selected.contentType
|
|
4012
|
+
}) : await uploadAnonymousSkill(client, {
|
|
3415
4013
|
name,
|
|
3416
4014
|
content: selected.content,
|
|
3417
4015
|
type: selected.contentType
|
|
3418
4016
|
});
|
|
4017
|
+
Le(ok ? "Done!" : "Share failed.");
|
|
4018
|
+
if (!ok) process.exit(1);
|
|
3419
4019
|
}
|
|
3420
|
-
Le("Done!");
|
|
3421
4020
|
});
|
|
3422
4021
|
async function ensureAnonymousIdentity() {
|
|
3423
4022
|
const config = loadConfig();
|
|
@@ -3425,6 +4024,10 @@ async function ensureAnonymousIdentity() {
|
|
|
3425
4024
|
const client = new ApiClient();
|
|
3426
4025
|
const res2 = await client.get("/api/cli/auth");
|
|
3427
4026
|
if (res2.success) return;
|
|
4027
|
+
if (res2.networkError) {
|
|
4028
|
+
R2.error(`Could not verify your login: ${res2.error}. Try again.`);
|
|
4029
|
+
process.exit(1);
|
|
4030
|
+
}
|
|
3428
4031
|
}
|
|
3429
4032
|
let keyPair = getAnonymousKey();
|
|
3430
4033
|
if (!keyPair) {
|
|
@@ -3475,11 +4078,46 @@ async function uploadAnonymousSkill(client, params) {
|
|
|
3475
4078
|
});
|
|
3476
4079
|
if (!res.success || !res.data) {
|
|
3477
4080
|
s.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3478
|
-
return;
|
|
4081
|
+
return false;
|
|
3479
4082
|
}
|
|
3480
4083
|
s.stop("Shared!");
|
|
3481
4084
|
R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
|
|
3482
4085
|
R2.info(`Install: localskills install ${res.data.publicId}`);
|
|
4086
|
+
return true;
|
|
4087
|
+
}
|
|
4088
|
+
async function uploadAnonymousPackage(client, params) {
|
|
4089
|
+
const s = bt2();
|
|
4090
|
+
let packed;
|
|
4091
|
+
try {
|
|
4092
|
+
packed = packFolder(params.dir, { requireSkillMd: params.type === "skill" });
|
|
4093
|
+
} catch (e2) {
|
|
4094
|
+
R2.error(e2 instanceof PackError ? e2.message : String(e2));
|
|
4095
|
+
return false;
|
|
4096
|
+
}
|
|
4097
|
+
for (const w of packed.warnings) R2.warn(w);
|
|
4098
|
+
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
|
+
const form = new FormData();
|
|
4107
|
+
form.append("name", params.name);
|
|
4108
|
+
form.append("tenantId", tenantId);
|
|
4109
|
+
form.append("visibility", "unlisted");
|
|
4110
|
+
form.append("type", params.type);
|
|
4111
|
+
form.append("file", zipBlob(packed.zip), "skill.zip");
|
|
4112
|
+
const res = await client.postForm("/api/skills", form);
|
|
4113
|
+
if (!res.success || !res.data) {
|
|
4114
|
+
s.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
4115
|
+
return false;
|
|
4116
|
+
}
|
|
4117
|
+
s.stop("Shared!");
|
|
4118
|
+
R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
|
|
4119
|
+
R2.info(`Install: localskills install ${res.data.publicId}`);
|
|
4120
|
+
return true;
|
|
3483
4121
|
}
|
|
3484
4122
|
|
|
3485
4123
|
// src/commands/profile.ts
|