@localskills/cli 0.3.0 → 0.7.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/dist/index.js +744 -413
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -161,7 +161,7 @@ var require_src = __commonJS({
|
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
// src/index.ts
|
|
164
|
-
import { Command as
|
|
164
|
+
import { Command as Command9 } from "commander";
|
|
165
165
|
|
|
166
166
|
// src/commands/auth.ts
|
|
167
167
|
import { Command } from "commander";
|
|
@@ -1114,7 +1114,8 @@ var DEFAULT_CONFIG = {
|
|
|
1114
1114
|
defaults: {
|
|
1115
1115
|
scope: "project",
|
|
1116
1116
|
method: "symlink"
|
|
1117
|
-
}
|
|
1117
|
+
},
|
|
1118
|
+
anonymous_key: null
|
|
1118
1119
|
};
|
|
1119
1120
|
function loadConfig() {
|
|
1120
1121
|
if (!existsSync(CONFIG_PATH)) {
|
|
@@ -1159,6 +1160,8 @@ function migrateV1toV2(v1) {
|
|
|
1159
1160
|
name: skill.slug,
|
|
1160
1161
|
hash: skill.hash,
|
|
1161
1162
|
version: 0,
|
|
1163
|
+
semver: null,
|
|
1164
|
+
semverRange: null,
|
|
1162
1165
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1163
1166
|
installations: [
|
|
1164
1167
|
{
|
|
@@ -1187,6 +1190,14 @@ function clearToken() {
|
|
|
1187
1190
|
config.token = null;
|
|
1188
1191
|
saveConfig(config);
|
|
1189
1192
|
}
|
|
1193
|
+
function getAnonymousKey() {
|
|
1194
|
+
return loadConfig().anonymous_key ?? null;
|
|
1195
|
+
}
|
|
1196
|
+
function setAnonymousKey(key) {
|
|
1197
|
+
const config = loadConfig();
|
|
1198
|
+
config.anonymous_key = key;
|
|
1199
|
+
saveConfig(config);
|
|
1200
|
+
}
|
|
1190
1201
|
|
|
1191
1202
|
// src/lib/api-client.ts
|
|
1192
1203
|
var ApiClient = class {
|
|
@@ -1229,25 +1240,16 @@ var ApiClient = class {
|
|
|
1229
1240
|
});
|
|
1230
1241
|
return this.handleResponse(res);
|
|
1231
1242
|
}
|
|
1232
|
-
async
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
headers
|
|
1236
|
-
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
method: "DELETE",
|
|
1243
|
-
headers: this.headers()
|
|
1244
|
-
});
|
|
1245
|
-
return this.handleResponse(res);
|
|
1246
|
-
}
|
|
1247
|
-
async getRaw(path) {
|
|
1248
|
-
return fetch(`${this.baseUrl}${path}`, {
|
|
1249
|
-
headers: this.headers()
|
|
1250
|
-
});
|
|
1243
|
+
async fetchBinary(url) {
|
|
1244
|
+
const headers = {};
|
|
1245
|
+
if (this.token) {
|
|
1246
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
1247
|
+
}
|
|
1248
|
+
const res = await fetch(url, { headers });
|
|
1249
|
+
if (!res.ok) {
|
|
1250
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
1251
|
+
}
|
|
1252
|
+
return Buffer.from(await res.arrayBuffer());
|
|
1251
1253
|
}
|
|
1252
1254
|
isAuthenticated() {
|
|
1253
1255
|
return this.token !== null;
|
|
@@ -1285,9 +1287,29 @@ function openBrowser(url) {
|
|
|
1285
1287
|
}
|
|
1286
1288
|
}
|
|
1287
1289
|
function sleep(ms) {
|
|
1288
|
-
return new Promise((
|
|
1290
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1289
1291
|
}
|
|
1290
|
-
var loginCommand = new Command("login").description("Log in to localskills.sh").option("--token <token>", "Use an API token directly (headless mode)").action(async (opts) => {
|
|
1292
|
+
var loginCommand = new Command("login").description("Log in to localskills.sh").option("--token <token>", "Use an API token directly (headless mode)").option("--oidc-token <token>", "Exchange a CI/CD OIDC token for an API token").option("--team <slug>", "Team slug (required with --oidc-token)").action(async (opts) => {
|
|
1293
|
+
if (opts.oidcToken) {
|
|
1294
|
+
if (!opts.team) {
|
|
1295
|
+
console.error("Error: --team is required with --oidc-token");
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
const client2 = new ApiClient();
|
|
1299
|
+
const res = await client2.post(
|
|
1300
|
+
"/api/oidc/token",
|
|
1301
|
+
{ token: opts.oidcToken, team: opts.team }
|
|
1302
|
+
);
|
|
1303
|
+
if (!res.success || !res.data) {
|
|
1304
|
+
console.error(`OIDC login failed: ${res.error || "unknown error"}`);
|
|
1305
|
+
process.exit(1);
|
|
1306
|
+
}
|
|
1307
|
+
setToken(res.data.token);
|
|
1308
|
+
console.log(
|
|
1309
|
+
`Authenticated via OIDC (expires ${new Date(res.data.expiresAt).toISOString()})`
|
|
1310
|
+
);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1291
1313
|
if (opts.token) {
|
|
1292
1314
|
setToken(opts.token);
|
|
1293
1315
|
const client2 = new ApiClient();
|
|
@@ -1382,22 +1404,75 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
|
|
|
1382
1404
|
|
|
1383
1405
|
// src/commands/install.ts
|
|
1384
1406
|
import { Command as Command2 } from "commander";
|
|
1407
|
+
import { mkdirSync as mkdirSync7 } from "fs";
|
|
1408
|
+
|
|
1409
|
+
// ../../packages/shared/dist/utils/semver.js
|
|
1410
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
1411
|
+
var RANGE_RE = /^(\^|~|>=)?\d+\.\d+\.\d+$|^\*$/;
|
|
1412
|
+
function parseSemVer(v) {
|
|
1413
|
+
if (!SEMVER_RE.test(v))
|
|
1414
|
+
return null;
|
|
1415
|
+
const [major, minor, patch] = v.split(".").map(Number);
|
|
1416
|
+
if (major > 999999 || minor > 999999 || patch > 999999)
|
|
1417
|
+
return null;
|
|
1418
|
+
return { major, minor, patch };
|
|
1419
|
+
}
|
|
1420
|
+
function isValidSemVer(v) {
|
|
1421
|
+
return parseSemVer(v) !== null;
|
|
1422
|
+
}
|
|
1423
|
+
function isValidSemVerRange(range) {
|
|
1424
|
+
return RANGE_RE.test(range);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// ../../packages/shared/dist/utils/index.js
|
|
1428
|
+
function titleFromSlug(slug) {
|
|
1429
|
+
return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/lib/cli-helpers.ts
|
|
1433
|
+
function requireAuth(client) {
|
|
1434
|
+
if (!client.isAuthenticated()) {
|
|
1435
|
+
console.error("Not authenticated. Run `localskills login` first.");
|
|
1436
|
+
process.exit(1);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function buildVersionQuery(range) {
|
|
1440
|
+
if (!range) return "";
|
|
1441
|
+
if (isValidSemVer(range)) {
|
|
1442
|
+
return `?semver=${encodeURIComponent(range)}`;
|
|
1443
|
+
}
|
|
1444
|
+
if (isValidSemVerRange(range)) {
|
|
1445
|
+
return `?range=${encodeURIComponent(range)}`;
|
|
1446
|
+
}
|
|
1447
|
+
console.error(`Invalid version specifier: ${range}`);
|
|
1448
|
+
process.exit(1);
|
|
1449
|
+
}
|
|
1450
|
+
function formatVersionLabel(semver, version) {
|
|
1451
|
+
return semver ? `v${semver}` : `v${version}`;
|
|
1452
|
+
}
|
|
1453
|
+
function cancelGuard(value) {
|
|
1454
|
+
if (Ct(value)) {
|
|
1455
|
+
Ne("Cancelled.");
|
|
1456
|
+
process.exit(0);
|
|
1457
|
+
}
|
|
1458
|
+
return value;
|
|
1459
|
+
}
|
|
1385
1460
|
|
|
1386
1461
|
// src/lib/cache.ts
|
|
1387
1462
|
import {
|
|
1388
|
-
existsSync as
|
|
1389
|
-
mkdirSync as
|
|
1463
|
+
existsSync as existsSync13,
|
|
1464
|
+
mkdirSync as mkdirSync5,
|
|
1390
1465
|
readFileSync as readFileSync4,
|
|
1391
1466
|
readdirSync,
|
|
1392
|
-
writeFileSync as
|
|
1393
|
-
rmSync as
|
|
1467
|
+
writeFileSync as writeFileSync5,
|
|
1468
|
+
rmSync as rmSync3
|
|
1394
1469
|
} from "fs";
|
|
1395
|
-
import { join as
|
|
1396
|
-
import { homedir as
|
|
1470
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
1471
|
+
import { homedir as homedir8 } from "os";
|
|
1397
1472
|
|
|
1398
1473
|
// src/lib/installers/cursor.ts
|
|
1399
|
-
import { existsSync as
|
|
1400
|
-
import { join as
|
|
1474
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1475
|
+
import { join as join3 } from "path";
|
|
1401
1476
|
import { homedir as homedir2 } from "os";
|
|
1402
1477
|
|
|
1403
1478
|
// src/lib/content-transform.ts
|
|
@@ -1437,10 +1512,13 @@ function stripFrontmatter(content) {
|
|
|
1437
1512
|
return content;
|
|
1438
1513
|
}
|
|
1439
1514
|
|
|
1515
|
+
// src/lib/installers/common.ts
|
|
1516
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
1517
|
+
import { join as join2 } from "path";
|
|
1518
|
+
|
|
1440
1519
|
// src/lib/symlink.ts
|
|
1441
1520
|
import {
|
|
1442
1521
|
symlinkSync,
|
|
1443
|
-
readlinkSync,
|
|
1444
1522
|
unlinkSync,
|
|
1445
1523
|
lstatSync,
|
|
1446
1524
|
existsSync as existsSync2,
|
|
@@ -1477,6 +1555,31 @@ function isSymlink(path) {
|
|
|
1477
1555
|
}
|
|
1478
1556
|
}
|
|
1479
1557
|
|
|
1558
|
+
// src/lib/installers/common.ts
|
|
1559
|
+
function safeSlugName(slug) {
|
|
1560
|
+
return slug.replace(/\//g, "-");
|
|
1561
|
+
}
|
|
1562
|
+
var DEFAULT_METHOD = "symlink";
|
|
1563
|
+
function installFileOrSymlink(opts, targetPath) {
|
|
1564
|
+
if (opts.method === "symlink") {
|
|
1565
|
+
createSymlink(opts.cachePath, targetPath);
|
|
1566
|
+
} else {
|
|
1567
|
+
mkdirSync3(join2(targetPath, ".."), { recursive: true });
|
|
1568
|
+
writeFileSync2(targetPath, opts.content);
|
|
1569
|
+
}
|
|
1570
|
+
return targetPath;
|
|
1571
|
+
}
|
|
1572
|
+
function uninstallFile(installation) {
|
|
1573
|
+
if (installation.method === "symlink") {
|
|
1574
|
+
removeSymlink(installation.path);
|
|
1575
|
+
} else if (existsSync3(installation.path)) {
|
|
1576
|
+
unlinkSync2(installation.path);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function defaultTransformContent(content) {
|
|
1580
|
+
return toPlainMD(content);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1480
1583
|
// src/lib/installers/cursor.ts
|
|
1481
1584
|
var descriptor = {
|
|
1482
1585
|
id: "cursor",
|
|
@@ -1490,40 +1593,30 @@ function detect(projectDir) {
|
|
|
1490
1593
|
const home = homedir2();
|
|
1491
1594
|
const cwd = projectDir || process.cwd();
|
|
1492
1595
|
return {
|
|
1493
|
-
global:
|
|
1494
|
-
project:
|
|
1596
|
+
global: existsSync4(join3(home, ".cursor")),
|
|
1597
|
+
project: existsSync4(join3(cwd, ".cursor"))
|
|
1495
1598
|
};
|
|
1496
1599
|
}
|
|
1497
1600
|
function resolvePath(slug, scope, projectDir, _contentType) {
|
|
1498
|
-
const safeName = slug
|
|
1601
|
+
const safeName = safeSlugName(slug);
|
|
1499
1602
|
if (scope === "global") {
|
|
1500
|
-
return
|
|
1603
|
+
return join3(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
|
|
1501
1604
|
}
|
|
1502
1605
|
const dir = projectDir || process.cwd();
|
|
1503
|
-
return
|
|
1606
|
+
return join3(dir, ".cursor", "rules", `${safeName}.mdc`);
|
|
1504
1607
|
}
|
|
1505
1608
|
function transformContent(content, skill) {
|
|
1506
1609
|
return toCursorMDC(content, skill);
|
|
1507
1610
|
}
|
|
1508
1611
|
function install(opts) {
|
|
1509
1612
|
const targetPath = resolvePath(opts.slug, opts.scope, opts.projectDir);
|
|
1510
|
-
|
|
1511
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1512
|
-
} else {
|
|
1513
|
-
mkdirSync3(join2(targetPath, ".."), { recursive: true });
|
|
1514
|
-
writeFileSync2(targetPath, opts.content);
|
|
1515
|
-
}
|
|
1516
|
-
return targetPath;
|
|
1613
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1517
1614
|
}
|
|
1518
1615
|
function uninstall(installation, _slug) {
|
|
1519
|
-
|
|
1520
|
-
removeSymlink(installation.path);
|
|
1521
|
-
} else if (existsSync3(installation.path)) {
|
|
1522
|
-
unlinkSync2(installation.path);
|
|
1523
|
-
}
|
|
1616
|
+
uninstallFile(installation);
|
|
1524
1617
|
}
|
|
1525
1618
|
function defaultMethod() {
|
|
1526
|
-
return
|
|
1619
|
+
return DEFAULT_METHOD;
|
|
1527
1620
|
}
|
|
1528
1621
|
var cursorAdapter = {
|
|
1529
1622
|
descriptor,
|
|
@@ -1536,8 +1629,8 @@ var cursorAdapter = {
|
|
|
1536
1629
|
};
|
|
1537
1630
|
|
|
1538
1631
|
// src/lib/installers/claude.ts
|
|
1539
|
-
import { existsSync as
|
|
1540
|
-
import { join as
|
|
1632
|
+
import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
|
|
1633
|
+
import { join as join4 } from "path";
|
|
1541
1634
|
import { homedir as homedir3 } from "os";
|
|
1542
1635
|
var descriptor2 = {
|
|
1543
1636
|
id: "claude",
|
|
@@ -1551,17 +1644,17 @@ function detect2(projectDir) {
|
|
|
1551
1644
|
const home = homedir3();
|
|
1552
1645
|
const cwd = projectDir || process.cwd();
|
|
1553
1646
|
return {
|
|
1554
|
-
global:
|
|
1555
|
-
project:
|
|
1647
|
+
global: existsSync5(join4(home, ".claude")),
|
|
1648
|
+
project: existsSync5(join4(cwd, ".claude"))
|
|
1556
1649
|
};
|
|
1557
1650
|
}
|
|
1558
1651
|
function resolvePath2(slug, scope, projectDir, contentType) {
|
|
1559
|
-
const safeName = slug
|
|
1560
|
-
const base = scope === "global" ?
|
|
1652
|
+
const safeName = safeSlugName(slug);
|
|
1653
|
+
const base = scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
|
|
1561
1654
|
if (contentType === "rule") {
|
|
1562
|
-
return
|
|
1655
|
+
return join4(base, "rules", `${safeName}.md`);
|
|
1563
1656
|
}
|
|
1564
|
-
return
|
|
1657
|
+
return join4(base, "skills", safeName, "SKILL.md");
|
|
1565
1658
|
}
|
|
1566
1659
|
function transformContent2(content, skill) {
|
|
1567
1660
|
if (skill.type === "rule") {
|
|
@@ -1571,32 +1664,21 @@ function transformContent2(content, skill) {
|
|
|
1571
1664
|
}
|
|
1572
1665
|
function install2(opts) {
|
|
1573
1666
|
const targetPath = resolvePath2(opts.slug, opts.scope, opts.projectDir, opts.contentType);
|
|
1574
|
-
|
|
1575
|
-
if (opts.method === "symlink") {
|
|
1576
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1577
|
-
} else {
|
|
1578
|
-
mkdirSync4(targetDir, { recursive: true });
|
|
1579
|
-
writeFileSync3(targetPath, opts.content);
|
|
1580
|
-
}
|
|
1581
|
-
return targetPath;
|
|
1667
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1582
1668
|
}
|
|
1583
1669
|
function uninstall2(installation, _slug) {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
} else if (existsSync4(installation.path)) {
|
|
1587
|
-
unlinkSync3(installation.path);
|
|
1588
|
-
}
|
|
1589
|
-
const parentDir = join3(installation.path, "..");
|
|
1670
|
+
uninstallFile(installation);
|
|
1671
|
+
const parentDir = join4(installation.path, "..");
|
|
1590
1672
|
try {
|
|
1591
1673
|
const { readdirSync: readdirSync3 } = __require("fs");
|
|
1592
|
-
if (
|
|
1593
|
-
|
|
1674
|
+
if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
|
|
1675
|
+
rmSync2(parentDir, { recursive: true });
|
|
1594
1676
|
}
|
|
1595
1677
|
} catch {
|
|
1596
1678
|
}
|
|
1597
1679
|
}
|
|
1598
1680
|
function defaultMethod2() {
|
|
1599
|
-
return
|
|
1681
|
+
return DEFAULT_METHOD;
|
|
1600
1682
|
}
|
|
1601
1683
|
var claudeAdapter = {
|
|
1602
1684
|
descriptor: descriptor2,
|
|
@@ -1609,19 +1691,50 @@ var claudeAdapter = {
|
|
|
1609
1691
|
};
|
|
1610
1692
|
|
|
1611
1693
|
// src/lib/installers/codex.ts
|
|
1612
|
-
import { join as
|
|
1694
|
+
import { join as join6 } from "path";
|
|
1695
|
+
import { homedir as homedir5 } from "os";
|
|
1696
|
+
|
|
1697
|
+
// src/lib/detect.ts
|
|
1698
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1699
|
+
import { execFileSync } from "child_process";
|
|
1613
1700
|
import { homedir as homedir4 } from "os";
|
|
1614
|
-
import {
|
|
1701
|
+
import { join as join5 } from "path";
|
|
1702
|
+
function commandExists(cmd) {
|
|
1703
|
+
try {
|
|
1704
|
+
execFileSync("which", [cmd], { stdio: "ignore" });
|
|
1705
|
+
return true;
|
|
1706
|
+
} catch {
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function detectInstalledPlatforms(projectDir) {
|
|
1711
|
+
const detected = [];
|
|
1712
|
+
const home = homedir4();
|
|
1713
|
+
const cwd = projectDir || process.cwd();
|
|
1714
|
+
if (existsSync6(join5(home, ".cursor")) || existsSync6(join5(cwd, ".cursor")))
|
|
1715
|
+
detected.push("cursor");
|
|
1716
|
+
if (existsSync6(join5(home, ".claude")) || commandExists("claude"))
|
|
1717
|
+
detected.push("claude");
|
|
1718
|
+
if (commandExists("codex")) detected.push("codex");
|
|
1719
|
+
if (existsSync6(join5(home, ".codeium")) || existsSync6(join5(cwd, ".windsurf")))
|
|
1720
|
+
detected.push("windsurf");
|
|
1721
|
+
if (existsSync6(join5(cwd, ".clinerules"))) detected.push("cline");
|
|
1722
|
+
if (existsSync6(join5(cwd, ".github"))) detected.push("copilot");
|
|
1723
|
+
if (commandExists("opencode") || existsSync6(join5(cwd, ".opencode")))
|
|
1724
|
+
detected.push("opencode");
|
|
1725
|
+
if (commandExists("aider")) detected.push("aider");
|
|
1726
|
+
return detected;
|
|
1727
|
+
}
|
|
1615
1728
|
|
|
1616
1729
|
// src/lib/marked-sections.ts
|
|
1617
|
-
import { existsSync as
|
|
1730
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1618
1731
|
import { dirname as dirname2 } from "path";
|
|
1619
1732
|
var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
|
|
1620
1733
|
var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
|
|
1621
1734
|
function upsertSection(filePath, slug, content) {
|
|
1622
|
-
|
|
1735
|
+
mkdirSync4(dirname2(filePath), { recursive: true });
|
|
1623
1736
|
let existing = "";
|
|
1624
|
-
if (
|
|
1737
|
+
if (existsSync7(filePath)) {
|
|
1625
1738
|
existing = readFileSync2(filePath, "utf-8");
|
|
1626
1739
|
}
|
|
1627
1740
|
const start = START_MARKER(slug);
|
|
@@ -1638,10 +1751,10 @@ ${end}`;
|
|
|
1638
1751
|
const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
|
|
1639
1752
|
result = existing + separator + section + "\n";
|
|
1640
1753
|
}
|
|
1641
|
-
|
|
1754
|
+
writeFileSync3(filePath, result);
|
|
1642
1755
|
}
|
|
1643
1756
|
function removeSection(filePath, slug) {
|
|
1644
|
-
if (!
|
|
1757
|
+
if (!existsSync7(filePath)) return false;
|
|
1645
1758
|
const existing = readFileSync2(filePath, "utf-8");
|
|
1646
1759
|
const start = START_MARKER(slug);
|
|
1647
1760
|
const end = END_MARKER(slug);
|
|
@@ -1653,11 +1766,11 @@ function removeSection(filePath, slug) {
|
|
|
1653
1766
|
while (before.endsWith("\n\n")) before = before.slice(0, -1);
|
|
1654
1767
|
while (after.startsWith("\n\n")) after = after.slice(1);
|
|
1655
1768
|
const result = (before + after).trim();
|
|
1656
|
-
|
|
1769
|
+
writeFileSync3(filePath, result ? result + "\n" : "");
|
|
1657
1770
|
return true;
|
|
1658
1771
|
}
|
|
1659
1772
|
function listSections(filePath) {
|
|
1660
|
-
if (!
|
|
1773
|
+
if (!existsSync7(filePath)) return [];
|
|
1661
1774
|
const content = readFileSync2(filePath, "utf-8");
|
|
1662
1775
|
const regex = /<!-- localskills:start:(.+?) -->/g;
|
|
1663
1776
|
const slugs = [];
|
|
@@ -1678,22 +1791,17 @@ var descriptor3 = {
|
|
|
1678
1791
|
fileExtension: ".md"
|
|
1679
1792
|
};
|
|
1680
1793
|
function detect3() {
|
|
1681
|
-
|
|
1682
|
-
try {
|
|
1683
|
-
execSync("which codex", { stdio: "ignore" });
|
|
1684
|
-
hasCommand = true;
|
|
1685
|
-
} catch {
|
|
1686
|
-
}
|
|
1794
|
+
const hasCommand = commandExists("codex");
|
|
1687
1795
|
return { global: hasCommand, project: hasCommand };
|
|
1688
1796
|
}
|
|
1689
1797
|
function resolvePath3(slug, scope, projectDir, _contentType) {
|
|
1690
1798
|
if (scope === "global") {
|
|
1691
|
-
return
|
|
1799
|
+
return join6(homedir5(), ".codex", "AGENTS.md");
|
|
1692
1800
|
}
|
|
1693
|
-
return
|
|
1801
|
+
return join6(projectDir || process.cwd(), "AGENTS.md");
|
|
1694
1802
|
}
|
|
1695
1803
|
function transformContent3(content) {
|
|
1696
|
-
return
|
|
1804
|
+
return defaultTransformContent(content);
|
|
1697
1805
|
}
|
|
1698
1806
|
function install3(opts) {
|
|
1699
1807
|
const filePath = resolvePath3(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1719,9 +1827,9 @@ var codexAdapter = {
|
|
|
1719
1827
|
};
|
|
1720
1828
|
|
|
1721
1829
|
// src/lib/installers/windsurf.ts
|
|
1722
|
-
import { existsSync as
|
|
1723
|
-
import { join as
|
|
1724
|
-
import { homedir as
|
|
1830
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1831
|
+
import { join as join7 } from "path";
|
|
1832
|
+
import { homedir as homedir6 } from "os";
|
|
1725
1833
|
var descriptor4 = {
|
|
1726
1834
|
id: "windsurf",
|
|
1727
1835
|
name: "Windsurf",
|
|
@@ -1732,22 +1840,22 @@ var descriptor4 = {
|
|
|
1732
1840
|
fileExtension: ".md"
|
|
1733
1841
|
};
|
|
1734
1842
|
function detect4(projectDir) {
|
|
1735
|
-
const home =
|
|
1843
|
+
const home = homedir6();
|
|
1736
1844
|
const cwd = projectDir || process.cwd();
|
|
1737
1845
|
return {
|
|
1738
|
-
global:
|
|
1739
|
-
project:
|
|
1846
|
+
global: existsSync8(join7(home, ".codeium")),
|
|
1847
|
+
project: existsSync8(join7(cwd, ".windsurf"))
|
|
1740
1848
|
};
|
|
1741
1849
|
}
|
|
1742
1850
|
function resolvePath4(slug, scope, projectDir, _contentType) {
|
|
1743
|
-
const safeName = slug
|
|
1851
|
+
const safeName = safeSlugName(slug);
|
|
1744
1852
|
if (scope === "global") {
|
|
1745
|
-
return
|
|
1853
|
+
return join7(homedir6(), ".codeium", "windsurf", "memories", "global_rules.md");
|
|
1746
1854
|
}
|
|
1747
|
-
return
|
|
1855
|
+
return join7(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
|
|
1748
1856
|
}
|
|
1749
1857
|
function transformContent4(content) {
|
|
1750
|
-
return
|
|
1858
|
+
return defaultTransformContent(content);
|
|
1751
1859
|
}
|
|
1752
1860
|
function install4(opts) {
|
|
1753
1861
|
const targetPath = resolvePath4(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1755,21 +1863,16 @@ function install4(opts) {
|
|
|
1755
1863
|
upsertSection(targetPath, opts.slug, `## ${opts.slug}
|
|
1756
1864
|
|
|
1757
1865
|
${opts.content}`);
|
|
1758
|
-
} else if (opts.method === "symlink") {
|
|
1759
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1760
1866
|
} else {
|
|
1761
|
-
|
|
1762
|
-
writeFileSync5(targetPath, opts.content);
|
|
1867
|
+
installFileOrSymlink(opts, targetPath);
|
|
1763
1868
|
}
|
|
1764
1869
|
return targetPath;
|
|
1765
1870
|
}
|
|
1766
1871
|
function uninstall4(installation, slug) {
|
|
1767
1872
|
if (installation.method === "section") {
|
|
1768
1873
|
removeSection(installation.path, slug);
|
|
1769
|
-
} else
|
|
1770
|
-
|
|
1771
|
-
} else if (existsSync6(installation.path)) {
|
|
1772
|
-
unlinkSync4(installation.path);
|
|
1874
|
+
} else {
|
|
1875
|
+
uninstallFile(installation);
|
|
1773
1876
|
}
|
|
1774
1877
|
}
|
|
1775
1878
|
function defaultMethod4(scope) {
|
|
@@ -1786,8 +1889,8 @@ var windsurfAdapter = {
|
|
|
1786
1889
|
};
|
|
1787
1890
|
|
|
1788
1891
|
// src/lib/installers/cline.ts
|
|
1789
|
-
import { existsSync as
|
|
1790
|
-
import { join as
|
|
1892
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1893
|
+
import { join as join8 } from "path";
|
|
1791
1894
|
var descriptor5 = {
|
|
1792
1895
|
id: "cline",
|
|
1793
1896
|
name: "Cline",
|
|
@@ -1800,38 +1903,28 @@ function detect5(projectDir) {
|
|
|
1800
1903
|
const cwd = projectDir || process.cwd();
|
|
1801
1904
|
return {
|
|
1802
1905
|
global: false,
|
|
1803
|
-
project:
|
|
1906
|
+
project: existsSync9(join8(cwd, ".clinerules"))
|
|
1804
1907
|
};
|
|
1805
1908
|
}
|
|
1806
1909
|
function resolvePath5(slug, scope, projectDir, _contentType) {
|
|
1807
1910
|
if (scope === "global") {
|
|
1808
1911
|
throw new Error("Cline does not support global installation");
|
|
1809
1912
|
}
|
|
1810
|
-
const safeName = slug
|
|
1811
|
-
return
|
|
1913
|
+
const safeName = safeSlugName(slug);
|
|
1914
|
+
return join8(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
|
|
1812
1915
|
}
|
|
1813
1916
|
function transformContent5(content) {
|
|
1814
|
-
return
|
|
1917
|
+
return defaultTransformContent(content);
|
|
1815
1918
|
}
|
|
1816
1919
|
function install5(opts) {
|
|
1817
1920
|
const targetPath = resolvePath5(opts.slug, opts.scope, opts.projectDir);
|
|
1818
|
-
|
|
1819
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1820
|
-
} else {
|
|
1821
|
-
mkdirSync7(join6(targetPath, ".."), { recursive: true });
|
|
1822
|
-
writeFileSync6(targetPath, opts.content);
|
|
1823
|
-
}
|
|
1824
|
-
return targetPath;
|
|
1921
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1825
1922
|
}
|
|
1826
1923
|
function uninstall5(installation, _slug) {
|
|
1827
|
-
|
|
1828
|
-
removeSymlink(installation.path);
|
|
1829
|
-
} else if (existsSync7(installation.path)) {
|
|
1830
|
-
unlinkSync5(installation.path);
|
|
1831
|
-
}
|
|
1924
|
+
uninstallFile(installation);
|
|
1832
1925
|
}
|
|
1833
1926
|
function defaultMethod5() {
|
|
1834
|
-
return
|
|
1927
|
+
return DEFAULT_METHOD;
|
|
1835
1928
|
}
|
|
1836
1929
|
var clineAdapter = {
|
|
1837
1930
|
descriptor: descriptor5,
|
|
@@ -1844,8 +1937,8 @@ var clineAdapter = {
|
|
|
1844
1937
|
};
|
|
1845
1938
|
|
|
1846
1939
|
// src/lib/installers/copilot.ts
|
|
1847
|
-
import { existsSync as
|
|
1848
|
-
import { join as
|
|
1940
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1941
|
+
import { join as join9 } from "path";
|
|
1849
1942
|
var descriptor6 = {
|
|
1850
1943
|
id: "copilot",
|
|
1851
1944
|
name: "GitHub Copilot",
|
|
@@ -1858,17 +1951,17 @@ function detect6(projectDir) {
|
|
|
1858
1951
|
const cwd = projectDir || process.cwd();
|
|
1859
1952
|
return {
|
|
1860
1953
|
global: false,
|
|
1861
|
-
project:
|
|
1954
|
+
project: existsSync10(join9(cwd, ".github"))
|
|
1862
1955
|
};
|
|
1863
1956
|
}
|
|
1864
1957
|
function resolvePath6(slug, scope, projectDir, _contentType) {
|
|
1865
1958
|
if (scope === "global") {
|
|
1866
1959
|
throw new Error("GitHub Copilot does not support global installation");
|
|
1867
1960
|
}
|
|
1868
|
-
return
|
|
1961
|
+
return join9(projectDir || process.cwd(), ".github", "copilot-instructions.md");
|
|
1869
1962
|
}
|
|
1870
1963
|
function transformContent6(content) {
|
|
1871
|
-
return
|
|
1964
|
+
return defaultTransformContent(content);
|
|
1872
1965
|
}
|
|
1873
1966
|
function install6(opts) {
|
|
1874
1967
|
const filePath = resolvePath6(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1894,10 +1987,9 @@ var copilotAdapter = {
|
|
|
1894
1987
|
};
|
|
1895
1988
|
|
|
1896
1989
|
// src/lib/installers/opencode.ts
|
|
1897
|
-
import { existsSync as
|
|
1898
|
-
import { join as
|
|
1899
|
-
import { homedir as
|
|
1900
|
-
import { execSync as execSync2 } from "child_process";
|
|
1990
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1991
|
+
import { join as join10 } from "path";
|
|
1992
|
+
import { homedir as homedir7 } from "os";
|
|
1901
1993
|
var descriptor7 = {
|
|
1902
1994
|
id: "opencode",
|
|
1903
1995
|
name: "OpenCode",
|
|
@@ -1908,46 +2000,31 @@ var descriptor7 = {
|
|
|
1908
2000
|
};
|
|
1909
2001
|
function detect7(projectDir) {
|
|
1910
2002
|
const cwd = projectDir || process.cwd();
|
|
1911
|
-
|
|
1912
|
-
try {
|
|
1913
|
-
execSync2("which opencode", { stdio: "ignore" });
|
|
1914
|
-
hasCommand = true;
|
|
1915
|
-
} catch {
|
|
1916
|
-
}
|
|
2003
|
+
const hasCommand = commandExists("opencode");
|
|
1917
2004
|
return {
|
|
1918
2005
|
global: hasCommand,
|
|
1919
|
-
project: hasCommand ||
|
|
2006
|
+
project: hasCommand || existsSync11(join10(cwd, ".opencode"))
|
|
1920
2007
|
};
|
|
1921
2008
|
}
|
|
1922
2009
|
function resolvePath7(slug, scope, projectDir, _contentType) {
|
|
1923
|
-
const safeName = slug
|
|
2010
|
+
const safeName = safeSlugName(slug);
|
|
1924
2011
|
if (scope === "global") {
|
|
1925
|
-
return
|
|
2012
|
+
return join10(homedir7(), ".config", "opencode", "rules", `${safeName}.md`);
|
|
1926
2013
|
}
|
|
1927
|
-
return
|
|
2014
|
+
return join10(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
|
|
1928
2015
|
}
|
|
1929
2016
|
function transformContent7(content) {
|
|
1930
|
-
return
|
|
2017
|
+
return defaultTransformContent(content);
|
|
1931
2018
|
}
|
|
1932
2019
|
function install7(opts) {
|
|
1933
2020
|
const targetPath = resolvePath7(opts.slug, opts.scope, opts.projectDir);
|
|
1934
|
-
|
|
1935
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1936
|
-
} else {
|
|
1937
|
-
mkdirSync8(join8(targetPath, ".."), { recursive: true });
|
|
1938
|
-
writeFileSync7(targetPath, opts.content);
|
|
1939
|
-
}
|
|
1940
|
-
return targetPath;
|
|
2021
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1941
2022
|
}
|
|
1942
2023
|
function uninstall7(installation, _slug) {
|
|
1943
|
-
|
|
1944
|
-
removeSymlink(installation.path);
|
|
1945
|
-
} else if (existsSync9(installation.path)) {
|
|
1946
|
-
unlinkSync6(installation.path);
|
|
1947
|
-
}
|
|
2024
|
+
uninstallFile(installation);
|
|
1948
2025
|
}
|
|
1949
2026
|
function defaultMethod7() {
|
|
1950
|
-
return
|
|
2027
|
+
return DEFAULT_METHOD;
|
|
1951
2028
|
}
|
|
1952
2029
|
var opencodeAdapter = {
|
|
1953
2030
|
descriptor: descriptor7,
|
|
@@ -1960,9 +2037,8 @@ var opencodeAdapter = {
|
|
|
1960
2037
|
};
|
|
1961
2038
|
|
|
1962
2039
|
// src/lib/installers/aider.ts
|
|
1963
|
-
import { existsSync as
|
|
1964
|
-
import { join as
|
|
1965
|
-
import { execSync as execSync3 } from "child_process";
|
|
2040
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync4, readFileSync as readFileSync3 } from "fs";
|
|
2041
|
+
import { join as join11 } from "path";
|
|
1966
2042
|
var descriptor8 = {
|
|
1967
2043
|
id: "aider",
|
|
1968
2044
|
name: "Aider",
|
|
@@ -1972,29 +2048,24 @@ var descriptor8 = {
|
|
|
1972
2048
|
fileExtension: ".md"
|
|
1973
2049
|
};
|
|
1974
2050
|
function detect8() {
|
|
1975
|
-
|
|
1976
|
-
try {
|
|
1977
|
-
execSync3("which aider", { stdio: "ignore" });
|
|
1978
|
-
hasCommand = true;
|
|
1979
|
-
} catch {
|
|
1980
|
-
}
|
|
2051
|
+
const hasCommand = commandExists("aider");
|
|
1981
2052
|
return { global: false, project: hasCommand };
|
|
1982
2053
|
}
|
|
1983
2054
|
function resolvePath8(slug, scope, projectDir, contentType) {
|
|
1984
2055
|
if (scope === "global") {
|
|
1985
2056
|
throw new Error("Aider does not support global installation");
|
|
1986
2057
|
}
|
|
1987
|
-
const safeName = slug
|
|
2058
|
+
const safeName = safeSlugName(slug);
|
|
1988
2059
|
const subdir = contentType === "rule" ? "rules" : "skills";
|
|
1989
|
-
return
|
|
2060
|
+
return join11(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
|
|
1990
2061
|
}
|
|
1991
2062
|
function transformContent8(content) {
|
|
1992
|
-
return
|
|
2063
|
+
return defaultTransformContent(content);
|
|
1993
2064
|
}
|
|
1994
2065
|
function addAiderRead(projectDir, relativePath) {
|
|
1995
|
-
const configPath =
|
|
2066
|
+
const configPath = join11(projectDir, ".aider.conf.yml");
|
|
1996
2067
|
let content = "";
|
|
1997
|
-
if (
|
|
2068
|
+
if (existsSync12(configPath)) {
|
|
1998
2069
|
content = readFileSync3(configPath, "utf-8");
|
|
1999
2070
|
}
|
|
2000
2071
|
if (content.includes(relativePath)) return;
|
|
@@ -2004,44 +2075,35 @@ function addAiderRead(projectDir, relativePath) {
|
|
|
2004
2075
|
} else {
|
|
2005
2076
|
content = content.trimEnd() + (content ? "\n" : "") + readLine + "\n";
|
|
2006
2077
|
}
|
|
2007
|
-
|
|
2078
|
+
writeFileSync4(configPath, content);
|
|
2008
2079
|
}
|
|
2009
2080
|
function removeAiderRead(projectDir, relativePath) {
|
|
2010
|
-
const configPath =
|
|
2011
|
-
if (!
|
|
2081
|
+
const configPath = join11(projectDir, ".aider.conf.yml");
|
|
2082
|
+
if (!existsSync12(configPath)) return;
|
|
2012
2083
|
let content = readFileSync3(configPath, "utf-8");
|
|
2013
2084
|
const lines = content.split("\n");
|
|
2014
2085
|
const filtered = lines.filter((line) => !line.includes(relativePath));
|
|
2015
|
-
|
|
2086
|
+
writeFileSync4(configPath, filtered.join("\n"));
|
|
2016
2087
|
}
|
|
2017
2088
|
function install8(opts) {
|
|
2018
2089
|
const targetPath = resolvePath8(opts.slug, opts.scope, opts.projectDir, opts.contentType);
|
|
2019
2090
|
const projectDir = opts.projectDir || process.cwd();
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
} else {
|
|
2023
|
-
mkdirSync9(join9(targetPath, ".."), { recursive: true });
|
|
2024
|
-
writeFileSync8(targetPath, opts.content);
|
|
2025
|
-
}
|
|
2026
|
-
const safeName = opts.slug.replace(/\//g, "-");
|
|
2091
|
+
installFileOrSymlink(opts, targetPath);
|
|
2092
|
+
const safeName = safeSlugName(opts.slug);
|
|
2027
2093
|
const subdir = opts.contentType === "rule" ? "rules" : "skills";
|
|
2028
2094
|
const relativePath = `.aider/${subdir}/${safeName}.md`;
|
|
2029
2095
|
addAiderRead(projectDir, relativePath);
|
|
2030
2096
|
return targetPath;
|
|
2031
2097
|
}
|
|
2032
2098
|
function uninstall8(installation, slug) {
|
|
2033
|
-
|
|
2034
|
-
removeSymlink(installation.path);
|
|
2035
|
-
} else if (existsSync10(installation.path)) {
|
|
2036
|
-
unlinkSync7(installation.path);
|
|
2037
|
-
}
|
|
2099
|
+
uninstallFile(installation);
|
|
2038
2100
|
const projectDir = installation.projectDir || process.cwd();
|
|
2039
|
-
const safeName = slug
|
|
2101
|
+
const safeName = safeSlugName(slug);
|
|
2040
2102
|
removeAiderRead(projectDir, `.aider/skills/${safeName}.md`);
|
|
2041
2103
|
removeAiderRead(projectDir, `.aider/rules/${safeName}.md`);
|
|
2042
2104
|
}
|
|
2043
2105
|
function defaultMethod8() {
|
|
2044
|
-
return
|
|
2106
|
+
return DEFAULT_METHOD;
|
|
2045
2107
|
}
|
|
2046
2108
|
var aiderAdapter = {
|
|
2047
2109
|
descriptor: descriptor8,
|
|
@@ -2074,7 +2136,7 @@ function getAllAdapters() {
|
|
|
2074
2136
|
}
|
|
2075
2137
|
|
|
2076
2138
|
// src/lib/cache.ts
|
|
2077
|
-
var CACHE_DIR =
|
|
2139
|
+
var CACHE_DIR = join12(homedir8(), ".localskills", "cache");
|
|
2078
2140
|
function slugToDir(slug) {
|
|
2079
2141
|
if (slug.includes("..") || slug.includes("\0")) {
|
|
2080
2142
|
throw new Error("Invalid slug: contains forbidden characters");
|
|
@@ -2082,7 +2144,7 @@ function slugToDir(slug) {
|
|
|
2082
2144
|
return slug.replace(/\//g, "--");
|
|
2083
2145
|
}
|
|
2084
2146
|
function getCacheDir(slug) {
|
|
2085
|
-
const dir = resolve2(
|
|
2147
|
+
const dir = resolve2(join12(CACHE_DIR, slugToDir(slug)));
|
|
2086
2148
|
if (!dir.startsWith(resolve2(CACHE_DIR) + "/") && dir !== resolve2(CACHE_DIR)) {
|
|
2087
2149
|
throw new Error("Invalid slug: path traversal detected");
|
|
2088
2150
|
}
|
|
@@ -2090,17 +2152,18 @@ function getCacheDir(slug) {
|
|
|
2090
2152
|
}
|
|
2091
2153
|
function store(slug, content, skill, version) {
|
|
2092
2154
|
const dir = getCacheDir(slug);
|
|
2093
|
-
|
|
2094
|
-
|
|
2155
|
+
mkdirSync5(dir, { recursive: true });
|
|
2156
|
+
writeFileSync5(join12(dir, "raw.md"), content);
|
|
2095
2157
|
const meta = {
|
|
2096
2158
|
hash: skill.contentHash,
|
|
2097
2159
|
version,
|
|
2160
|
+
semver: skill.currentSemver ?? null,
|
|
2098
2161
|
name: skill.name,
|
|
2099
2162
|
description: skill.description,
|
|
2100
2163
|
type: skill.type ?? "skill",
|
|
2101
2164
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2102
2165
|
};
|
|
2103
|
-
|
|
2166
|
+
writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2104
2167
|
clearPlatformFiles(slug);
|
|
2105
2168
|
}
|
|
2106
2169
|
function getPlatformFile(slug, platform, skill) {
|
|
@@ -2111,98 +2174,103 @@ function getPlatformFile(slug, platform, skill) {
|
|
|
2111
2174
|
const transformed = adapter.transformContent(raw, skill);
|
|
2112
2175
|
if (platform === "claude") {
|
|
2113
2176
|
if (skill.type === "rule") {
|
|
2114
|
-
const claudeRuleDir =
|
|
2115
|
-
|
|
2116
|
-
const filePath3 =
|
|
2117
|
-
|
|
2177
|
+
const claudeRuleDir = join12(dir, "claude-rule");
|
|
2178
|
+
mkdirSync5(claudeRuleDir, { recursive: true });
|
|
2179
|
+
const filePath3 = join12(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
|
|
2180
|
+
writeFileSync5(filePath3, transformed);
|
|
2118
2181
|
return filePath3;
|
|
2119
2182
|
}
|
|
2120
|
-
const claudeDir =
|
|
2121
|
-
|
|
2122
|
-
const filePath2 =
|
|
2123
|
-
|
|
2183
|
+
const claudeDir = join12(dir, "claude");
|
|
2184
|
+
mkdirSync5(claudeDir, { recursive: true });
|
|
2185
|
+
const filePath2 = join12(claudeDir, "SKILL.md");
|
|
2186
|
+
writeFileSync5(filePath2, transformed);
|
|
2124
2187
|
return filePath2;
|
|
2125
2188
|
}
|
|
2126
2189
|
const ext = adapter.descriptor.fileExtension;
|
|
2127
|
-
const filePath =
|
|
2128
|
-
|
|
2190
|
+
const filePath = join12(dir, `${platform}${ext}`);
|
|
2191
|
+
writeFileSync5(filePath, transformed);
|
|
2129
2192
|
return filePath;
|
|
2130
2193
|
}
|
|
2131
2194
|
function getRawContent(slug) {
|
|
2132
|
-
const filePath =
|
|
2133
|
-
if (!
|
|
2195
|
+
const filePath = join12(getCacheDir(slug), "raw.md");
|
|
2196
|
+
if (!existsSync13(filePath)) return null;
|
|
2134
2197
|
return readFileSync4(filePath, "utf-8");
|
|
2135
2198
|
}
|
|
2136
2199
|
function purge(slug) {
|
|
2137
2200
|
const dir = getCacheDir(slug);
|
|
2138
|
-
if (
|
|
2139
|
-
|
|
2201
|
+
if (existsSync13(dir)) {
|
|
2202
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
2140
2203
|
}
|
|
2141
2204
|
}
|
|
2205
|
+
function storePackage(slug, zipBuffer, manifest, skill, version) {
|
|
2206
|
+
const dir = getCacheDir(slug);
|
|
2207
|
+
mkdirSync5(dir, { recursive: true });
|
|
2208
|
+
writeFileSync5(join12(dir, "package.zip"), zipBuffer);
|
|
2209
|
+
writeFileSync5(join12(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
2210
|
+
const meta = {
|
|
2211
|
+
hash: skill.contentHash,
|
|
2212
|
+
version,
|
|
2213
|
+
semver: skill.currentSemver ?? null,
|
|
2214
|
+
name: skill.name,
|
|
2215
|
+
description: skill.description,
|
|
2216
|
+
type: skill.type ?? "skill",
|
|
2217
|
+
format: "package",
|
|
2218
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2219
|
+
};
|
|
2220
|
+
writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2221
|
+
}
|
|
2142
2222
|
function clearPlatformFiles(slug) {
|
|
2143
2223
|
const dir = getCacheDir(slug);
|
|
2144
|
-
if (!
|
|
2145
|
-
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json"]);
|
|
2224
|
+
if (!existsSync13(dir)) return;
|
|
2225
|
+
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "package.zip", "manifest.json"]);
|
|
2146
2226
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2147
2227
|
if (!keep.has(entry.name)) {
|
|
2148
|
-
|
|
2228
|
+
rmSync3(join12(dir, entry.name), { recursive: true, force: true });
|
|
2149
2229
|
}
|
|
2150
2230
|
}
|
|
2151
2231
|
}
|
|
2152
2232
|
|
|
2153
|
-
// src/lib/
|
|
2154
|
-
import {
|
|
2155
|
-
import {
|
|
2156
|
-
import {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2233
|
+
// src/lib/extract.ts
|
|
2234
|
+
import { unzipSync } from "fflate";
|
|
2235
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
2236
|
+
import { join as join13, dirname as dirname3, resolve as resolve3 } from "path";
|
|
2237
|
+
function extractPackage(zipBuffer, targetDir) {
|
|
2238
|
+
const resolvedTarget = resolve3(targetDir);
|
|
2239
|
+
const extracted = unzipSync(new Uint8Array(zipBuffer));
|
|
2240
|
+
const writtenFiles = [];
|
|
2241
|
+
for (const [path, data] of Object.entries(extracted)) {
|
|
2242
|
+
if (path.endsWith("/")) continue;
|
|
2243
|
+
if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
const fullPath = resolve3(join13(targetDir, path));
|
|
2247
|
+
if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
mkdirSync6(dirname3(fullPath), { recursive: true });
|
|
2251
|
+
writeFileSync6(fullPath, Buffer.from(data));
|
|
2252
|
+
writtenFiles.push(path);
|
|
2164
2253
|
}
|
|
2165
|
-
|
|
2166
|
-
function detectInstalledPlatforms(projectDir) {
|
|
2167
|
-
const detected = [];
|
|
2168
|
-
const home = homedir8();
|
|
2169
|
-
const cwd = projectDir || process.cwd();
|
|
2170
|
-
if (existsSync12(join11(home, ".cursor")) || existsSync12(join11(cwd, ".cursor")))
|
|
2171
|
-
detected.push("cursor");
|
|
2172
|
-
if (existsSync12(join11(home, ".claude")) || commandExists("claude"))
|
|
2173
|
-
detected.push("claude");
|
|
2174
|
-
if (commandExists("codex")) detected.push("codex");
|
|
2175
|
-
if (existsSync12(join11(home, ".codeium")) || existsSync12(join11(cwd, ".windsurf")))
|
|
2176
|
-
detected.push("windsurf");
|
|
2177
|
-
if (existsSync12(join11(cwd, ".clinerules"))) detected.push("cline");
|
|
2178
|
-
if (existsSync12(join11(cwd, ".github"))) detected.push("copilot");
|
|
2179
|
-
if (commandExists("opencode") || existsSync12(join11(cwd, ".opencode")))
|
|
2180
|
-
detected.push("opencode");
|
|
2181
|
-
if (commandExists("aider")) detected.push("aider");
|
|
2182
|
-
return detected;
|
|
2254
|
+
return writtenFiles;
|
|
2183
2255
|
}
|
|
2184
2256
|
|
|
2185
2257
|
// src/lib/interactive.ts
|
|
2186
2258
|
async function interactiveInstall(availableSkills, detectedPlatforms) {
|
|
2187
2259
|
We("localskills install");
|
|
2188
|
-
const slug = await Je({
|
|
2260
|
+
const slug = cancelGuard(await Je({
|
|
2189
2261
|
message: "Which skill would you like to install?",
|
|
2190
2262
|
options: availableSkills.map((s) => ({
|
|
2191
2263
|
value: s.slug,
|
|
2192
2264
|
label: s.name,
|
|
2193
2265
|
hint: truncate(s.description, 60)
|
|
2194
2266
|
}))
|
|
2195
|
-
});
|
|
2196
|
-
if (Ct(slug)) {
|
|
2197
|
-
Ne("Cancelled.");
|
|
2198
|
-
process.exit(0);
|
|
2199
|
-
}
|
|
2267
|
+
}));
|
|
2200
2268
|
const rest = await interactiveTargets(detectedPlatforms);
|
|
2201
2269
|
return { slug, ...rest };
|
|
2202
2270
|
}
|
|
2203
2271
|
async function interactiveTargets(detectedPlatforms) {
|
|
2204
2272
|
const allAdapters = getAllAdapters();
|
|
2205
|
-
const platforms = await je({
|
|
2273
|
+
const platforms = cancelGuard(await je({
|
|
2206
2274
|
message: "Which platforms should receive this skill?",
|
|
2207
2275
|
options: allAdapters.map((a) => ({
|
|
2208
2276
|
value: a.descriptor.id,
|
|
@@ -2211,12 +2279,8 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2211
2279
|
})),
|
|
2212
2280
|
initialValues: detectedPlatforms.length > 0 ? detectedPlatforms : void 0,
|
|
2213
2281
|
required: true
|
|
2214
|
-
});
|
|
2215
|
-
|
|
2216
|
-
Ne("Cancelled.");
|
|
2217
|
-
process.exit(0);
|
|
2218
|
-
}
|
|
2219
|
-
const scope = await Je({
|
|
2282
|
+
}));
|
|
2283
|
+
const scope = cancelGuard(await Je({
|
|
2220
2284
|
message: "Install scope?",
|
|
2221
2285
|
options: [
|
|
2222
2286
|
{
|
|
@@ -2231,12 +2295,8 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2231
2295
|
}
|
|
2232
2296
|
],
|
|
2233
2297
|
initialValue: "project"
|
|
2234
|
-
});
|
|
2235
|
-
|
|
2236
|
-
Ne("Cancelled.");
|
|
2237
|
-
process.exit(0);
|
|
2238
|
-
}
|
|
2239
|
-
const method = await Je({
|
|
2298
|
+
}));
|
|
2299
|
+
const method = cancelGuard(await Je({
|
|
2240
2300
|
message: "Install method?",
|
|
2241
2301
|
options: [
|
|
2242
2302
|
{
|
|
@@ -2251,31 +2311,18 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2251
2311
|
}
|
|
2252
2312
|
],
|
|
2253
2313
|
initialValue: "symlink"
|
|
2254
|
-
});
|
|
2255
|
-
|
|
2256
|
-
Ne("Cancelled.");
|
|
2257
|
-
process.exit(0);
|
|
2258
|
-
}
|
|
2259
|
-
return {
|
|
2260
|
-
platforms,
|
|
2261
|
-
scope,
|
|
2262
|
-
method
|
|
2263
|
-
};
|
|
2314
|
+
}));
|
|
2315
|
+
return { platforms, scope, method };
|
|
2264
2316
|
}
|
|
2265
2317
|
async function interactiveUninstall(installedSlugs) {
|
|
2266
2318
|
We("localskills uninstall");
|
|
2267
|
-
|
|
2319
|
+
return cancelGuard(await Je({
|
|
2268
2320
|
message: "Which skill would you like to uninstall?",
|
|
2269
2321
|
options: installedSlugs.map((s) => ({
|
|
2270
2322
|
value: s,
|
|
2271
2323
|
label: s
|
|
2272
2324
|
}))
|
|
2273
|
-
});
|
|
2274
|
-
if (Ct(slug)) {
|
|
2275
|
-
Ne("Cancelled.");
|
|
2276
|
-
process.exit(0);
|
|
2277
|
-
}
|
|
2278
|
-
return slug;
|
|
2325
|
+
}));
|
|
2279
2326
|
}
|
|
2280
2327
|
function truncate(str, max) {
|
|
2281
2328
|
if (str.length <= max) return str;
|
|
@@ -2311,6 +2358,19 @@ function parsePlatforms(raw) {
|
|
|
2311
2358
|
}
|
|
2312
2359
|
return platforms;
|
|
2313
2360
|
}
|
|
2361
|
+
function buildSkillRecord(cacheKey, skill, version, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
|
|
2362
|
+
return {
|
|
2363
|
+
slug: cacheKey,
|
|
2364
|
+
name: skill.name,
|
|
2365
|
+
type: skill.type ?? "skill",
|
|
2366
|
+
hash: skill.contentHash,
|
|
2367
|
+
version,
|
|
2368
|
+
semver: resolvedSemver ?? null,
|
|
2369
|
+
semverRange: requestedRange ?? null,
|
|
2370
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2371
|
+
installations: existingInstallations ? [...existingInstallations, ...newInstallations] : newInstallations
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2314
2374
|
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(
|
|
2315
2375
|
async (slugArg, opts) => {
|
|
2316
2376
|
const client = new ApiClient();
|
|
@@ -2363,10 +2423,17 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2363
2423
|
method = explicitMethod || answers.method;
|
|
2364
2424
|
}
|
|
2365
2425
|
}
|
|
2426
|
+
let requestedRange = null;
|
|
2427
|
+
if (slug.includes("@")) {
|
|
2428
|
+
const atIdx = slug.lastIndexOf("@");
|
|
2429
|
+
requestedRange = slug.substring(atIdx + 1);
|
|
2430
|
+
slug = slug.substring(0, atIdx);
|
|
2431
|
+
}
|
|
2432
|
+
const versionQuery = buildVersionQuery(requestedRange);
|
|
2366
2433
|
const spinner = bt2();
|
|
2367
2434
|
spinner.start(`Fetching ${slug}...`);
|
|
2368
2435
|
const res = await client.get(
|
|
2369
|
-
`/api/skills/${encodeURIComponent(slug)}/content`
|
|
2436
|
+
`/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
|
|
2370
2437
|
);
|
|
2371
2438
|
if (!res.success || !res.data) {
|
|
2372
2439
|
spinner.stop("Failed.");
|
|
@@ -2378,9 +2445,62 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2378
2445
|
process.exit(1);
|
|
2379
2446
|
return;
|
|
2380
2447
|
}
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
const cacheKey = skill.publicId || slug;
|
|
2448
|
+
const resData = res.data;
|
|
2449
|
+
const format = resData.format ?? "text";
|
|
2450
|
+
const cacheKey = resData.skill.publicId || slug;
|
|
2451
|
+
if (format === "package") {
|
|
2452
|
+
const { skill: skill2, downloadUrl, manifest, version: version2, semver: resolvedSemver2 } = resData;
|
|
2453
|
+
spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version2)} (package, ${manifest.files.length} files)`);
|
|
2454
|
+
const dlSpinner = bt2();
|
|
2455
|
+
dlSpinner.start("Downloading package...");
|
|
2456
|
+
const zipBuffer = await client.fetchBinary(downloadUrl);
|
|
2457
|
+
dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
|
|
2458
|
+
storePackage(cacheKey, zipBuffer, manifest, skill2, version2);
|
|
2459
|
+
const installations2 = [];
|
|
2460
|
+
const results2 = [];
|
|
2461
|
+
for (const platformId of platforms) {
|
|
2462
|
+
const adapter = getAdapter(platformId);
|
|
2463
|
+
const desc = adapter.descriptor;
|
|
2464
|
+
if (scope === "global" && !desc.supportsGlobal) {
|
|
2465
|
+
R2.warn(`${desc.name} does not support global \u2014 skipping.`);
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (scope === "project" && !desc.supportsProject) {
|
|
2469
|
+
R2.warn(`${desc.name} does not support project \u2014 skipping.`);
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
const targetPath = adapter.resolvePath(cacheKey, scope, projectDir, skill2.type ?? "skill");
|
|
2473
|
+
mkdirSync7(targetPath, { recursive: true });
|
|
2474
|
+
const written = extractPackage(zipBuffer, targetPath);
|
|
2475
|
+
const installation = {
|
|
2476
|
+
platform: platformId,
|
|
2477
|
+
scope,
|
|
2478
|
+
method: "copy",
|
|
2479
|
+
path: targetPath,
|
|
2480
|
+
projectDir,
|
|
2481
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2482
|
+
};
|
|
2483
|
+
installations2.push(installation);
|
|
2484
|
+
results2.push(`${desc.name} \u2192 ${targetPath} (${written.length} files extracted)`);
|
|
2485
|
+
}
|
|
2486
|
+
config.installed_skills[cacheKey] = buildSkillRecord(
|
|
2487
|
+
cacheKey,
|
|
2488
|
+
skill2,
|
|
2489
|
+
version2,
|
|
2490
|
+
resolvedSemver2,
|
|
2491
|
+
requestedRange,
|
|
2492
|
+
config.installed_skills[cacheKey]?.installations,
|
|
2493
|
+
installations2
|
|
2494
|
+
);
|
|
2495
|
+
saveConfig(config);
|
|
2496
|
+
for (const r of results2) {
|
|
2497
|
+
R2.success(r);
|
|
2498
|
+
}
|
|
2499
|
+
Le(`Done! Installed to ${installations2.length} target(s).`);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const { skill, content, version, semver: resolvedSemver } = resData;
|
|
2503
|
+
spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version)}`);
|
|
2384
2504
|
store(cacheKey, content, skill, version);
|
|
2385
2505
|
const contentType = skill.type ?? "skill";
|
|
2386
2506
|
const installations = [];
|
|
@@ -2420,17 +2540,15 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2420
2540
|
const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
|
|
2421
2541
|
results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
|
|
2422
2542
|
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
name: skill.name,
|
|
2427
|
-
type: contentType,
|
|
2428
|
-
hash: skill.contentHash,
|
|
2543
|
+
config.installed_skills[cacheKey] = buildSkillRecord(
|
|
2544
|
+
cacheKey,
|
|
2545
|
+
skill,
|
|
2429
2546
|
version,
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2547
|
+
resolvedSemver,
|
|
2548
|
+
requestedRange,
|
|
2549
|
+
config.installed_skills[cacheKey]?.installations,
|
|
2550
|
+
installations
|
|
2551
|
+
);
|
|
2434
2552
|
saveConfig(config);
|
|
2435
2553
|
for (const r of results) {
|
|
2436
2554
|
R2.success(r);
|
|
@@ -2489,33 +2607,62 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
|
|
|
2489
2607
|
|
|
2490
2608
|
// src/commands/list.ts
|
|
2491
2609
|
import { Command as Command4 } from "commander";
|
|
2492
|
-
var listCommand = new Command4("list").description("List available skills").option("--public", "Show public skills only").action(async (opts) => {
|
|
2610
|
+
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) => {
|
|
2493
2611
|
const client = new ApiClient();
|
|
2494
|
-
if (!
|
|
2495
|
-
console.error("
|
|
2612
|
+
if ((opts.tag || opts.search) && !opts.public) {
|
|
2613
|
+
console.error("The --tag and --search flags require --public.");
|
|
2496
2614
|
process.exit(1);
|
|
2497
2615
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2616
|
+
if (opts.public) {
|
|
2617
|
+
const params = new URLSearchParams();
|
|
2618
|
+
if (opts.tag) params.set("tag", opts.tag);
|
|
2619
|
+
if (opts.search) params.set("q", opts.search);
|
|
2620
|
+
const qs = params.toString();
|
|
2621
|
+
const path = qs ? `/api/explore?${qs}` : "/api/explore";
|
|
2622
|
+
const res = await client.get(path);
|
|
2623
|
+
if (!res.success || !res.data) {
|
|
2624
|
+
console.error(`Error: ${res.error || "Failed to fetch skills"}`);
|
|
2625
|
+
process.exit(1);
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
if (res.data.length === 0) {
|
|
2629
|
+
console.log("No skills found.");
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
console.log("Public skills:\n");
|
|
2633
|
+
for (const skill of res.data) {
|
|
2634
|
+
const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
|
|
2635
|
+
const tags = skill.tags.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
|
|
2636
|
+
console.log(` ${skill.slug} ${ver}${tags} \u2014 ${skill.description || skill.name}`);
|
|
2637
|
+
}
|
|
2638
|
+
console.log(`
|
|
2639
|
+
${res.data.length} skill(s) found.`);
|
|
2640
|
+
} else {
|
|
2641
|
+
requireAuth(client);
|
|
2642
|
+
const res = await client.get("/api/skills");
|
|
2643
|
+
if (!res.success || !res.data) {
|
|
2644
|
+
console.error(`Error: ${res.error || "Failed to fetch skills"}`);
|
|
2645
|
+
process.exit(1);
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
if (res.data.length === 0) {
|
|
2649
|
+
console.log("No skills found.");
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
console.log("Available skills:\n");
|
|
2653
|
+
for (const skill of res.data) {
|
|
2654
|
+
const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
|
|
2655
|
+
const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
|
|
2656
|
+
const tags = skill.tags?.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
|
|
2657
|
+
console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${skill.description || skill.name}`);
|
|
2658
|
+
}
|
|
2659
|
+
console.log(`
|
|
2515
2660
|
${res.data.length} skill(s) found.`);
|
|
2661
|
+
}
|
|
2516
2662
|
});
|
|
2517
2663
|
|
|
2518
2664
|
// src/commands/pull.ts
|
|
2665
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
2519
2666
|
import { Command as Command5 } from "commander";
|
|
2520
2667
|
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) => {
|
|
2521
2668
|
const config = loadConfig();
|
|
@@ -2535,53 +2682,70 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
|
|
|
2535
2682
|
R2.warn(`${slug} \u2014 not found in config, skipping.`);
|
|
2536
2683
|
continue;
|
|
2537
2684
|
}
|
|
2685
|
+
const versionQuery = buildVersionQuery(installed.semverRange);
|
|
2538
2686
|
spinner.start(`Checking ${slug}...`);
|
|
2539
2687
|
const res = await client.get(
|
|
2540
|
-
`/api/skills/${encodeURIComponent(slug)}/content`
|
|
2688
|
+
`/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
|
|
2541
2689
|
);
|
|
2542
2690
|
if (!res.success || !res.data) {
|
|
2543
2691
|
spinner.stop(`${slug} \u2014 failed: ${res.error || "not found"}`);
|
|
2544
2692
|
continue;
|
|
2545
2693
|
}
|
|
2546
|
-
const
|
|
2694
|
+
const resData = res.data;
|
|
2695
|
+
const format = resData.format ?? "text";
|
|
2696
|
+
const { skill, version } = resData;
|
|
2547
2697
|
if (skill.contentHash === installed.hash) {
|
|
2548
2698
|
spinner.stop(`${slug} \u2014 up to date`);
|
|
2549
2699
|
skipped++;
|
|
2550
2700
|
continue;
|
|
2551
2701
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2702
|
+
if (format === "package") {
|
|
2703
|
+
const { downloadUrl, manifest } = resData;
|
|
2704
|
+
const zipBuffer = await client.fetchBinary(downloadUrl);
|
|
2705
|
+
storePackage(slug, zipBuffer, manifest, skill, version);
|
|
2706
|
+
for (const installation of installed.installations) {
|
|
2707
|
+
const adapter = getAdapter(installation.platform);
|
|
2708
|
+
const targetPath = adapter.resolvePath(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
|
|
2709
|
+
mkdirSync8(targetPath, { recursive: true });
|
|
2710
|
+
extractPackage(zipBuffer, targetPath);
|
|
2557
2711
|
}
|
|
2558
|
-
|
|
2559
|
-
const
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
slug,
|
|
2564
|
-
|
|
2712
|
+
} else {
|
|
2713
|
+
const { content } = resData;
|
|
2714
|
+
store(slug, content, skill, version);
|
|
2715
|
+
for (const installation of installed.installations) {
|
|
2716
|
+
if (installation.method === "symlink") {
|
|
2717
|
+
getPlatformFile(slug, installation.platform, skill);
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
const adapter = getAdapter(installation.platform);
|
|
2721
|
+
const transformed = adapter.transformContent(content, skill);
|
|
2722
|
+
if (installation.method === "section") {
|
|
2723
|
+
upsertSection(
|
|
2724
|
+
installation.path,
|
|
2725
|
+
slug,
|
|
2726
|
+
`## ${slug}
|
|
2565
2727
|
|
|
2566
2728
|
${transformed}`
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2729
|
+
);
|
|
2730
|
+
} else {
|
|
2731
|
+
const cachePath = getPlatformFile(slug, installation.platform, skill);
|
|
2732
|
+
adapter.install({
|
|
2733
|
+
slug,
|
|
2734
|
+
content: transformed,
|
|
2735
|
+
scope: installation.scope,
|
|
2736
|
+
method: "copy",
|
|
2737
|
+
cachePath,
|
|
2738
|
+
projectDir: installation.projectDir
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2578
2741
|
}
|
|
2579
2742
|
}
|
|
2580
2743
|
installed.hash = skill.contentHash;
|
|
2581
2744
|
installed.version = version;
|
|
2745
|
+
installed.semver = resData.semver ?? null;
|
|
2582
2746
|
installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2583
2747
|
updated++;
|
|
2584
|
-
spinner.stop(`${slug} \u2014 updated to
|
|
2748
|
+
spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version)}`);
|
|
2585
2749
|
}
|
|
2586
2750
|
saveConfig(config);
|
|
2587
2751
|
Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
|
|
@@ -2589,44 +2753,44 @@ ${transformed}`
|
|
|
2589
2753
|
|
|
2590
2754
|
// src/commands/publish.ts
|
|
2591
2755
|
import { Command as Command6 } from "commander";
|
|
2592
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
2593
|
-
import { resolve as
|
|
2756
|
+
import { readFileSync as readFileSync6, existsSync as existsSync15 } from "fs";
|
|
2757
|
+
import { resolve as resolve4, basename as basename2, extname as extname2 } from "path";
|
|
2594
2758
|
import { homedir as homedir10 } from "os";
|
|
2595
2759
|
|
|
2596
2760
|
// src/lib/scanner.ts
|
|
2597
|
-
import { existsSync as
|
|
2598
|
-
import { join as
|
|
2761
|
+
import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
2762
|
+
import { join as join14, basename, extname } from "path";
|
|
2599
2763
|
import { homedir as homedir9 } from "os";
|
|
2600
|
-
import { readlinkSync
|
|
2764
|
+
import { readlinkSync, lstatSync as lstatSync2 } from "fs";
|
|
2601
2765
|
function scanForSkills(projectDir) {
|
|
2602
2766
|
const home = homedir9();
|
|
2603
2767
|
const cwd = projectDir || process.cwd();
|
|
2604
2768
|
const results = [];
|
|
2605
|
-
scanDirectory(
|
|
2606
|
-
scanDirectory(
|
|
2607
|
-
scanClaudeSkills(
|
|
2608
|
-
scanClaudeSkills(
|
|
2609
|
-
scanDirectory(
|
|
2610
|
-
scanDirectory(
|
|
2611
|
-
scanSingleFile(
|
|
2612
|
-
scanSingleFile(
|
|
2769
|
+
scanDirectory(join14(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
|
|
2770
|
+
scanDirectory(join14(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
|
|
2771
|
+
scanClaudeSkills(join14(home, ".claude", "skills"), "global", results);
|
|
2772
|
+
scanClaudeSkills(join14(cwd, ".claude", "skills"), "project", results);
|
|
2773
|
+
scanDirectory(join14(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
|
|
2774
|
+
scanDirectory(join14(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
|
|
2775
|
+
scanSingleFile(join14(home, ".codex", "AGENTS.md"), "codex", "global", results);
|
|
2776
|
+
scanSingleFile(join14(cwd, "AGENTS.md"), "codex", "project", results);
|
|
2613
2777
|
scanSingleFile(
|
|
2614
|
-
|
|
2778
|
+
join14(home, ".codeium", "windsurf", "memories", "global_rules.md"),
|
|
2615
2779
|
"windsurf",
|
|
2616
2780
|
"global",
|
|
2617
2781
|
results
|
|
2618
2782
|
);
|
|
2619
|
-
scanDirectory(
|
|
2620
|
-
scanDirectory(
|
|
2783
|
+
scanDirectory(join14(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
|
|
2784
|
+
scanDirectory(join14(cwd, ".clinerules"), ".md", "cline", "project", results);
|
|
2621
2785
|
scanSingleFile(
|
|
2622
|
-
|
|
2786
|
+
join14(cwd, ".github", "copilot-instructions.md"),
|
|
2623
2787
|
"copilot",
|
|
2624
2788
|
"project",
|
|
2625
2789
|
results
|
|
2626
2790
|
);
|
|
2627
|
-
scanDirectory(
|
|
2628
|
-
scanDirectory(
|
|
2629
|
-
scanDirectory(
|
|
2791
|
+
scanDirectory(join14(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
|
|
2792
|
+
scanDirectory(join14(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
|
|
2793
|
+
scanDirectory(join14(cwd, ".aider", "skills"), ".md", "aider", "project", results);
|
|
2630
2794
|
return results;
|
|
2631
2795
|
}
|
|
2632
2796
|
function filterTracked(detected, config) {
|
|
@@ -2636,13 +2800,13 @@ function filterTracked(detected, config) {
|
|
|
2636
2800
|
trackedPaths.add(inst.path);
|
|
2637
2801
|
}
|
|
2638
2802
|
}
|
|
2639
|
-
const cacheDir =
|
|
2803
|
+
const cacheDir = join14(homedir9(), ".localskills", "cache");
|
|
2640
2804
|
return detected.filter((skill) => {
|
|
2641
2805
|
if (trackedPaths.has(skill.filePath)) return false;
|
|
2642
2806
|
try {
|
|
2643
2807
|
const stat = lstatSync2(skill.filePath);
|
|
2644
2808
|
if (stat.isSymbolicLink()) {
|
|
2645
|
-
const target =
|
|
2809
|
+
const target = readlinkSync(skill.filePath);
|
|
2646
2810
|
if (target.startsWith(cacheDir)) return false;
|
|
2647
2811
|
}
|
|
2648
2812
|
} catch {
|
|
@@ -2653,11 +2817,9 @@ function filterTracked(detected, config) {
|
|
|
2653
2817
|
function slugFromFilename(filename) {
|
|
2654
2818
|
return basename(filename, extname(filename));
|
|
2655
2819
|
}
|
|
2656
|
-
|
|
2657
|
-
return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2658
|
-
}
|
|
2820
|
+
var nameFromSlug = titleFromSlug;
|
|
2659
2821
|
function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
|
|
2660
|
-
if (!
|
|
2822
|
+
if (!existsSync14(dir)) return;
|
|
2661
2823
|
let entries;
|
|
2662
2824
|
try {
|
|
2663
2825
|
entries = readdirSync2(dir);
|
|
@@ -2666,7 +2828,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
2666
2828
|
}
|
|
2667
2829
|
for (const entry of entries) {
|
|
2668
2830
|
if (!entry.endsWith(ext)) continue;
|
|
2669
|
-
const filePath =
|
|
2831
|
+
const filePath = join14(dir, entry);
|
|
2670
2832
|
try {
|
|
2671
2833
|
const raw = readFileSync5(filePath, "utf-8");
|
|
2672
2834
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -2686,7 +2848,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
2686
2848
|
}
|
|
2687
2849
|
}
|
|
2688
2850
|
function scanClaudeSkills(skillsDir, scope, results) {
|
|
2689
|
-
if (!
|
|
2851
|
+
if (!existsSync14(skillsDir)) return;
|
|
2690
2852
|
let entries;
|
|
2691
2853
|
try {
|
|
2692
2854
|
entries = readdirSync2(skillsDir);
|
|
@@ -2694,8 +2856,8 @@ function scanClaudeSkills(skillsDir, scope, results) {
|
|
|
2694
2856
|
return;
|
|
2695
2857
|
}
|
|
2696
2858
|
for (const entry of entries) {
|
|
2697
|
-
const skillFile =
|
|
2698
|
-
if (!
|
|
2859
|
+
const skillFile = join14(skillsDir, entry, "SKILL.md");
|
|
2860
|
+
if (!existsSync14(skillFile)) continue;
|
|
2699
2861
|
try {
|
|
2700
2862
|
const raw = readFileSync5(skillFile, "utf-8");
|
|
2701
2863
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -2714,7 +2876,7 @@ function scanClaudeSkills(skillsDir, scope, results) {
|
|
|
2714
2876
|
}
|
|
2715
2877
|
}
|
|
2716
2878
|
function scanSingleFile(filePath, platform, scope, results) {
|
|
2717
|
-
if (!
|
|
2879
|
+
if (!existsSync14(filePath)) return;
|
|
2718
2880
|
let raw;
|
|
2719
2881
|
try {
|
|
2720
2882
|
raw = readFileSync5(filePath, "utf-8");
|
|
@@ -2765,10 +2927,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2765
2927
|
).option("--type <type>", "Content type: skill or rule", "skill").option("-m, --message <message>", "Version message").action(
|
|
2766
2928
|
async (fileArg, opts) => {
|
|
2767
2929
|
const client = new ApiClient();
|
|
2768
|
-
|
|
2769
|
-
console.error("Not authenticated. Run `localskills login` first.");
|
|
2770
|
-
process.exit(1);
|
|
2771
|
-
}
|
|
2930
|
+
requireAuth(client);
|
|
2772
2931
|
const teamsRes = await client.get("/api/tenants");
|
|
2773
2932
|
if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
|
|
2774
2933
|
console.error(
|
|
@@ -2779,8 +2938,8 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2779
2938
|
}
|
|
2780
2939
|
const teams = teamsRes.data;
|
|
2781
2940
|
if (fileArg) {
|
|
2782
|
-
const filePath =
|
|
2783
|
-
if (!
|
|
2941
|
+
const filePath = resolve4(fileArg);
|
|
2942
|
+
if (!existsSync15(filePath)) {
|
|
2784
2943
|
console.error(`File not found: ${filePath}`);
|
|
2785
2944
|
process.exit(1);
|
|
2786
2945
|
return;
|
|
@@ -2793,7 +2952,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2793
2952
|
return;
|
|
2794
2953
|
}
|
|
2795
2954
|
const defaultSlug = basename2(filePath, extname2(filePath));
|
|
2796
|
-
const defaultName = defaultSlug
|
|
2955
|
+
const defaultName = titleFromSlug(defaultSlug);
|
|
2797
2956
|
const skillName = opts.name || defaultName;
|
|
2798
2957
|
const contentType = validateContentType(opts.type || "skill");
|
|
2799
2958
|
const visibility = validateVisibility(opts.visibility || "private");
|
|
@@ -2819,7 +2978,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2819
2978
|
Le("Nothing to publish.");
|
|
2820
2979
|
return;
|
|
2821
2980
|
}
|
|
2822
|
-
const
|
|
2981
|
+
const skills = cancelGuard(await je({
|
|
2823
2982
|
message: "Select items to publish",
|
|
2824
2983
|
options: detected.map((s) => ({
|
|
2825
2984
|
value: s,
|
|
@@ -2827,28 +2986,19 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2827
2986
|
hint: `${s.platform}/${s.scope}/${s.contentType} ${shortenPath(s.filePath)}`
|
|
2828
2987
|
})),
|
|
2829
2988
|
required: true
|
|
2830
|
-
});
|
|
2831
|
-
if (Ct(selected)) {
|
|
2832
|
-
Ne("Cancelled.");
|
|
2833
|
-
process.exit(0);
|
|
2834
|
-
}
|
|
2835
|
-
const skills = selected;
|
|
2989
|
+
}));
|
|
2836
2990
|
const tenantId = await resolveTeam(teams, opts.team);
|
|
2837
2991
|
for (const skill of skills) {
|
|
2838
2992
|
R2.step(`Publishing ${skill.suggestedName}...`);
|
|
2839
|
-
const name = await Ze({
|
|
2993
|
+
const name = cancelGuard(await Ze({
|
|
2840
2994
|
message: "Skill name?",
|
|
2841
2995
|
initialValue: skill.suggestedName,
|
|
2842
2996
|
validate: (v) => {
|
|
2843
2997
|
if (!v || v.length < 1) return "Name is required";
|
|
2844
2998
|
if (v.length > 100) return "Name must be 100 characters or less";
|
|
2845
2999
|
}
|
|
2846
|
-
});
|
|
2847
|
-
|
|
2848
|
-
Ne("Cancelled.");
|
|
2849
|
-
process.exit(0);
|
|
2850
|
-
}
|
|
2851
|
-
const visibility = await Je({
|
|
3000
|
+
}));
|
|
3001
|
+
const visibility = cancelGuard(await Je({
|
|
2852
3002
|
message: "Visibility?",
|
|
2853
3003
|
options: [
|
|
2854
3004
|
{ value: "private", label: "Private", hint: "Only team members" },
|
|
@@ -2856,23 +3006,15 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2856
3006
|
{ value: "unlisted", label: "Unlisted", hint: "Accessible via direct link" }
|
|
2857
3007
|
],
|
|
2858
3008
|
initialValue: "private"
|
|
2859
|
-
});
|
|
2860
|
-
|
|
2861
|
-
Ne("Cancelled.");
|
|
2862
|
-
process.exit(0);
|
|
2863
|
-
}
|
|
2864
|
-
const contentType = await Je({
|
|
3009
|
+
}));
|
|
3010
|
+
const contentType = cancelGuard(await Je({
|
|
2865
3011
|
message: "Type?",
|
|
2866
3012
|
options: [
|
|
2867
3013
|
{ value: "skill", label: "Skill", hint: "Reusable agent instructions" },
|
|
2868
3014
|
{ value: "rule", label: "Rule", hint: "Governance constraints" }
|
|
2869
3015
|
],
|
|
2870
3016
|
initialValue: skill.contentType
|
|
2871
|
-
});
|
|
2872
|
-
if (Ct(contentType)) {
|
|
2873
|
-
Ne("Cancelled.");
|
|
2874
|
-
process.exit(0);
|
|
2875
|
-
}
|
|
3017
|
+
}));
|
|
2876
3018
|
await uploadSkill(client, {
|
|
2877
3019
|
name,
|
|
2878
3020
|
content: skill.content,
|
|
@@ -2897,19 +3039,14 @@ async function resolveTeam(teams, teamFlag) {
|
|
|
2897
3039
|
if (teams.length === 1) {
|
|
2898
3040
|
return teams[0].id;
|
|
2899
3041
|
}
|
|
2900
|
-
|
|
3042
|
+
return cancelGuard(await Je({
|
|
2901
3043
|
message: "Which team?",
|
|
2902
3044
|
options: teams.map((t) => ({
|
|
2903
3045
|
value: t.id,
|
|
2904
3046
|
label: t.name,
|
|
2905
3047
|
hint: t.slug
|
|
2906
3048
|
}))
|
|
2907
|
-
});
|
|
2908
|
-
if (Ct(selected)) {
|
|
2909
|
-
Ne("Cancelled.");
|
|
2910
|
-
process.exit(0);
|
|
2911
|
-
}
|
|
2912
|
-
return selected;
|
|
3049
|
+
}));
|
|
2913
3050
|
}
|
|
2914
3051
|
async function uploadSkill(client, params) {
|
|
2915
3052
|
const spinner = bt2();
|
|
@@ -2954,8 +3091,200 @@ function shortenPath(filePath) {
|
|
|
2954
3091
|
return filePath;
|
|
2955
3092
|
}
|
|
2956
3093
|
|
|
3094
|
+
// src/commands/push.ts
|
|
3095
|
+
import { Command as Command7 } from "commander";
|
|
3096
|
+
import { readFileSync as readFileSync7, existsSync as existsSync16 } from "fs";
|
|
3097
|
+
import { resolve as resolve5 } from "path";
|
|
3098
|
+
var pushCommand = new Command7("push").description("Push a new version of an existing skill").argument("<file>", "Path to the skill file").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(
|
|
3099
|
+
async (fileArg, opts) => {
|
|
3100
|
+
const client = new ApiClient();
|
|
3101
|
+
requireAuth(client);
|
|
3102
|
+
const filePath = resolve5(fileArg);
|
|
3103
|
+
if (!existsSync16(filePath)) {
|
|
3104
|
+
console.error(`File not found: ${filePath}`);
|
|
3105
|
+
process.exit(1);
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3109
|
+
const content = stripFrontmatter(raw).trim();
|
|
3110
|
+
if (!content) {
|
|
3111
|
+
console.error("File is empty after stripping frontmatter.");
|
|
3112
|
+
process.exit(1);
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
|
|
3116
|
+
if (opts.version && bumpFlags.length > 0) {
|
|
3117
|
+
console.error("Cannot specify both --version and --patch/--minor/--major");
|
|
3118
|
+
process.exit(1);
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
let body = {
|
|
3122
|
+
content,
|
|
3123
|
+
message: opts.message
|
|
3124
|
+
};
|
|
3125
|
+
if (opts.version) {
|
|
3126
|
+
if (!isValidSemVer(opts.version)) {
|
|
3127
|
+
console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
|
|
3128
|
+
process.exit(1);
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
body.semver = opts.version;
|
|
3132
|
+
} else if (opts.major) {
|
|
3133
|
+
body.bump = "major";
|
|
3134
|
+
} else if (opts.minor) {
|
|
3135
|
+
body.bump = "minor";
|
|
3136
|
+
} else if (opts.patch) {
|
|
3137
|
+
body.bump = "patch";
|
|
3138
|
+
}
|
|
3139
|
+
const spinner = bt2();
|
|
3140
|
+
spinner.start("Pushing new version...");
|
|
3141
|
+
const res = await client.post(
|
|
3142
|
+
`/api/skills/${encodeURIComponent(opts.skill)}/versions`,
|
|
3143
|
+
body
|
|
3144
|
+
);
|
|
3145
|
+
if (!res.success || !res.data) {
|
|
3146
|
+
spinner.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3147
|
+
process.exit(1);
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
const v = res.data;
|
|
3151
|
+
spinner.stop(`Pushed ${formatVersionLabel(v.semver, v.version)}`);
|
|
3152
|
+
Le("Done!");
|
|
3153
|
+
}
|
|
3154
|
+
);
|
|
3155
|
+
|
|
3156
|
+
// src/commands/share.ts
|
|
3157
|
+
import { Command as Command8 } from "commander";
|
|
3158
|
+
import { readFileSync as readFileSync8, existsSync as existsSync17 } from "fs";
|
|
3159
|
+
import { resolve as resolve6, basename as basename3, extname as extname3 } from "path";
|
|
3160
|
+
import { generateKeyPairSync } from "crypto";
|
|
3161
|
+
var shareCommand = new Command8("share").description("Share a skill anonymously (no login required)").argument("[file]", "Path to a specific file to share").option("-n, --name <name>", "Skill name").option("--type <type>", "Content type: skill or rule", "skill").action(async (fileArg, opts) => {
|
|
3162
|
+
We("localskills share");
|
|
3163
|
+
await ensureAnonymousIdentity();
|
|
3164
|
+
const client = new ApiClient();
|
|
3165
|
+
if (fileArg) {
|
|
3166
|
+
const filePath = resolve6(fileArg);
|
|
3167
|
+
if (!existsSync17(filePath)) {
|
|
3168
|
+
R2.error(`File not found: ${filePath}`);
|
|
3169
|
+
process.exit(1);
|
|
3170
|
+
}
|
|
3171
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
3172
|
+
const content = stripFrontmatter(raw).trim();
|
|
3173
|
+
if (!content) {
|
|
3174
|
+
R2.error("File is empty after stripping frontmatter.");
|
|
3175
|
+
process.exit(1);
|
|
3176
|
+
}
|
|
3177
|
+
const defaultSlug = basename3(filePath, extname3(filePath));
|
|
3178
|
+
const defaultName = titleFromSlug(defaultSlug);
|
|
3179
|
+
const skillName = opts.name || defaultName;
|
|
3180
|
+
const contentType = opts.type === "rule" ? "rule" : "skill";
|
|
3181
|
+
await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
|
|
3182
|
+
} else {
|
|
3183
|
+
const spinner = bt2();
|
|
3184
|
+
spinner.start("Scanning for skills...");
|
|
3185
|
+
const config = loadConfig();
|
|
3186
|
+
const allDetected = scanForSkills();
|
|
3187
|
+
const detected = filterTracked(allDetected, config);
|
|
3188
|
+
spinner.stop(
|
|
3189
|
+
detected.length > 0 ? `Found ${detected.length} skill file${detected.length !== 1 ? "s" : ""}.` : "No skill files found."
|
|
3190
|
+
);
|
|
3191
|
+
if (detected.length === 0) {
|
|
3192
|
+
Le("Nothing to share. Pass a file path: localskills share <file>");
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
const selected = cancelGuard(
|
|
3196
|
+
await Je({
|
|
3197
|
+
message: "Select a skill to share",
|
|
3198
|
+
options: detected.map((s) => ({
|
|
3199
|
+
value: s,
|
|
3200
|
+
label: s.suggestedName,
|
|
3201
|
+
hint: `${s.platform} \xB7 ${s.contentType}`
|
|
3202
|
+
}))
|
|
3203
|
+
})
|
|
3204
|
+
);
|
|
3205
|
+
const name = cancelGuard(
|
|
3206
|
+
await Ze({
|
|
3207
|
+
message: "Skill name?",
|
|
3208
|
+
initialValue: selected.suggestedName,
|
|
3209
|
+
validate: (v) => {
|
|
3210
|
+
if (!v || v.length < 1) return "Name is required";
|
|
3211
|
+
if (v.length > 100) return "Name must be 100 characters or less";
|
|
3212
|
+
}
|
|
3213
|
+
})
|
|
3214
|
+
);
|
|
3215
|
+
await uploadAnonymousSkill(client, {
|
|
3216
|
+
name,
|
|
3217
|
+
content: selected.content,
|
|
3218
|
+
type: selected.contentType
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
Le("Done!");
|
|
3222
|
+
});
|
|
3223
|
+
async function ensureAnonymousIdentity() {
|
|
3224
|
+
const config = loadConfig();
|
|
3225
|
+
if (config.token) {
|
|
3226
|
+
const client = new ApiClient();
|
|
3227
|
+
const res2 = await client.get("/api/cli/auth");
|
|
3228
|
+
if (res2.success) return;
|
|
3229
|
+
}
|
|
3230
|
+
let keyPair = getAnonymousKey();
|
|
3231
|
+
if (!keyPair) {
|
|
3232
|
+
const s2 = bt2();
|
|
3233
|
+
s2.start("Generating anonymous identity...");
|
|
3234
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
3235
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
3236
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" }
|
|
3237
|
+
});
|
|
3238
|
+
const rawPubKey = publicKey.subarray(publicKey.length - 32);
|
|
3239
|
+
const rawPrivKey = privateKey.subarray(privateKey.length - 32);
|
|
3240
|
+
keyPair = {
|
|
3241
|
+
publicKey: rawPubKey.toString("base64"),
|
|
3242
|
+
privateKey: rawPrivKey.toString("base64")
|
|
3243
|
+
};
|
|
3244
|
+
setAnonymousKey(keyPair);
|
|
3245
|
+
s2.stop("Identity created.");
|
|
3246
|
+
}
|
|
3247
|
+
const s = bt2();
|
|
3248
|
+
s.start("Connecting to localskills.sh...");
|
|
3249
|
+
const tempClient = new ApiClient();
|
|
3250
|
+
const res = await tempClient.post("/api/cli/auth/anonymous", {
|
|
3251
|
+
publicKey: keyPair.publicKey,
|
|
3252
|
+
algorithm: "Ed25519"
|
|
3253
|
+
});
|
|
3254
|
+
if (!res.success || !res.data) {
|
|
3255
|
+
s.stop(`Registration failed: ${res.error || "Unknown error"}`);
|
|
3256
|
+
process.exit(1);
|
|
3257
|
+
}
|
|
3258
|
+
setToken(res.data.token);
|
|
3259
|
+
s.stop(`Connected as ${res.data.username}`);
|
|
3260
|
+
}
|
|
3261
|
+
async function uploadAnonymousSkill(client, params) {
|
|
3262
|
+
const s = bt2();
|
|
3263
|
+
s.start(`Sharing "${params.name}"...`);
|
|
3264
|
+
const teamsRes = await client.get("/api/tenants");
|
|
3265
|
+
if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
|
|
3266
|
+
s.stop("Failed to find your team. Try running `localskills share` again.");
|
|
3267
|
+
process.exit(1);
|
|
3268
|
+
}
|
|
3269
|
+
const tenantId = teamsRes.data[0].id;
|
|
3270
|
+
const res = await client.post("/api/skills", {
|
|
3271
|
+
name: params.name,
|
|
3272
|
+
content: params.content,
|
|
3273
|
+
tenantId,
|
|
3274
|
+
visibility: "unlisted",
|
|
3275
|
+
type: params.type
|
|
3276
|
+
});
|
|
3277
|
+
if (!res.success || !res.data) {
|
|
3278
|
+
s.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
s.stop("Shared!");
|
|
3282
|
+
R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
|
|
3283
|
+
R2.info(`Install: localskills install ${res.data.publicId}`);
|
|
3284
|
+
}
|
|
3285
|
+
|
|
2957
3286
|
// src/index.ts
|
|
2958
|
-
var program = new
|
|
3287
|
+
var program = new Command9();
|
|
2959
3288
|
program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0");
|
|
2960
3289
|
program.addCommand(loginCommand);
|
|
2961
3290
|
program.addCommand(logoutCommand);
|
|
@@ -2965,4 +3294,6 @@ program.addCommand(uninstallCommand);
|
|
|
2965
3294
|
program.addCommand(listCommand);
|
|
2966
3295
|
program.addCommand(pullCommand);
|
|
2967
3296
|
program.addCommand(publishCommand);
|
|
3297
|
+
program.addCommand(pushCommand);
|
|
3298
|
+
program.addCommand(shareCommand);
|
|
2968
3299
|
program.parse();
|