@localskills/cli 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -6
- package/dist/index.js +799 -188
- 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 = {};
|
|
@@ -1587,7 +1610,8 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
|
|
|
1587
1610
|
|
|
1588
1611
|
// src/commands/install.ts
|
|
1589
1612
|
import { Command as Command2 } from "commander";
|
|
1590
|
-
import { mkdirSync as mkdirSync7 } from "fs";
|
|
1613
|
+
import { mkdirSync as mkdirSync7, rmSync as rmSync4, cpSync } from "fs";
|
|
1614
|
+
import { dirname as dirname5 } from "path";
|
|
1591
1615
|
|
|
1592
1616
|
// ../../packages/shared/dist/utils/semver.js
|
|
1593
1617
|
var SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
@@ -1624,6 +1648,10 @@ function isGreaterThan(next, prev) {
|
|
|
1624
1648
|
}
|
|
1625
1649
|
|
|
1626
1650
|
// ../../packages/shared/dist/utils/index.js
|
|
1651
|
+
var PACKAGE_MAX_COMPRESSED_BYTES = 1 * 1024 * 1024;
|
|
1652
|
+
var PACKAGE_MAX_DECOMPRESSED_BYTES = 1 * 1024 * 1024;
|
|
1653
|
+
var PACKAGE_MAX_FILE_COUNT = 500;
|
|
1654
|
+
var PACKAGE_PREVIEW_BYTES = 2048;
|
|
1627
1655
|
function titleFromSlug(slug) {
|
|
1628
1656
|
return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1629
1657
|
}
|
|
@@ -1660,13 +1688,13 @@ function cancelGuard(value) {
|
|
|
1660
1688
|
// src/lib/cache.ts
|
|
1661
1689
|
import {
|
|
1662
1690
|
existsSync as existsSync13,
|
|
1663
|
-
mkdirSync as
|
|
1691
|
+
mkdirSync as mkdirSync6,
|
|
1664
1692
|
readFileSync as readFileSync4,
|
|
1665
|
-
readdirSync,
|
|
1666
|
-
writeFileSync as
|
|
1693
|
+
readdirSync as readdirSync2,
|
|
1694
|
+
writeFileSync as writeFileSync6,
|
|
1667
1695
|
rmSync as rmSync3
|
|
1668
1696
|
} from "fs";
|
|
1669
|
-
import { join as
|
|
1697
|
+
import { join as join13, resolve as resolve3 } from "path";
|
|
1670
1698
|
import { homedir as homedir8 } from "os";
|
|
1671
1699
|
|
|
1672
1700
|
// src/lib/installers/cursor.ts
|
|
@@ -1722,9 +1750,10 @@ import {
|
|
|
1722
1750
|
lstatSync,
|
|
1723
1751
|
existsSync as existsSync2,
|
|
1724
1752
|
mkdirSync as mkdirSync2,
|
|
1725
|
-
rmSync
|
|
1753
|
+
rmSync,
|
|
1754
|
+
readlinkSync
|
|
1726
1755
|
} from "fs";
|
|
1727
|
-
import { dirname, resolve } from "path";
|
|
1756
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
1728
1757
|
function createSymlink(targetPath, linkPath) {
|
|
1729
1758
|
const absTarget = resolve(targetPath);
|
|
1730
1759
|
const absLink = resolve(linkPath);
|
|
@@ -1753,6 +1782,17 @@ function isSymlink(path) {
|
|
|
1753
1782
|
return false;
|
|
1754
1783
|
}
|
|
1755
1784
|
}
|
|
1785
|
+
function isSymlinkInto(linkPath, dir) {
|
|
1786
|
+
try {
|
|
1787
|
+
if (!lstatSync(linkPath).isSymbolicLink()) return false;
|
|
1788
|
+
const target = resolve(dirname(linkPath), readlinkSync(linkPath));
|
|
1789
|
+
const base = resolve(dir);
|
|
1790
|
+
const rel = relative(base, target);
|
|
1791
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
1792
|
+
} catch {
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1756
1796
|
|
|
1757
1797
|
// src/lib/installers/common.ts
|
|
1758
1798
|
function safeSlugName(slug) {
|
|
@@ -1828,7 +1868,7 @@ var cursorAdapter = {
|
|
|
1828
1868
|
};
|
|
1829
1869
|
|
|
1830
1870
|
// src/lib/installers/claude.ts
|
|
1831
|
-
import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
|
|
1871
|
+
import { existsSync as existsSync5, rmSync as rmSync2, statSync, readdirSync } from "fs";
|
|
1832
1872
|
import { join as join4 } from "path";
|
|
1833
1873
|
import { homedir as homedir3 } from "os";
|
|
1834
1874
|
var descriptor2 = {
|
|
@@ -1847,14 +1887,20 @@ function detect2(projectDir) {
|
|
|
1847
1887
|
project: existsSync5(join4(cwd, ".claude"))
|
|
1848
1888
|
};
|
|
1849
1889
|
}
|
|
1890
|
+
function claudeBase(scope, projectDir) {
|
|
1891
|
+
return scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
|
|
1892
|
+
}
|
|
1850
1893
|
function resolvePath2(slug, scope, projectDir, contentType) {
|
|
1851
1894
|
const safeName = safeSlugName(slug);
|
|
1852
|
-
const base = scope
|
|
1895
|
+
const base = claudeBase(scope, projectDir);
|
|
1853
1896
|
if (contentType === "rule") {
|
|
1854
1897
|
return join4(base, "rules", `${safeName}.md`);
|
|
1855
1898
|
}
|
|
1856
1899
|
return join4(base, "skills", safeName, "SKILL.md");
|
|
1857
1900
|
}
|
|
1901
|
+
function resolvePackageDir(slug, scope, projectDir, _contentType) {
|
|
1902
|
+
return join4(claudeBase(scope, projectDir), "skills", safeSlugName(slug));
|
|
1903
|
+
}
|
|
1858
1904
|
function transformContent2(content, skill) {
|
|
1859
1905
|
if (skill.type === "rule") {
|
|
1860
1906
|
return toPlainMD(content);
|
|
@@ -1866,11 +1912,19 @@ function install2(opts) {
|
|
|
1866
1912
|
return installFileOrSymlink(opts, targetPath);
|
|
1867
1913
|
}
|
|
1868
1914
|
function uninstall2(installation, _slug) {
|
|
1869
|
-
|
|
1870
|
-
|
|
1915
|
+
const target = installation.path;
|
|
1916
|
+
if (isSymlink(target)) {
|
|
1917
|
+
removeSymlink(target);
|
|
1918
|
+
} else if (existsSync5(target)) {
|
|
1919
|
+
if (statSync(target).isDirectory()) {
|
|
1920
|
+
rmSync2(target, { recursive: true, force: true });
|
|
1921
|
+
} else {
|
|
1922
|
+
rmSync2(target, { force: true });
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
const parentDir = join4(target, "..");
|
|
1871
1926
|
try {
|
|
1872
|
-
|
|
1873
|
-
if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
|
|
1927
|
+
if (existsSync5(parentDir) && readdirSync(parentDir).length === 0) {
|
|
1874
1928
|
rmSync2(parentDir, { recursive: true });
|
|
1875
1929
|
}
|
|
1876
1930
|
} catch {
|
|
@@ -1883,6 +1937,7 @@ var claudeAdapter = {
|
|
|
1883
1937
|
descriptor: descriptor2,
|
|
1884
1938
|
detect: detect2,
|
|
1885
1939
|
resolvePath: resolvePath2,
|
|
1940
|
+
resolvePackageDir,
|
|
1886
1941
|
transformContent: transformContent2,
|
|
1887
1942
|
install: install2,
|
|
1888
1943
|
uninstall: uninstall2,
|
|
@@ -2334,8 +2389,32 @@ function getAllAdapters() {
|
|
|
2334
2389
|
return [...adapters.values()];
|
|
2335
2390
|
}
|
|
2336
2391
|
|
|
2392
|
+
// src/lib/extract.ts
|
|
2393
|
+
import { unzipSync } from "fflate";
|
|
2394
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
2395
|
+
import { join as join12, dirname as dirname3, resolve as resolve2 } from "path";
|
|
2396
|
+
function extractPackage(zipBuffer, targetDir) {
|
|
2397
|
+
const resolvedTarget = resolve2(targetDir);
|
|
2398
|
+
const extracted = unzipSync(new Uint8Array(zipBuffer));
|
|
2399
|
+
const writtenFiles = [];
|
|
2400
|
+
for (const [path, data] of Object.entries(extracted)) {
|
|
2401
|
+
if (path.endsWith("/")) continue;
|
|
2402
|
+
if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
const fullPath = resolve2(join12(targetDir, path));
|
|
2406
|
+
if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
mkdirSync5(dirname3(fullPath), { recursive: true });
|
|
2410
|
+
writeFileSync5(fullPath, Buffer.from(data));
|
|
2411
|
+
writtenFiles.push(path);
|
|
2412
|
+
}
|
|
2413
|
+
return writtenFiles;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2337
2416
|
// src/lib/cache.ts
|
|
2338
|
-
var CACHE_DIR =
|
|
2417
|
+
var CACHE_DIR = join13(homedir8(), ".localskills", "cache");
|
|
2339
2418
|
function slugToDir(slug) {
|
|
2340
2419
|
if (slug.includes("..") || slug.includes("\0")) {
|
|
2341
2420
|
throw new Error("Invalid slug: contains forbidden characters");
|
|
@@ -2343,16 +2422,16 @@ function slugToDir(slug) {
|
|
|
2343
2422
|
return slug.replace(/\//g, "--");
|
|
2344
2423
|
}
|
|
2345
2424
|
function getCacheDir(slug) {
|
|
2346
|
-
const dir =
|
|
2347
|
-
if (!dir.startsWith(
|
|
2425
|
+
const dir = resolve3(join13(CACHE_DIR, slugToDir(slug)));
|
|
2426
|
+
if (!dir.startsWith(resolve3(CACHE_DIR) + "/") && dir !== resolve3(CACHE_DIR)) {
|
|
2348
2427
|
throw new Error("Invalid slug: path traversal detected");
|
|
2349
2428
|
}
|
|
2350
2429
|
return dir;
|
|
2351
2430
|
}
|
|
2352
2431
|
function store(slug, content, skill, version2) {
|
|
2353
2432
|
const dir = getCacheDir(slug);
|
|
2354
|
-
|
|
2355
|
-
|
|
2433
|
+
mkdirSync6(dir, { recursive: true });
|
|
2434
|
+
writeFileSync6(join13(dir, "raw.md"), content);
|
|
2356
2435
|
const meta = {
|
|
2357
2436
|
hash: skill.contentHash,
|
|
2358
2437
|
version: version2,
|
|
@@ -2362,7 +2441,7 @@ function store(slug, content, skill, version2) {
|
|
|
2362
2441
|
type: skill.type ?? "skill",
|
|
2363
2442
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2364
2443
|
};
|
|
2365
|
-
|
|
2444
|
+
writeFileSync6(join13(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2366
2445
|
clearPlatformFiles(slug);
|
|
2367
2446
|
}
|
|
2368
2447
|
function getPlatformFile(slug, platform, skill) {
|
|
@@ -2373,25 +2452,25 @@ function getPlatformFile(slug, platform, skill) {
|
|
|
2373
2452
|
const transformed = adapter.transformContent(raw, skill);
|
|
2374
2453
|
if (platform === "claude") {
|
|
2375
2454
|
if (skill.type === "rule") {
|
|
2376
|
-
const claudeRuleDir =
|
|
2377
|
-
|
|
2378
|
-
const filePath3 =
|
|
2379
|
-
|
|
2455
|
+
const claudeRuleDir = join13(dir, "claude-rule");
|
|
2456
|
+
mkdirSync6(claudeRuleDir, { recursive: true });
|
|
2457
|
+
const filePath3 = join13(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
|
|
2458
|
+
writeFileSync6(filePath3, transformed);
|
|
2380
2459
|
return filePath3;
|
|
2381
2460
|
}
|
|
2382
|
-
const claudeDir =
|
|
2383
|
-
|
|
2384
|
-
const filePath2 =
|
|
2385
|
-
|
|
2461
|
+
const claudeDir = join13(dir, "claude");
|
|
2462
|
+
mkdirSync6(claudeDir, { recursive: true });
|
|
2463
|
+
const filePath2 = join13(claudeDir, "SKILL.md");
|
|
2464
|
+
writeFileSync6(filePath2, transformed);
|
|
2386
2465
|
return filePath2;
|
|
2387
2466
|
}
|
|
2388
2467
|
const ext = adapter.descriptor.fileExtension;
|
|
2389
|
-
const filePath =
|
|
2390
|
-
|
|
2468
|
+
const filePath = join13(dir, `${platform}${ext}`);
|
|
2469
|
+
writeFileSync6(filePath, transformed);
|
|
2391
2470
|
return filePath;
|
|
2392
2471
|
}
|
|
2393
2472
|
function getRawContent(slug) {
|
|
2394
|
-
const filePath =
|
|
2473
|
+
const filePath = join13(getCacheDir(slug), "raw.md");
|
|
2395
2474
|
if (!existsSync13(filePath)) return null;
|
|
2396
2475
|
return readFileSync4(filePath, "utf-8");
|
|
2397
2476
|
}
|
|
@@ -2401,11 +2480,18 @@ function purge(slug) {
|
|
|
2401
2480
|
rmSync3(dir, { recursive: true, force: true });
|
|
2402
2481
|
}
|
|
2403
2482
|
}
|
|
2483
|
+
function getPackageDir(slug) {
|
|
2484
|
+
return join13(getCacheDir(slug), "pkg");
|
|
2485
|
+
}
|
|
2404
2486
|
function storePackage(slug, zipBuffer, manifest, skill, version2) {
|
|
2405
2487
|
const dir = getCacheDir(slug);
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2488
|
+
mkdirSync6(dir, { recursive: true });
|
|
2489
|
+
const pkgDir = join13(dir, "pkg");
|
|
2490
|
+
rmSync3(pkgDir, { recursive: true, force: true });
|
|
2491
|
+
mkdirSync6(pkgDir, { recursive: true });
|
|
2492
|
+
extractPackage(zipBuffer, pkgDir);
|
|
2493
|
+
rmSync3(join13(dir, "package.zip"), { force: true });
|
|
2494
|
+
writeFileSync6(join13(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
2409
2495
|
const meta = {
|
|
2410
2496
|
hash: skill.contentHash,
|
|
2411
2497
|
version: version2,
|
|
@@ -2416,41 +2502,44 @@ function storePackage(slug, zipBuffer, manifest, skill, version2) {
|
|
|
2416
2502
|
format: "package",
|
|
2417
2503
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2418
2504
|
};
|
|
2419
|
-
|
|
2505
|
+
writeFileSync6(join13(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2420
2506
|
}
|
|
2421
2507
|
function clearPlatformFiles(slug) {
|
|
2422
2508
|
const dir = getCacheDir(slug);
|
|
2423
2509
|
if (!existsSync13(dir)) return;
|
|
2424
|
-
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "
|
|
2425
|
-
for (const entry of
|
|
2510
|
+
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "manifest.json", "pkg"]);
|
|
2511
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2426
2512
|
if (!keep.has(entry.name)) {
|
|
2427
|
-
rmSync3(
|
|
2513
|
+
rmSync3(join13(dir, entry.name), { recursive: true, force: true });
|
|
2428
2514
|
}
|
|
2429
2515
|
}
|
|
2430
2516
|
}
|
|
2431
2517
|
|
|
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);
|
|
2518
|
+
// src/lib/package-target.ts
|
|
2519
|
+
import { lstatSync as lstatSync2, readdirSync as readdirSync3 } from "fs";
|
|
2520
|
+
import { basename, dirname as dirname4, join as join14 } from "path";
|
|
2521
|
+
import { homedir as homedir9 } from "os";
|
|
2522
|
+
function packageTargetConflict(targetDir, ownedByConfig) {
|
|
2523
|
+
try {
|
|
2524
|
+
lstatSync2(targetDir);
|
|
2525
|
+
} catch {
|
|
2526
|
+
return null;
|
|
2452
2527
|
}
|
|
2453
|
-
return
|
|
2528
|
+
if (ownedByConfig) return null;
|
|
2529
|
+
if (isSymlinkInto(targetDir, join14(homedir9(), ".localskills", "cache"))) {
|
|
2530
|
+
return null;
|
|
2531
|
+
}
|
|
2532
|
+
return `${targetDir} already exists and isn't managed by localskills \u2014 move or remove it first`;
|
|
2533
|
+
}
|
|
2534
|
+
function soleTrackedOccupant(targetDir, installedPath) {
|
|
2535
|
+
if (dirname4(installedPath) !== targetDir) return false;
|
|
2536
|
+
let entries;
|
|
2537
|
+
try {
|
|
2538
|
+
entries = readdirSync3(targetDir);
|
|
2539
|
+
} catch {
|
|
2540
|
+
return false;
|
|
2541
|
+
}
|
|
2542
|
+
return entries.length === 1 && entries[0] === basename(installedPath);
|
|
2454
2543
|
}
|
|
2455
2544
|
|
|
2456
2545
|
// src/lib/interactive.ts
|
|
@@ -2570,6 +2659,32 @@ function buildSkillRecord(cacheKey, skill, version2, resolvedSemver, requestedRa
|
|
|
2570
2659
|
installations: existingInstallations ? [...existingInstallations, ...newInstallations] : newInstallations
|
|
2571
2660
|
};
|
|
2572
2661
|
}
|
|
2662
|
+
function reconcilePlatformInstalls(existing, slug, platformId, scope, projectDir, newPath, newFormat) {
|
|
2663
|
+
const remaining = [];
|
|
2664
|
+
const stale = [];
|
|
2665
|
+
let ownedTarget = false;
|
|
2666
|
+
for (const inst of existing ?? []) {
|
|
2667
|
+
const sameTarget = inst.platform === platformId && inst.scope === scope && (inst.projectDir ?? null) === (projectDir ?? null);
|
|
2668
|
+
if (!sameTarget) {
|
|
2669
|
+
remaining.push(inst);
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
if (inst.path === newPath && (inst.format ?? "text") === newFormat) {
|
|
2673
|
+
ownedTarget = true;
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
stale.push(inst);
|
|
2677
|
+
}
|
|
2678
|
+
return { remaining, stale, ownedTarget };
|
|
2679
|
+
}
|
|
2680
|
+
function removeStaleInstalls(stale, slug) {
|
|
2681
|
+
for (const inst of stale) {
|
|
2682
|
+
try {
|
|
2683
|
+
getAdapter(inst.platform).uninstall(inst, slug);
|
|
2684
|
+
} catch {
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2573
2688
|
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
2689
|
async (slugArg, opts) => {
|
|
2575
2690
|
const client = new ApiClient();
|
|
@@ -2652,11 +2767,20 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2652
2767
|
spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version3)} (package, ${manifest.files.length} files)`);
|
|
2653
2768
|
const dlSpinner = bt2();
|
|
2654
2769
|
dlSpinner.start("Downloading package...");
|
|
2655
|
-
|
|
2770
|
+
let zipBuffer;
|
|
2771
|
+
try {
|
|
2772
|
+
zipBuffer = await client.fetchBinary(downloadUrl);
|
|
2773
|
+
} catch (err) {
|
|
2774
|
+
dlSpinner.stop(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2775
|
+
process.exit(1);
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2656
2778
|
dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
|
|
2657
2779
|
storePackage(cacheKey, zipBuffer, manifest, skill2, version3);
|
|
2780
|
+
const pkgDir = getPackageDir(cacheKey);
|
|
2658
2781
|
const installations2 = [];
|
|
2659
2782
|
const results2 = [];
|
|
2783
|
+
let existingInstallations2 = config.installed_skills[cacheKey]?.installations;
|
|
2660
2784
|
for (const platformId of platforms) {
|
|
2661
2785
|
const adapter = getAdapter(platformId);
|
|
2662
2786
|
const desc = adapter.descriptor;
|
|
@@ -2668,19 +2792,55 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2668
2792
|
R2.warn(`${desc.name} does not support project \u2014 skipping.`);
|
|
2669
2793
|
continue;
|
|
2670
2794
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2795
|
+
if (!adapter.resolvePackageDir) {
|
|
2796
|
+
R2.warn(`${desc.name} does not support multi-file (folder) skills \u2014 skipping.`);
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
const targetDir = adapter.resolvePackageDir(cacheKey, scope, projectDir, skill2.type ?? "skill");
|
|
2800
|
+
const reconciled = reconcilePlatformInstalls(
|
|
2801
|
+
existingInstallations2,
|
|
2802
|
+
cacheKey,
|
|
2803
|
+
platformId,
|
|
2804
|
+
scope,
|
|
2805
|
+
projectDir,
|
|
2806
|
+
targetDir,
|
|
2807
|
+
"package"
|
|
2808
|
+
);
|
|
2809
|
+
const owned = reconciled.ownedTarget || reconciled.stale.some((inst) => soleTrackedOccupant(targetDir, inst.path));
|
|
2810
|
+
const conflict = packageTargetConflict(targetDir, owned);
|
|
2811
|
+
if (conflict) {
|
|
2812
|
+
R2.warn(`${desc.name} \u2014 ${conflict}. Skipping.`);
|
|
2813
|
+
existingInstallations2 = [...reconciled.remaining, ...reconciled.stale];
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2816
|
+
removeStaleInstalls(reconciled.stale, cacheKey);
|
|
2817
|
+
existingInstallations2 = reconciled.remaining;
|
|
2818
|
+
const actualMethod = method === "copy" ? "copy" : "symlink";
|
|
2819
|
+
if (actualMethod === "symlink") {
|
|
2820
|
+
createSymlink(pkgDir, targetDir);
|
|
2821
|
+
} else {
|
|
2822
|
+
rmSync4(targetDir, { recursive: true, force: true });
|
|
2823
|
+
mkdirSync7(dirname5(targetDir), { recursive: true });
|
|
2824
|
+
cpSync(pkgDir, targetDir, { recursive: true });
|
|
2825
|
+
}
|
|
2674
2826
|
const installation = {
|
|
2675
2827
|
platform: platformId,
|
|
2676
2828
|
scope,
|
|
2677
|
-
method:
|
|
2678
|
-
path:
|
|
2829
|
+
method: actualMethod,
|
|
2830
|
+
path: targetDir,
|
|
2679
2831
|
projectDir,
|
|
2680
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2832
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2833
|
+
format: "package"
|
|
2681
2834
|
};
|
|
2682
2835
|
installations2.push(installation);
|
|
2683
|
-
|
|
2836
|
+
const methodLabel = actualMethod === "symlink" ? "symlinked" : "copied";
|
|
2837
|
+
results2.push(`${desc.name} \u2192 ${targetDir} (${methodLabel}, ${manifest.files.length} files)`);
|
|
2838
|
+
}
|
|
2839
|
+
if (installations2.length === 0) {
|
|
2840
|
+
R2.error("No compatible targets for this folder skill.");
|
|
2841
|
+
R2.info("Folder (package) skills currently install to Claude Code only.");
|
|
2842
|
+
process.exit(1);
|
|
2843
|
+
return;
|
|
2684
2844
|
}
|
|
2685
2845
|
config.installed_skills[cacheKey] = buildSkillRecord(
|
|
2686
2846
|
cacheKey,
|
|
@@ -2688,7 +2848,7 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2688
2848
|
version3,
|
|
2689
2849
|
resolvedSemver2,
|
|
2690
2850
|
requestedRange,
|
|
2691
|
-
|
|
2851
|
+
existingInstallations2,
|
|
2692
2852
|
installations2
|
|
2693
2853
|
);
|
|
2694
2854
|
saveConfig(config);
|
|
@@ -2704,6 +2864,7 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2704
2864
|
const contentType = skill.type ?? "skill";
|
|
2705
2865
|
const installations = [];
|
|
2706
2866
|
const results = [];
|
|
2867
|
+
let existingInstallations = config.installed_skills[cacheKey]?.installations;
|
|
2707
2868
|
for (const platformId of platforms) {
|
|
2708
2869
|
const adapter = getAdapter(platformId);
|
|
2709
2870
|
const desc = adapter.descriptor;
|
|
@@ -2716,6 +2877,18 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2716
2877
|
continue;
|
|
2717
2878
|
}
|
|
2718
2879
|
const actualMethod = adapter.defaultMethod(scope) === "section" ? "section" : method;
|
|
2880
|
+
const newPath = adapter.resolvePath(cacheKey, scope, projectDir, contentType);
|
|
2881
|
+
const reconciledText = reconcilePlatformInstalls(
|
|
2882
|
+
existingInstallations,
|
|
2883
|
+
cacheKey,
|
|
2884
|
+
platformId,
|
|
2885
|
+
scope,
|
|
2886
|
+
projectDir,
|
|
2887
|
+
newPath,
|
|
2888
|
+
"text"
|
|
2889
|
+
);
|
|
2890
|
+
removeStaleInstalls(reconciledText.stale, cacheKey);
|
|
2891
|
+
existingInstallations = reconciledText.remaining;
|
|
2719
2892
|
const cachePath = getPlatformFile(cacheKey, platformId, skill);
|
|
2720
2893
|
const transformed = adapter.transformContent(content, skill);
|
|
2721
2894
|
const installedPath = adapter.install({
|
|
@@ -2733,7 +2906,8 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2733
2906
|
method: actualMethod,
|
|
2734
2907
|
path: installedPath,
|
|
2735
2908
|
projectDir,
|
|
2736
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2909
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2910
|
+
format: "text"
|
|
2737
2911
|
};
|
|
2738
2912
|
installations.push(installation);
|
|
2739
2913
|
const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
|
|
@@ -2745,7 +2919,7 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2745
2919
|
version2,
|
|
2746
2920
|
resolvedSemver,
|
|
2747
2921
|
requestedRange,
|
|
2748
|
-
|
|
2922
|
+
existingInstallations,
|
|
2749
2923
|
installations
|
|
2750
2924
|
);
|
|
2751
2925
|
saveConfig(config);
|
|
@@ -2861,8 +3035,21 @@ ${res.data.length} skill(s) found.`);
|
|
|
2861
3035
|
});
|
|
2862
3036
|
|
|
2863
3037
|
// src/commands/pull.ts
|
|
2864
|
-
import { mkdirSync as mkdirSync8 } from "fs";
|
|
3038
|
+
import { mkdirSync as mkdirSync8, rmSync as rmSync5, cpSync as cpSync2, statSync as statSync2 } from "fs";
|
|
3039
|
+
import { dirname as dirname6 } from "path";
|
|
2865
3040
|
import { Command as Command5 } from "commander";
|
|
3041
|
+
function errMsg(err) {
|
|
3042
|
+
return err instanceof Error ? err.message : String(err);
|
|
3043
|
+
}
|
|
3044
|
+
function installedAsPackage(inst) {
|
|
3045
|
+
if (inst.format === "package") return true;
|
|
3046
|
+
if (inst.format === "text") return false;
|
|
3047
|
+
try {
|
|
3048
|
+
return statSync2(inst.path).isDirectory();
|
|
3049
|
+
} catch {
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
2866
3053
|
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
3054
|
const config = loadConfig();
|
|
2868
3055
|
const slugs = slugArg ? [slugArg] : Object.keys(config.installed_skills);
|
|
@@ -2898,53 +3085,129 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
|
|
|
2898
3085
|
skipped++;
|
|
2899
3086
|
continue;
|
|
2900
3087
|
}
|
|
3088
|
+
let allHandled = true;
|
|
2901
3089
|
if (format === "package") {
|
|
2902
3090
|
const { downloadUrl, manifest } = resData;
|
|
2903
|
-
|
|
3091
|
+
let zipBuffer;
|
|
3092
|
+
try {
|
|
3093
|
+
zipBuffer = await client.fetchBinary(downloadUrl);
|
|
3094
|
+
} catch (err) {
|
|
3095
|
+
spinner.stop(`${slug} \u2014 download failed: ${errMsg(err)}`);
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
2904
3098
|
storePackage(slug, zipBuffer, manifest, skill, version2);
|
|
3099
|
+
const pkgDir = getPackageDir(slug);
|
|
3100
|
+
const kept = [];
|
|
2905
3101
|
for (const installation of installed.installations) {
|
|
2906
3102
|
const adapter = getAdapter(installation.platform);
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
|
|
3103
|
+
const wasPackage = installedAsPackage(installation);
|
|
3104
|
+
if (!adapter.resolvePackageDir) {
|
|
3105
|
+
R2.warn(
|
|
3106
|
+
`${adapter.descriptor.name} can't hold a folder skill \u2014 keeping the existing install; it won't receive this update.`
|
|
3107
|
+
);
|
|
3108
|
+
kept.push(installation);
|
|
3109
|
+
allHandled = false;
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
try {
|
|
3113
|
+
const targetDir = adapter.resolvePackageDir(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
|
|
3114
|
+
const owned = wasPackage && installation.path === targetDir || !wasPackage && soleTrackedOccupant(targetDir, installation.path);
|
|
3115
|
+
const conflict = packageTargetConflict(targetDir, owned);
|
|
3116
|
+
if (conflict) {
|
|
3117
|
+
R2.warn(
|
|
3118
|
+
`${adapter.descriptor.name} \u2014 ${conflict}. Skipping; reinstall with \`localskills install ${slug}\`.`
|
|
3119
|
+
);
|
|
3120
|
+
kept.push(installation);
|
|
3121
|
+
allHandled = false;
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
if (!wasPackage) {
|
|
3125
|
+
adapter.uninstall(installation, slug);
|
|
3126
|
+
}
|
|
3127
|
+
const useMethod = installation.method === "copy" ? "copy" : "symlink";
|
|
3128
|
+
if (useMethod === "symlink") {
|
|
3129
|
+
createSymlink(pkgDir, targetDir);
|
|
3130
|
+
} else {
|
|
3131
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
3132
|
+
mkdirSync8(dirname6(targetDir), { recursive: true });
|
|
3133
|
+
cpSync2(pkgDir, targetDir, { recursive: true });
|
|
3134
|
+
}
|
|
3135
|
+
kept.push({ ...installation, method: useMethod, path: targetDir, format: "package" });
|
|
3136
|
+
} catch (err) {
|
|
3137
|
+
R2.warn(`${adapter.descriptor.name} \u2014 failed to update: ${errMsg(err)}`);
|
|
3138
|
+
kept.push(installation);
|
|
3139
|
+
allHandled = false;
|
|
3140
|
+
}
|
|
2910
3141
|
}
|
|
3142
|
+
installed.installations = kept;
|
|
2911
3143
|
} else {
|
|
2912
3144
|
const { content } = resData;
|
|
2913
3145
|
store(slug, content, skill, version2);
|
|
2914
3146
|
for (const installation of installed.installations) {
|
|
2915
|
-
if (installation.method === "symlink") {
|
|
2916
|
-
getPlatformFile(slug, installation.platform, skill);
|
|
2917
|
-
continue;
|
|
2918
|
-
}
|
|
2919
3147
|
const adapter = getAdapter(installation.platform);
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
installation
|
|
2924
|
-
|
|
2925
|
-
|
|
3148
|
+
try {
|
|
3149
|
+
const transformed = adapter.transformContent(content, skill);
|
|
3150
|
+
if (installedAsPackage(installation)) {
|
|
3151
|
+
adapter.uninstall(installation, slug);
|
|
3152
|
+
const method = installation.method === "section" ? "copy" : installation.method;
|
|
3153
|
+
const cachePath = getPlatformFile(slug, installation.platform, skill);
|
|
3154
|
+
const installedPath = adapter.install({
|
|
3155
|
+
slug,
|
|
3156
|
+
content: transformed,
|
|
3157
|
+
scope: installation.scope,
|
|
3158
|
+
method,
|
|
3159
|
+
cachePath,
|
|
3160
|
+
projectDir: installation.projectDir,
|
|
3161
|
+
contentType: skill.type
|
|
3162
|
+
});
|
|
3163
|
+
installation.path = installedPath;
|
|
3164
|
+
installation.method = method;
|
|
3165
|
+
installation.format = "text";
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
if (installation.method === "symlink") {
|
|
3169
|
+
getPlatformFile(slug, installation.platform, skill);
|
|
3170
|
+
installation.format = "text";
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
3173
|
+
if (installation.method === "section") {
|
|
3174
|
+
upsertSection(
|
|
3175
|
+
installation.path,
|
|
3176
|
+
slug,
|
|
3177
|
+
`## ${slug}
|
|
2926
3178
|
|
|
2927
3179
|
${transformed}`
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3180
|
+
);
|
|
3181
|
+
} else {
|
|
3182
|
+
const cachePath = getPlatformFile(slug, installation.platform, skill);
|
|
3183
|
+
adapter.install({
|
|
3184
|
+
slug,
|
|
3185
|
+
content: transformed,
|
|
3186
|
+
scope: installation.scope,
|
|
3187
|
+
method: "copy",
|
|
3188
|
+
cachePath,
|
|
3189
|
+
projectDir: installation.projectDir,
|
|
3190
|
+
contentType: skill.type
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
installation.format = "text";
|
|
3194
|
+
} catch (err) {
|
|
3195
|
+
R2.warn(`${adapter.descriptor.name} \u2014 failed to update: ${errMsg(err)}`);
|
|
3196
|
+
allHandled = false;
|
|
2939
3197
|
}
|
|
2940
3198
|
}
|
|
2941
3199
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3200
|
+
if (allHandled) {
|
|
3201
|
+
installed.hash = skill.contentHash;
|
|
3202
|
+
installed.version = version2;
|
|
3203
|
+
installed.semver = resData.semver ?? null;
|
|
3204
|
+
installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3205
|
+
updated++;
|
|
3206
|
+
spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version2)}`);
|
|
3207
|
+
} else {
|
|
3208
|
+
updated++;
|
|
3209
|
+
spinner.stop(`${slug} \u2014 partially updated to ${formatVersionLabel(res.data.semver, version2)} (see warnings above)`);
|
|
3210
|
+
}
|
|
2948
3211
|
}
|
|
2949
3212
|
saveConfig(config);
|
|
2950
3213
|
Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
|
|
@@ -2952,44 +3215,43 @@ ${transformed}`
|
|
|
2952
3215
|
|
|
2953
3216
|
// src/commands/publish.ts
|
|
2954
3217
|
import { Command as Command6 } from "commander";
|
|
2955
|
-
import { readFileSync as
|
|
2956
|
-
import { resolve as resolve4, basename as
|
|
2957
|
-
import { homedir as
|
|
3218
|
+
import { readFileSync as readFileSync7, existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
3219
|
+
import { resolve as resolve4, basename as basename3, extname as extname2 } from "path";
|
|
3220
|
+
import { homedir as homedir11 } from "os";
|
|
2958
3221
|
|
|
2959
3222
|
// 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";
|
|
3223
|
+
import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "fs";
|
|
3224
|
+
import { join as join15, basename as basename2, extname, dirname as dirname7 } from "path";
|
|
3225
|
+
import { homedir as homedir10 } from "os";
|
|
2964
3226
|
function scanForSkills(projectDir) {
|
|
2965
|
-
const home =
|
|
3227
|
+
const home = homedir10();
|
|
2966
3228
|
const cwd = projectDir || process.cwd();
|
|
2967
3229
|
const results = [];
|
|
2968
|
-
scanDirectory(
|
|
2969
|
-
scanDirectory(
|
|
2970
|
-
scanClaudeSkills(
|
|
2971
|
-
scanClaudeSkills(
|
|
2972
|
-
scanDirectory(
|
|
2973
|
-
scanDirectory(
|
|
2974
|
-
scanSingleFile(
|
|
2975
|
-
scanSingleFile(
|
|
3230
|
+
scanDirectory(join15(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
|
|
3231
|
+
scanDirectory(join15(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
|
|
3232
|
+
scanClaudeSkills(join15(home, ".claude", "skills"), "global", results);
|
|
3233
|
+
scanClaudeSkills(join15(cwd, ".claude", "skills"), "project", results);
|
|
3234
|
+
scanDirectory(join15(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
|
|
3235
|
+
scanDirectory(join15(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
|
|
3236
|
+
scanSingleFile(join15(home, ".codex", "AGENTS.md"), "codex", "global", results);
|
|
3237
|
+
scanSingleFile(join15(cwd, "AGENTS.md"), "codex", "project", results);
|
|
2976
3238
|
scanSingleFile(
|
|
2977
|
-
|
|
3239
|
+
join15(home, ".codeium", "windsurf", "memories", "global_rules.md"),
|
|
2978
3240
|
"windsurf",
|
|
2979
3241
|
"global",
|
|
2980
3242
|
results
|
|
2981
3243
|
);
|
|
2982
|
-
scanDirectory(
|
|
2983
|
-
scanDirectory(
|
|
3244
|
+
scanDirectory(join15(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
|
|
3245
|
+
scanDirectory(join15(cwd, ".clinerules"), ".md", "cline", "project", results);
|
|
2984
3246
|
scanSingleFile(
|
|
2985
|
-
|
|
3247
|
+
join15(cwd, ".github", "copilot-instructions.md"),
|
|
2986
3248
|
"copilot",
|
|
2987
3249
|
"project",
|
|
2988
3250
|
results
|
|
2989
3251
|
);
|
|
2990
|
-
scanDirectory(
|
|
2991
|
-
scanDirectory(
|
|
2992
|
-
scanDirectory(
|
|
3252
|
+
scanDirectory(join15(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
|
|
3253
|
+
scanDirectory(join15(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
|
|
3254
|
+
scanDirectory(join15(cwd, ".aider", "skills"), ".md", "aider", "project", results);
|
|
2993
3255
|
return results;
|
|
2994
3256
|
}
|
|
2995
3257
|
function filterTracked(detected, config) {
|
|
@@ -2999,35 +3261,32 @@ function filterTracked(detected, config) {
|
|
|
2999
3261
|
trackedPaths.add(inst.path);
|
|
3000
3262
|
}
|
|
3001
3263
|
}
|
|
3002
|
-
const cacheDir =
|
|
3264
|
+
const cacheDir = join15(homedir10(), ".localskills", "cache");
|
|
3003
3265
|
return detected.filter((skill) => {
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
if (
|
|
3008
|
-
|
|
3009
|
-
if (target.startsWith(cacheDir)) return false;
|
|
3010
|
-
}
|
|
3011
|
-
} catch {
|
|
3266
|
+
const candidates = [skill.filePath, dirname7(skill.filePath)];
|
|
3267
|
+
if (skill.dir) candidates.push(skill.dir);
|
|
3268
|
+
for (const path of candidates) {
|
|
3269
|
+
if (trackedPaths.has(path)) return false;
|
|
3270
|
+
if (isSymlinkInto(path, cacheDir)) return false;
|
|
3012
3271
|
}
|
|
3013
3272
|
return true;
|
|
3014
3273
|
});
|
|
3015
3274
|
}
|
|
3016
3275
|
function slugFromFilename(filename) {
|
|
3017
|
-
return
|
|
3276
|
+
return basename2(filename, extname(filename));
|
|
3018
3277
|
}
|
|
3019
3278
|
var nameFromSlug = titleFromSlug;
|
|
3020
3279
|
function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
|
|
3021
3280
|
if (!existsSync14(dir)) return;
|
|
3022
3281
|
let entries;
|
|
3023
3282
|
try {
|
|
3024
|
-
entries =
|
|
3283
|
+
entries = readdirSync4(dir);
|
|
3025
3284
|
} catch {
|
|
3026
3285
|
return;
|
|
3027
3286
|
}
|
|
3028
3287
|
for (const entry of entries) {
|
|
3029
3288
|
if (!entry.endsWith(ext)) continue;
|
|
3030
|
-
const filePath =
|
|
3289
|
+
const filePath = join15(dir, entry);
|
|
3031
3290
|
try {
|
|
3032
3291
|
const raw = readFileSync5(filePath, "utf-8");
|
|
3033
3292
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -3035,6 +3294,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
3035
3294
|
const slug = slugFromFilename(entry);
|
|
3036
3295
|
results.push({
|
|
3037
3296
|
filePath,
|
|
3297
|
+
format: "text",
|
|
3038
3298
|
platform,
|
|
3039
3299
|
scope,
|
|
3040
3300
|
suggestedName: nameFromSlug(slug),
|
|
@@ -3046,28 +3306,56 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
3046
3306
|
}
|
|
3047
3307
|
}
|
|
3048
3308
|
}
|
|
3309
|
+
var SCAN_JUNK = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".git", "node_modules", ".localskills"]);
|
|
3310
|
+
function dirHasFilesBesides(dir, excludeRootFile) {
|
|
3311
|
+
const stack = [dir];
|
|
3312
|
+
while (stack.length > 0) {
|
|
3313
|
+
const current = stack.pop();
|
|
3314
|
+
let entries;
|
|
3315
|
+
try {
|
|
3316
|
+
entries = readdirSync4(current, { withFileTypes: true });
|
|
3317
|
+
} catch {
|
|
3318
|
+
continue;
|
|
3319
|
+
}
|
|
3320
|
+
for (const entry of entries) {
|
|
3321
|
+
if (SCAN_JUNK.has(entry.name)) continue;
|
|
3322
|
+
if (entry.isDirectory()) {
|
|
3323
|
+
stack.push(join15(current, entry.name));
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
if (current === dir && entry.name === excludeRootFile) continue;
|
|
3327
|
+
return true;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
return false;
|
|
3331
|
+
}
|
|
3049
3332
|
function scanClaudeSkills(skillsDir, scope, results) {
|
|
3050
3333
|
if (!existsSync14(skillsDir)) return;
|
|
3051
3334
|
let entries;
|
|
3052
3335
|
try {
|
|
3053
|
-
entries =
|
|
3336
|
+
entries = readdirSync4(skillsDir);
|
|
3054
3337
|
} catch {
|
|
3055
3338
|
return;
|
|
3056
3339
|
}
|
|
3057
3340
|
for (const entry of entries) {
|
|
3058
|
-
const
|
|
3341
|
+
const skillDir = join15(skillsDir, entry);
|
|
3342
|
+
const skillFile = join15(skillDir, "SKILL.md");
|
|
3059
3343
|
if (!existsSync14(skillFile)) continue;
|
|
3060
3344
|
try {
|
|
3061
3345
|
const raw = readFileSync5(skillFile, "utf-8");
|
|
3062
3346
|
const content = stripFrontmatter(raw).trim();
|
|
3063
|
-
|
|
3347
|
+
const isPackage = dirHasFilesBesides(skillDir, "SKILL.md");
|
|
3348
|
+
if (!isPackage && !content) continue;
|
|
3064
3349
|
results.push({
|
|
3065
3350
|
filePath: skillFile,
|
|
3351
|
+
...isPackage ? { dir: skillDir } : {},
|
|
3352
|
+
format: isPackage ? "package" : "text",
|
|
3066
3353
|
platform: "claude",
|
|
3067
3354
|
scope,
|
|
3068
3355
|
suggestedName: nameFromSlug(entry),
|
|
3069
3356
|
suggestedSlug: entry,
|
|
3070
3357
|
content,
|
|
3358
|
+
// for packages: SKILL.md body, used only for the hint/preview
|
|
3071
3359
|
contentType: "skill"
|
|
3072
3360
|
});
|
|
3073
3361
|
} catch {
|
|
@@ -3094,6 +3382,7 @@ function scanSingleFile(filePath, platform, scope, results) {
|
|
|
3094
3382
|
if (!content2) continue;
|
|
3095
3383
|
results.push({
|
|
3096
3384
|
filePath,
|
|
3385
|
+
format: "text",
|
|
3097
3386
|
platform,
|
|
3098
3387
|
scope,
|
|
3099
3388
|
suggestedName: nameFromSlug(slug2),
|
|
@@ -3109,6 +3398,7 @@ function scanSingleFile(filePath, platform, scope, results) {
|
|
|
3109
3398
|
const slug = slugFromFilename(filePath);
|
|
3110
3399
|
results.push({
|
|
3111
3400
|
filePath,
|
|
3401
|
+
format: "text",
|
|
3112
3402
|
platform,
|
|
3113
3403
|
scope,
|
|
3114
3404
|
suggestedName: nameFromSlug(slug),
|
|
@@ -3118,8 +3408,177 @@ function scanSingleFile(filePath, platform, scope, results) {
|
|
|
3118
3408
|
});
|
|
3119
3409
|
}
|
|
3120
3410
|
|
|
3411
|
+
// src/lib/pack.ts
|
|
3412
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
3413
|
+
import { join as join16, relative as relative2, sep as sep2 } from "path";
|
|
3414
|
+
import { zipSync } from "fflate";
|
|
3415
|
+
var PackError = class extends Error {
|
|
3416
|
+
constructor(message) {
|
|
3417
|
+
super(message);
|
|
3418
|
+
this.name = "PackError";
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
function zipBlob(zip) {
|
|
3422
|
+
return new Blob([new Uint8Array(zip)], { type: "application/zip" });
|
|
3423
|
+
}
|
|
3424
|
+
var SKIP = /* @__PURE__ */ new Set([
|
|
3425
|
+
".DS_Store",
|
|
3426
|
+
".git",
|
|
3427
|
+
"node_modules",
|
|
3428
|
+
".localskills",
|
|
3429
|
+
"Thumbs.db"
|
|
3430
|
+
]);
|
|
3431
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3432
|
+
".png",
|
|
3433
|
+
".jpg",
|
|
3434
|
+
".jpeg",
|
|
3435
|
+
".gif",
|
|
3436
|
+
".webp",
|
|
3437
|
+
".ico",
|
|
3438
|
+
".bmp",
|
|
3439
|
+
".tiff",
|
|
3440
|
+
".woff",
|
|
3441
|
+
".woff2",
|
|
3442
|
+
".ttf",
|
|
3443
|
+
".eot",
|
|
3444
|
+
".otf",
|
|
3445
|
+
".pdf",
|
|
3446
|
+
".zip",
|
|
3447
|
+
".tar",
|
|
3448
|
+
".gz",
|
|
3449
|
+
".bz2",
|
|
3450
|
+
".7z",
|
|
3451
|
+
".rar",
|
|
3452
|
+
".exe",
|
|
3453
|
+
".dll",
|
|
3454
|
+
".so",
|
|
3455
|
+
".dylib",
|
|
3456
|
+
".bin",
|
|
3457
|
+
".wasm",
|
|
3458
|
+
".pyc",
|
|
3459
|
+
".class",
|
|
3460
|
+
".o",
|
|
3461
|
+
".obj",
|
|
3462
|
+
".mp3",
|
|
3463
|
+
".mp4",
|
|
3464
|
+
".wav",
|
|
3465
|
+
".avi",
|
|
3466
|
+
".mov",
|
|
3467
|
+
".mkv",
|
|
3468
|
+
".sqlite",
|
|
3469
|
+
".db"
|
|
3470
|
+
]);
|
|
3471
|
+
function getExtension(path) {
|
|
3472
|
+
const dot = path.lastIndexOf(".");
|
|
3473
|
+
return dot === -1 ? "" : path.substring(dot).toLowerCase();
|
|
3474
|
+
}
|
|
3475
|
+
function isBinaryByContent(data) {
|
|
3476
|
+
const sample = data.subarray(0, 8192);
|
|
3477
|
+
for (let i = 0; i < sample.length; i++) if (sample[i] === 0) return true;
|
|
3478
|
+
return false;
|
|
3479
|
+
}
|
|
3480
|
+
function validatePath(path) {
|
|
3481
|
+
if (path.startsWith("/") || path.startsWith("\\")) return false;
|
|
3482
|
+
if (path.includes("..")) return false;
|
|
3483
|
+
if (path.includes("\0")) return false;
|
|
3484
|
+
if (path.includes("\\")) return false;
|
|
3485
|
+
if (path === "__proto__") return false;
|
|
3486
|
+
return true;
|
|
3487
|
+
}
|
|
3488
|
+
function walk(root, current, acc) {
|
|
3489
|
+
for (const entry of readdirSync5(current, { withFileTypes: true })) {
|
|
3490
|
+
if (SKIP.has(entry.name)) continue;
|
|
3491
|
+
const abs = join16(current, entry.name);
|
|
3492
|
+
const rel = relative2(root, abs).split(sep2).join("/");
|
|
3493
|
+
let isFile = entry.isFile();
|
|
3494
|
+
let size;
|
|
3495
|
+
if (entry.isDirectory()) {
|
|
3496
|
+
walk(root, abs, acc);
|
|
3497
|
+
continue;
|
|
3498
|
+
}
|
|
3499
|
+
if (entry.isSymbolicLink()) {
|
|
3500
|
+
let st3;
|
|
3501
|
+
try {
|
|
3502
|
+
st3 = statSync3(abs);
|
|
3503
|
+
} catch {
|
|
3504
|
+
acc.warnings.push(`Skipped broken symlink: ${rel}`);
|
|
3505
|
+
continue;
|
|
3506
|
+
}
|
|
3507
|
+
if (st3.isDirectory()) {
|
|
3508
|
+
acc.warnings.push(`Skipped symlinked directory: ${rel}/`);
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
isFile = st3.isFile();
|
|
3512
|
+
size = st3.size;
|
|
3513
|
+
}
|
|
3514
|
+
if (!isFile) continue;
|
|
3515
|
+
acc.count++;
|
|
3516
|
+
if (acc.count > PACKAGE_MAX_FILE_COUNT) {
|
|
3517
|
+
throw new PackError(
|
|
3518
|
+
`Folder contains more than ${PACKAGE_MAX_FILE_COUNT} files`
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
size ??= statSync3(abs).size;
|
|
3522
|
+
if (acc.bytes + size > PACKAGE_MAX_DECOMPRESSED_BYTES) {
|
|
3523
|
+
throw new PackError(
|
|
3524
|
+
`Folder exceeds ${PACKAGE_MAX_DECOMPRESSED_BYTES / 1024 / 1024} MB uncompressed limit`
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
const data = new Uint8Array(readFileSync6(abs));
|
|
3528
|
+
acc.bytes += data.length;
|
|
3529
|
+
if (acc.bytes > PACKAGE_MAX_DECOMPRESSED_BYTES) {
|
|
3530
|
+
throw new PackError(
|
|
3531
|
+
`Folder exceeds ${PACKAGE_MAX_DECOMPRESSED_BYTES / 1024 / 1024} MB uncompressed limit`
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
acc.files.set(rel, data);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
function packFolder(dir, opts = {}) {
|
|
3538
|
+
const requireSkillMd = opts.requireSkillMd ?? true;
|
|
3539
|
+
const stat = statSync3(dir);
|
|
3540
|
+
if (!stat.isDirectory()) throw new PackError(`Not a directory: ${dir}`);
|
|
3541
|
+
const acc = { files: /* @__PURE__ */ new Map(), count: 0, bytes: 0, warnings: [] };
|
|
3542
|
+
walk(dir, dir, acc);
|
|
3543
|
+
const entries = acc.files;
|
|
3544
|
+
if (entries.size === 0) throw new PackError("Folder contains no files");
|
|
3545
|
+
if (requireSkillMd && !entries.has("SKILL.md")) {
|
|
3546
|
+
throw new PackError(
|
|
3547
|
+
"No SKILL.md at the folder root. A skill folder must contain a SKILL.md file."
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
let totalSize = 0;
|
|
3551
|
+
const files = [];
|
|
3552
|
+
for (const [path, data] of entries) {
|
|
3553
|
+
if (!validatePath(path)) throw new PackError(`Invalid file path: ${path}`);
|
|
3554
|
+
totalSize += data.length;
|
|
3555
|
+
const binary = BINARY_EXTENSIONS.has(getExtension(path)) || isBinaryByContent(data);
|
|
3556
|
+
const file = { path, size: data.length, isBinary: binary };
|
|
3557
|
+
if (!binary && data.length > 0) {
|
|
3558
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(data);
|
|
3559
|
+
file.preview = text.length > PACKAGE_PREVIEW_BYTES ? text.substring(0, PACKAGE_PREVIEW_BYTES) : text;
|
|
3560
|
+
}
|
|
3561
|
+
files.push(file);
|
|
3562
|
+
}
|
|
3563
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
3564
|
+
const zipInput = /* @__PURE__ */ Object.create(null);
|
|
3565
|
+
for (const [path, data] of entries) zipInput[path] = data;
|
|
3566
|
+
const zip = Buffer.from(zipSync(zipInput));
|
|
3567
|
+
if (zip.length > PACKAGE_MAX_COMPRESSED_BYTES) {
|
|
3568
|
+
throw new PackError(
|
|
3569
|
+
`Archive exceeds ${PACKAGE_MAX_COMPRESSED_BYTES / 1024 / 1024} MB compressed limit`
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
return {
|
|
3573
|
+
zip,
|
|
3574
|
+
manifest: { version: 1, totalSize, archiveSize: zip.length, files },
|
|
3575
|
+
fileCount: files.length,
|
|
3576
|
+
warnings: acc.warnings
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3121
3580
|
// src/commands/publish.ts
|
|
3122
|
-
var publishCommand = new Command6("publish").description("Publish local
|
|
3581
|
+
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
3582
|
"--visibility <visibility>",
|
|
3124
3583
|
"Visibility: public, private, or unlisted",
|
|
3125
3584
|
"private"
|
|
@@ -3139,30 +3598,46 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
3139
3598
|
if (fileArg) {
|
|
3140
3599
|
const filePath = resolve4(fileArg);
|
|
3141
3600
|
if (!existsSync15(filePath)) {
|
|
3142
|
-
console.error(`
|
|
3601
|
+
console.error(`Path not found: ${filePath}`);
|
|
3143
3602
|
process.exit(1);
|
|
3144
3603
|
return;
|
|
3145
3604
|
}
|
|
3146
|
-
|
|
3605
|
+
if (statSync4(filePath).isDirectory()) {
|
|
3606
|
+
const contentType2 = validateContentType(opts.type || "skill");
|
|
3607
|
+
const visibility2 = validateVisibility(opts.visibility || "private");
|
|
3608
|
+
const tenantId2 = await resolveTeam(teams, opts.team);
|
|
3609
|
+
const skillName2 = opts.name || titleFromSlug(basename3(filePath));
|
|
3610
|
+
const ok2 = await uploadPackage(client, {
|
|
3611
|
+
name: skillName2,
|
|
3612
|
+
dir: filePath,
|
|
3613
|
+
tenantId: tenantId2,
|
|
3614
|
+
visibility: visibility2,
|
|
3615
|
+
type: contentType2
|
|
3616
|
+
});
|
|
3617
|
+
if (!ok2) process.exit(1);
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3147
3621
|
const content = stripFrontmatter(raw).trim();
|
|
3148
3622
|
if (!content) {
|
|
3149
3623
|
console.error("File is empty after stripping frontmatter.");
|
|
3150
3624
|
process.exit(1);
|
|
3151
3625
|
return;
|
|
3152
3626
|
}
|
|
3153
|
-
const defaultSlug =
|
|
3627
|
+
const defaultSlug = basename3(filePath, extname2(filePath));
|
|
3154
3628
|
const defaultName = titleFromSlug(defaultSlug);
|
|
3155
3629
|
const skillName = opts.name || defaultName;
|
|
3156
3630
|
const contentType = validateContentType(opts.type || "skill");
|
|
3157
3631
|
const visibility = validateVisibility(opts.visibility || "private");
|
|
3158
3632
|
const tenantId = await resolveTeam(teams, opts.team);
|
|
3159
|
-
await uploadSkill(client, {
|
|
3633
|
+
const ok = await uploadSkill(client, {
|
|
3160
3634
|
name: skillName,
|
|
3161
3635
|
content,
|
|
3162
3636
|
tenantId,
|
|
3163
3637
|
visibility,
|
|
3164
3638
|
type: contentType
|
|
3165
3639
|
});
|
|
3640
|
+
if (!ok) process.exit(1);
|
|
3166
3641
|
} else {
|
|
3167
3642
|
We("localskills publish");
|
|
3168
3643
|
const spinner = bt2();
|
|
@@ -3187,6 +3662,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
3187
3662
|
required: true
|
|
3188
3663
|
}));
|
|
3189
3664
|
const tenantId = await resolveTeam(teams, opts.team);
|
|
3665
|
+
let failures = 0;
|
|
3190
3666
|
for (const skill of skills) {
|
|
3191
3667
|
R2.step(`Publishing ${skill.suggestedName}...`);
|
|
3192
3668
|
const name = cancelGuard(await Ze({
|
|
@@ -3214,15 +3690,23 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
3214
3690
|
],
|
|
3215
3691
|
initialValue: skill.contentType
|
|
3216
3692
|
}));
|
|
3217
|
-
await
|
|
3693
|
+
const ok = skill.format === "package" && skill.dir ? await uploadPackage(client, {
|
|
3694
|
+
name,
|
|
3695
|
+
dir: skill.dir,
|
|
3696
|
+
tenantId,
|
|
3697
|
+
visibility,
|
|
3698
|
+
type: contentType
|
|
3699
|
+
}) : await uploadSkill(client, {
|
|
3218
3700
|
name,
|
|
3219
3701
|
content: skill.content,
|
|
3220
3702
|
tenantId,
|
|
3221
3703
|
visibility,
|
|
3222
3704
|
type: contentType
|
|
3223
3705
|
});
|
|
3706
|
+
if (!ok) failures++;
|
|
3224
3707
|
}
|
|
3225
|
-
Le("Done!");
|
|
3708
|
+
Le(failures > 0 ? `Done, with ${failures} failure(s).` : "Done!");
|
|
3709
|
+
if (failures > 0) process.exit(1);
|
|
3226
3710
|
}
|
|
3227
3711
|
}
|
|
3228
3712
|
);
|
|
@@ -3259,10 +3743,37 @@ async function uploadSkill(client, params) {
|
|
|
3259
3743
|
});
|
|
3260
3744
|
if (!res.success || !res.data) {
|
|
3261
3745
|
spinner.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3262
|
-
return;
|
|
3746
|
+
return false;
|
|
3263
3747
|
}
|
|
3264
3748
|
spinner.stop(`Published!`);
|
|
3265
3749
|
R2.success(`\u2192 localskills.sh/s/${res.data.slug}`);
|
|
3750
|
+
return true;
|
|
3751
|
+
}
|
|
3752
|
+
async function uploadPackage(client, params) {
|
|
3753
|
+
const spinner = bt2();
|
|
3754
|
+
let packed;
|
|
3755
|
+
try {
|
|
3756
|
+
packed = packFolder(params.dir, { requireSkillMd: params.type === "skill" });
|
|
3757
|
+
} catch (e2) {
|
|
3758
|
+
R2.error(e2 instanceof PackError ? e2.message : String(e2));
|
|
3759
|
+
return false;
|
|
3760
|
+
}
|
|
3761
|
+
for (const w of packed.warnings) R2.warn(w);
|
|
3762
|
+
spinner.start(`Uploading ${params.name} (${packed.fileCount} files)...`);
|
|
3763
|
+
const form = new FormData();
|
|
3764
|
+
form.append("name", params.name);
|
|
3765
|
+
form.append("tenantId", params.tenantId);
|
|
3766
|
+
form.append("visibility", params.visibility);
|
|
3767
|
+
form.append("type", params.type);
|
|
3768
|
+
form.append("file", zipBlob(packed.zip), "skill.zip");
|
|
3769
|
+
const res = await client.postForm("/api/skills", form);
|
|
3770
|
+
if (!res.success || !res.data) {
|
|
3771
|
+
spinner.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3772
|
+
return false;
|
|
3773
|
+
}
|
|
3774
|
+
spinner.stop(`Published! (${packed.fileCount} files)`);
|
|
3775
|
+
R2.success(`\u2192 localskills.sh/s/${res.data.slug}`);
|
|
3776
|
+
return true;
|
|
3266
3777
|
}
|
|
3267
3778
|
function validateContentType(value) {
|
|
3268
3779
|
if (value === "skill" || value === "rule") {
|
|
@@ -3279,7 +3790,7 @@ function validateVisibility(value) {
|
|
|
3279
3790
|
process.exit(1);
|
|
3280
3791
|
}
|
|
3281
3792
|
function shortenPath(filePath) {
|
|
3282
|
-
const home =
|
|
3793
|
+
const home = homedir11();
|
|
3283
3794
|
if (filePath.startsWith(home)) {
|
|
3284
3795
|
return "~" + filePath.slice(home.length);
|
|
3285
3796
|
}
|
|
@@ -3292,41 +3803,78 @@ function shortenPath(filePath) {
|
|
|
3292
3803
|
|
|
3293
3804
|
// src/commands/push.ts
|
|
3294
3805
|
import { Command as Command7 } from "commander";
|
|
3295
|
-
import { readFileSync as
|
|
3806
|
+
import { readFileSync as readFileSync8, existsSync as existsSync16, statSync as statSync5 } from "fs";
|
|
3296
3807
|
import { resolve as resolve5 } from "path";
|
|
3297
|
-
var pushCommand = new Command7("push").description("Push a new version of an existing skill").argument("<
|
|
3808
|
+
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
3809
|
async (fileArg, opts) => {
|
|
3299
3810
|
const client = new ApiClient();
|
|
3300
3811
|
requireAuth(client);
|
|
3301
3812
|
const filePath = resolve5(fileArg);
|
|
3302
3813
|
if (!existsSync16(filePath)) {
|
|
3303
|
-
console.error(`
|
|
3814
|
+
console.error(`Path not found: ${filePath}`);
|
|
3304
3815
|
process.exit(1);
|
|
3305
3816
|
return;
|
|
3306
3817
|
}
|
|
3307
|
-
const
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
console.error("File is empty after stripping frontmatter.");
|
|
3818
|
+
const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
|
|
3819
|
+
if (bumpFlags.length > 1) {
|
|
3820
|
+
console.error("Specify only one of --patch, --minor, or --major");
|
|
3311
3821
|
process.exit(1);
|
|
3312
3822
|
return;
|
|
3313
3823
|
}
|
|
3314
|
-
const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
|
|
3315
3824
|
if (opts.version && bumpFlags.length > 0) {
|
|
3316
3825
|
console.error("Cannot specify both --version and --patch/--minor/--major");
|
|
3317
3826
|
process.exit(1);
|
|
3318
3827
|
return;
|
|
3319
3828
|
}
|
|
3829
|
+
if (opts.version && !isValidSemVer(opts.version)) {
|
|
3830
|
+
console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
|
|
3831
|
+
process.exit(1);
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
if (statSync5(filePath).isDirectory()) {
|
|
3835
|
+
let packed;
|
|
3836
|
+
try {
|
|
3837
|
+
packed = packFolder(filePath, { requireSkillMd: false });
|
|
3838
|
+
} catch (e2) {
|
|
3839
|
+
console.error(e2 instanceof PackError ? e2.message : String(e2));
|
|
3840
|
+
process.exit(1);
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
for (const w of packed.warnings) R2.warn(w);
|
|
3844
|
+
const form = new FormData();
|
|
3845
|
+
form.append("file", zipBlob(packed.zip), "skill.zip");
|
|
3846
|
+
if (opts.message) form.append("message", opts.message);
|
|
3847
|
+
if (opts.version) form.append("semver", opts.version);
|
|
3848
|
+
else if (opts.major) form.append("bump", "major");
|
|
3849
|
+
else if (opts.minor) form.append("bump", "minor");
|
|
3850
|
+
else if (opts.patch) form.append("bump", "patch");
|
|
3851
|
+
const spinner2 = bt2();
|
|
3852
|
+
spinner2.start(`Pushing new version (${packed.fileCount} files)...`);
|
|
3853
|
+
const res2 = await client.postForm(
|
|
3854
|
+
`/api/skills/${encodeURIComponent(opts.skill)}/versions`,
|
|
3855
|
+
form
|
|
3856
|
+
);
|
|
3857
|
+
if (!res2.success || !res2.data) {
|
|
3858
|
+
spinner2.stop(`Failed: ${res2.error || "Unknown error"}`);
|
|
3859
|
+
process.exit(1);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
spinner2.stop(`Pushed ${formatVersionLabel(res2.data.semver, res2.data.version)}`);
|
|
3863
|
+
Le("Done!");
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
3867
|
+
const content = stripFrontmatter(raw).trim();
|
|
3868
|
+
if (!content) {
|
|
3869
|
+
console.error("File is empty after stripping frontmatter.");
|
|
3870
|
+
process.exit(1);
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3320
3873
|
let body = {
|
|
3321
3874
|
content,
|
|
3322
3875
|
message: opts.message
|
|
3323
3876
|
};
|
|
3324
3877
|
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
3878
|
body.semver = opts.version;
|
|
3331
3879
|
} else if (opts.major) {
|
|
3332
3880
|
body.bump = "major";
|
|
@@ -3354,30 +3902,49 @@ var pushCommand = new Command7("push").description("Push a new version of an exi
|
|
|
3354
3902
|
|
|
3355
3903
|
// src/commands/share.ts
|
|
3356
3904
|
import { Command as Command8 } from "commander";
|
|
3357
|
-
import { readFileSync as
|
|
3358
|
-
import { resolve as resolve6, basename as
|
|
3905
|
+
import { readFileSync as readFileSync9, existsSync as existsSync17, statSync as statSync6 } from "fs";
|
|
3906
|
+
import { resolve as resolve6, basename as basename4, extname as extname3 } from "path";
|
|
3359
3907
|
import { generateKeyPairSync } from "crypto";
|
|
3360
|
-
var shareCommand = new Command8("share").description("Share a skill anonymously (no login required)").argument("[
|
|
3908
|
+
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
3909
|
We("localskills share");
|
|
3910
|
+
if (opts.type && opts.type !== "skill" && opts.type !== "rule") {
|
|
3911
|
+
R2.error(`Invalid type: ${opts.type}. Use skill or rule.`);
|
|
3912
|
+
process.exit(1);
|
|
3913
|
+
}
|
|
3362
3914
|
await ensureAnonymousIdentity();
|
|
3363
3915
|
const client = new ApiClient();
|
|
3364
3916
|
if (fileArg) {
|
|
3365
3917
|
const filePath = resolve6(fileArg);
|
|
3366
3918
|
if (!existsSync17(filePath)) {
|
|
3367
|
-
R2.error(`
|
|
3919
|
+
R2.error(`Path not found: ${filePath}`);
|
|
3368
3920
|
process.exit(1);
|
|
3369
3921
|
}
|
|
3370
|
-
|
|
3922
|
+
if (statSync6(filePath).isDirectory()) {
|
|
3923
|
+
const contentType2 = opts.type === "rule" ? "rule" : "skill";
|
|
3924
|
+
const skillName2 = opts.name || titleFromSlug(basename4(filePath));
|
|
3925
|
+
const ok2 = await uploadAnonymousPackage(client, {
|
|
3926
|
+
name: skillName2,
|
|
3927
|
+
dir: filePath,
|
|
3928
|
+
type: contentType2
|
|
3929
|
+
});
|
|
3930
|
+
Le(ok2 ? "Done!" : "Share failed.");
|
|
3931
|
+
if (!ok2) process.exit(1);
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
const raw = readFileSync9(filePath, "utf-8");
|
|
3371
3935
|
const content = stripFrontmatter(raw).trim();
|
|
3372
3936
|
if (!content) {
|
|
3373
3937
|
R2.error("File is empty after stripping frontmatter.");
|
|
3374
3938
|
process.exit(1);
|
|
3375
3939
|
}
|
|
3376
|
-
const defaultSlug =
|
|
3940
|
+
const defaultSlug = basename4(filePath, extname3(filePath));
|
|
3377
3941
|
const defaultName = titleFromSlug(defaultSlug);
|
|
3378
3942
|
const skillName = opts.name || defaultName;
|
|
3379
3943
|
const contentType = opts.type === "rule" ? "rule" : "skill";
|
|
3380
|
-
await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
|
|
3944
|
+
const ok = await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
|
|
3945
|
+
Le(ok ? "Done!" : "Share failed.");
|
|
3946
|
+
if (!ok) process.exit(1);
|
|
3947
|
+
return;
|
|
3381
3948
|
} else {
|
|
3382
3949
|
const spinner = bt2();
|
|
3383
3950
|
spinner.start("Scanning for skills...");
|
|
@@ -3411,13 +3978,18 @@ var shareCommand = new Command8("share").description("Share a skill anonymously
|
|
|
3411
3978
|
}
|
|
3412
3979
|
})
|
|
3413
3980
|
);
|
|
3414
|
-
await
|
|
3981
|
+
const ok = selected.format === "package" && selected.dir ? await uploadAnonymousPackage(client, {
|
|
3982
|
+
name,
|
|
3983
|
+
dir: selected.dir,
|
|
3984
|
+
type: selected.contentType
|
|
3985
|
+
}) : await uploadAnonymousSkill(client, {
|
|
3415
3986
|
name,
|
|
3416
3987
|
content: selected.content,
|
|
3417
3988
|
type: selected.contentType
|
|
3418
3989
|
});
|
|
3990
|
+
Le(ok ? "Done!" : "Share failed.");
|
|
3991
|
+
if (!ok) process.exit(1);
|
|
3419
3992
|
}
|
|
3420
|
-
Le("Done!");
|
|
3421
3993
|
});
|
|
3422
3994
|
async function ensureAnonymousIdentity() {
|
|
3423
3995
|
const config = loadConfig();
|
|
@@ -3425,6 +3997,10 @@ async function ensureAnonymousIdentity() {
|
|
|
3425
3997
|
const client = new ApiClient();
|
|
3426
3998
|
const res2 = await client.get("/api/cli/auth");
|
|
3427
3999
|
if (res2.success) return;
|
|
4000
|
+
if (res2.networkError) {
|
|
4001
|
+
R2.error(`Could not verify your login: ${res2.error}. Try again.`);
|
|
4002
|
+
process.exit(1);
|
|
4003
|
+
}
|
|
3428
4004
|
}
|
|
3429
4005
|
let keyPair = getAnonymousKey();
|
|
3430
4006
|
if (!keyPair) {
|
|
@@ -3475,11 +4051,46 @@ async function uploadAnonymousSkill(client, params) {
|
|
|
3475
4051
|
});
|
|
3476
4052
|
if (!res.success || !res.data) {
|
|
3477
4053
|
s.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3478
|
-
return;
|
|
4054
|
+
return false;
|
|
4055
|
+
}
|
|
4056
|
+
s.stop("Shared!");
|
|
4057
|
+
R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
|
|
4058
|
+
R2.info(`Install: localskills install ${res.data.publicId}`);
|
|
4059
|
+
return true;
|
|
4060
|
+
}
|
|
4061
|
+
async function uploadAnonymousPackage(client, params) {
|
|
4062
|
+
const s = bt2();
|
|
4063
|
+
let packed;
|
|
4064
|
+
try {
|
|
4065
|
+
packed = packFolder(params.dir, { requireSkillMd: params.type === "skill" });
|
|
4066
|
+
} catch (e2) {
|
|
4067
|
+
R2.error(e2 instanceof PackError ? e2.message : String(e2));
|
|
4068
|
+
return false;
|
|
4069
|
+
}
|
|
4070
|
+
for (const w of packed.warnings) R2.warn(w);
|
|
4071
|
+
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
|
+
const form = new FormData();
|
|
4080
|
+
form.append("name", params.name);
|
|
4081
|
+
form.append("tenantId", tenantId);
|
|
4082
|
+
form.append("visibility", "unlisted");
|
|
4083
|
+
form.append("type", params.type);
|
|
4084
|
+
form.append("file", zipBlob(packed.zip), "skill.zip");
|
|
4085
|
+
const res = await client.postForm("/api/skills", form);
|
|
4086
|
+
if (!res.success || !res.data) {
|
|
4087
|
+
s.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
4088
|
+
return false;
|
|
3479
4089
|
}
|
|
3480
4090
|
s.stop("Shared!");
|
|
3481
4091
|
R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
|
|
3482
4092
|
R2.info(`Install: localskills install ${res.data.publicId}`);
|
|
4093
|
+
return true;
|
|
3483
4094
|
}
|
|
3484
4095
|
|
|
3485
4096
|
// src/commands/profile.ts
|