@localskills/cli 0.2.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 +769 -420
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -161,12 +161,12 @@ 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";
|
|
168
168
|
import { randomBytes, createHash } from "crypto";
|
|
169
|
-
import {
|
|
169
|
+
import { spawn } from "child_process";
|
|
170
170
|
|
|
171
171
|
// ../../node_modules/.pnpm/@clack+core@1.0.1/node_modules/@clack/core/dist/index.mjs
|
|
172
172
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
@@ -1101,7 +1101,7 @@ ${l}
|
|
|
1101
1101
|
} }).prompt();
|
|
1102
1102
|
|
|
1103
1103
|
// src/lib/config.ts
|
|
1104
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1104
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
1105
1105
|
import { join } from "path";
|
|
1106
1106
|
import { homedir } from "os";
|
|
1107
1107
|
var CONFIG_DIR = join(homedir(), ".localskills");
|
|
@@ -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)) {
|
|
@@ -1131,8 +1132,15 @@ function loadConfig() {
|
|
|
1131
1132
|
}
|
|
1132
1133
|
}
|
|
1133
1134
|
function saveConfig(config) {
|
|
1134
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1135
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n"
|
|
1135
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1136
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
|
|
1137
|
+
mode: 384
|
|
1138
|
+
});
|
|
1139
|
+
try {
|
|
1140
|
+
chmodSync(CONFIG_DIR, 448);
|
|
1141
|
+
chmodSync(CONFIG_PATH, 384);
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1136
1144
|
}
|
|
1137
1145
|
function migrateV1toV2(v1) {
|
|
1138
1146
|
const v2 = {
|
|
@@ -1152,6 +1160,8 @@ function migrateV1toV2(v1) {
|
|
|
1152
1160
|
name: skill.slug,
|
|
1153
1161
|
hash: skill.hash,
|
|
1154
1162
|
version: 0,
|
|
1163
|
+
semver: null,
|
|
1164
|
+
semverRange: null,
|
|
1155
1165
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1156
1166
|
installations: [
|
|
1157
1167
|
{
|
|
@@ -1180,6 +1190,14 @@ function clearToken() {
|
|
|
1180
1190
|
config.token = null;
|
|
1181
1191
|
saveConfig(config);
|
|
1182
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
|
+
}
|
|
1183
1201
|
|
|
1184
1202
|
// src/lib/api-client.ts
|
|
1185
1203
|
var ApiClient = class {
|
|
@@ -1222,25 +1240,16 @@ var ApiClient = class {
|
|
|
1222
1240
|
});
|
|
1223
1241
|
return this.handleResponse(res);
|
|
1224
1242
|
}
|
|
1225
|
-
async
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
headers
|
|
1229
|
-
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
method: "DELETE",
|
|
1236
|
-
headers: this.headers()
|
|
1237
|
-
});
|
|
1238
|
-
return this.handleResponse(res);
|
|
1239
|
-
}
|
|
1240
|
-
async getRaw(path) {
|
|
1241
|
-
return fetch(`${this.baseUrl}${path}`, {
|
|
1242
|
-
headers: this.headers()
|
|
1243
|
-
});
|
|
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());
|
|
1244
1253
|
}
|
|
1245
1254
|
isAuthenticated() {
|
|
1246
1255
|
return this.token !== null;
|
|
@@ -1256,20 +1265,51 @@ function generateUserCode(length = 8) {
|
|
|
1256
1265
|
function openBrowser(url) {
|
|
1257
1266
|
try {
|
|
1258
1267
|
const platform = process.platform;
|
|
1268
|
+
let command = "";
|
|
1269
|
+
let args = [];
|
|
1259
1270
|
if (platform === "darwin") {
|
|
1260
|
-
|
|
1271
|
+
command = "open";
|
|
1272
|
+
args = [url];
|
|
1261
1273
|
} else if (platform === "win32") {
|
|
1262
|
-
|
|
1274
|
+
command = "rundll32";
|
|
1275
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
1263
1276
|
} else {
|
|
1264
|
-
|
|
1277
|
+
command = "xdg-open";
|
|
1278
|
+
args = [url];
|
|
1265
1279
|
}
|
|
1280
|
+
const child = spawn(command, args, {
|
|
1281
|
+
stdio: "ignore",
|
|
1282
|
+
detached: true,
|
|
1283
|
+
shell: false
|
|
1284
|
+
});
|
|
1285
|
+
child.unref();
|
|
1266
1286
|
} catch {
|
|
1267
1287
|
}
|
|
1268
1288
|
}
|
|
1269
1289
|
function sleep(ms) {
|
|
1270
|
-
return new Promise((
|
|
1290
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1271
1291
|
}
|
|
1272
|
-
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
|
+
}
|
|
1273
1313
|
if (opts.token) {
|
|
1274
1314
|
setToken(opts.token);
|
|
1275
1315
|
const client2 = new ApiClient();
|
|
@@ -1364,22 +1404,75 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
|
|
|
1364
1404
|
|
|
1365
1405
|
// src/commands/install.ts
|
|
1366
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
|
+
}
|
|
1367
1460
|
|
|
1368
1461
|
// src/lib/cache.ts
|
|
1369
1462
|
import {
|
|
1370
|
-
existsSync as
|
|
1371
|
-
mkdirSync as
|
|
1463
|
+
existsSync as existsSync13,
|
|
1464
|
+
mkdirSync as mkdirSync5,
|
|
1372
1465
|
readFileSync as readFileSync4,
|
|
1373
1466
|
readdirSync,
|
|
1374
|
-
writeFileSync as
|
|
1375
|
-
rmSync as
|
|
1467
|
+
writeFileSync as writeFileSync5,
|
|
1468
|
+
rmSync as rmSync3
|
|
1376
1469
|
} from "fs";
|
|
1377
|
-
import { join as
|
|
1378
|
-
import { homedir as
|
|
1470
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
1471
|
+
import { homedir as homedir8 } from "os";
|
|
1379
1472
|
|
|
1380
1473
|
// src/lib/installers/cursor.ts
|
|
1381
|
-
import { existsSync as
|
|
1382
|
-
import { join as
|
|
1474
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1475
|
+
import { join as join3 } from "path";
|
|
1383
1476
|
import { homedir as homedir2 } from "os";
|
|
1384
1477
|
|
|
1385
1478
|
// src/lib/content-transform.ts
|
|
@@ -1419,10 +1512,13 @@ function stripFrontmatter(content) {
|
|
|
1419
1512
|
return content;
|
|
1420
1513
|
}
|
|
1421
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
|
+
|
|
1422
1519
|
// src/lib/symlink.ts
|
|
1423
1520
|
import {
|
|
1424
1521
|
symlinkSync,
|
|
1425
|
-
readlinkSync,
|
|
1426
1522
|
unlinkSync,
|
|
1427
1523
|
lstatSync,
|
|
1428
1524
|
existsSync as existsSync2,
|
|
@@ -1459,6 +1555,31 @@ function isSymlink(path) {
|
|
|
1459
1555
|
}
|
|
1460
1556
|
}
|
|
1461
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
|
+
|
|
1462
1583
|
// src/lib/installers/cursor.ts
|
|
1463
1584
|
var descriptor = {
|
|
1464
1585
|
id: "cursor",
|
|
@@ -1472,40 +1593,30 @@ function detect(projectDir) {
|
|
|
1472
1593
|
const home = homedir2();
|
|
1473
1594
|
const cwd = projectDir || process.cwd();
|
|
1474
1595
|
return {
|
|
1475
|
-
global:
|
|
1476
|
-
project:
|
|
1596
|
+
global: existsSync4(join3(home, ".cursor")),
|
|
1597
|
+
project: existsSync4(join3(cwd, ".cursor"))
|
|
1477
1598
|
};
|
|
1478
1599
|
}
|
|
1479
1600
|
function resolvePath(slug, scope, projectDir, _contentType) {
|
|
1480
|
-
const safeName = slug
|
|
1601
|
+
const safeName = safeSlugName(slug);
|
|
1481
1602
|
if (scope === "global") {
|
|
1482
|
-
return
|
|
1603
|
+
return join3(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
|
|
1483
1604
|
}
|
|
1484
1605
|
const dir = projectDir || process.cwd();
|
|
1485
|
-
return
|
|
1606
|
+
return join3(dir, ".cursor", "rules", `${safeName}.mdc`);
|
|
1486
1607
|
}
|
|
1487
1608
|
function transformContent(content, skill) {
|
|
1488
1609
|
return toCursorMDC(content, skill);
|
|
1489
1610
|
}
|
|
1490
1611
|
function install(opts) {
|
|
1491
1612
|
const targetPath = resolvePath(opts.slug, opts.scope, opts.projectDir);
|
|
1492
|
-
|
|
1493
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1494
|
-
} else {
|
|
1495
|
-
mkdirSync3(join2(targetPath, ".."), { recursive: true });
|
|
1496
|
-
writeFileSync2(targetPath, opts.content);
|
|
1497
|
-
}
|
|
1498
|
-
return targetPath;
|
|
1613
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1499
1614
|
}
|
|
1500
1615
|
function uninstall(installation, _slug) {
|
|
1501
|
-
|
|
1502
|
-
removeSymlink(installation.path);
|
|
1503
|
-
} else if (existsSync3(installation.path)) {
|
|
1504
|
-
unlinkSync2(installation.path);
|
|
1505
|
-
}
|
|
1616
|
+
uninstallFile(installation);
|
|
1506
1617
|
}
|
|
1507
1618
|
function defaultMethod() {
|
|
1508
|
-
return
|
|
1619
|
+
return DEFAULT_METHOD;
|
|
1509
1620
|
}
|
|
1510
1621
|
var cursorAdapter = {
|
|
1511
1622
|
descriptor,
|
|
@@ -1518,8 +1629,8 @@ var cursorAdapter = {
|
|
|
1518
1629
|
};
|
|
1519
1630
|
|
|
1520
1631
|
// src/lib/installers/claude.ts
|
|
1521
|
-
import { existsSync as
|
|
1522
|
-
import { join as
|
|
1632
|
+
import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
|
|
1633
|
+
import { join as join4 } from "path";
|
|
1523
1634
|
import { homedir as homedir3 } from "os";
|
|
1524
1635
|
var descriptor2 = {
|
|
1525
1636
|
id: "claude",
|
|
@@ -1533,17 +1644,17 @@ function detect2(projectDir) {
|
|
|
1533
1644
|
const home = homedir3();
|
|
1534
1645
|
const cwd = projectDir || process.cwd();
|
|
1535
1646
|
return {
|
|
1536
|
-
global:
|
|
1537
|
-
project:
|
|
1647
|
+
global: existsSync5(join4(home, ".claude")),
|
|
1648
|
+
project: existsSync5(join4(cwd, ".claude"))
|
|
1538
1649
|
};
|
|
1539
1650
|
}
|
|
1540
1651
|
function resolvePath2(slug, scope, projectDir, contentType) {
|
|
1541
|
-
const safeName = slug
|
|
1542
|
-
const base = scope === "global" ?
|
|
1652
|
+
const safeName = safeSlugName(slug);
|
|
1653
|
+
const base = scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
|
|
1543
1654
|
if (contentType === "rule") {
|
|
1544
|
-
return
|
|
1655
|
+
return join4(base, "rules", `${safeName}.md`);
|
|
1545
1656
|
}
|
|
1546
|
-
return
|
|
1657
|
+
return join4(base, "skills", safeName, "SKILL.md");
|
|
1547
1658
|
}
|
|
1548
1659
|
function transformContent2(content, skill) {
|
|
1549
1660
|
if (skill.type === "rule") {
|
|
@@ -1553,32 +1664,21 @@ function transformContent2(content, skill) {
|
|
|
1553
1664
|
}
|
|
1554
1665
|
function install2(opts) {
|
|
1555
1666
|
const targetPath = resolvePath2(opts.slug, opts.scope, opts.projectDir, opts.contentType);
|
|
1556
|
-
|
|
1557
|
-
if (opts.method === "symlink") {
|
|
1558
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1559
|
-
} else {
|
|
1560
|
-
mkdirSync4(targetDir, { recursive: true });
|
|
1561
|
-
writeFileSync3(targetPath, opts.content);
|
|
1562
|
-
}
|
|
1563
|
-
return targetPath;
|
|
1667
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1564
1668
|
}
|
|
1565
1669
|
function uninstall2(installation, _slug) {
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
} else if (existsSync4(installation.path)) {
|
|
1569
|
-
unlinkSync3(installation.path);
|
|
1570
|
-
}
|
|
1571
|
-
const parentDir = join3(installation.path, "..");
|
|
1670
|
+
uninstallFile(installation);
|
|
1671
|
+
const parentDir = join4(installation.path, "..");
|
|
1572
1672
|
try {
|
|
1573
1673
|
const { readdirSync: readdirSync3 } = __require("fs");
|
|
1574
|
-
if (
|
|
1575
|
-
|
|
1674
|
+
if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
|
|
1675
|
+
rmSync2(parentDir, { recursive: true });
|
|
1576
1676
|
}
|
|
1577
1677
|
} catch {
|
|
1578
1678
|
}
|
|
1579
1679
|
}
|
|
1580
1680
|
function defaultMethod2() {
|
|
1581
|
-
return
|
|
1681
|
+
return DEFAULT_METHOD;
|
|
1582
1682
|
}
|
|
1583
1683
|
var claudeAdapter = {
|
|
1584
1684
|
descriptor: descriptor2,
|
|
@@ -1591,19 +1691,50 @@ var claudeAdapter = {
|
|
|
1591
1691
|
};
|
|
1592
1692
|
|
|
1593
1693
|
// src/lib/installers/codex.ts
|
|
1594
|
-
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";
|
|
1595
1700
|
import { homedir as homedir4 } from "os";
|
|
1596
|
-
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
|
+
}
|
|
1597
1728
|
|
|
1598
1729
|
// src/lib/marked-sections.ts
|
|
1599
|
-
import { existsSync as
|
|
1730
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1600
1731
|
import { dirname as dirname2 } from "path";
|
|
1601
1732
|
var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
|
|
1602
1733
|
var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
|
|
1603
1734
|
function upsertSection(filePath, slug, content) {
|
|
1604
|
-
|
|
1735
|
+
mkdirSync4(dirname2(filePath), { recursive: true });
|
|
1605
1736
|
let existing = "";
|
|
1606
|
-
if (
|
|
1737
|
+
if (existsSync7(filePath)) {
|
|
1607
1738
|
existing = readFileSync2(filePath, "utf-8");
|
|
1608
1739
|
}
|
|
1609
1740
|
const start = START_MARKER(slug);
|
|
@@ -1620,10 +1751,10 @@ ${end}`;
|
|
|
1620
1751
|
const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
|
|
1621
1752
|
result = existing + separator + section + "\n";
|
|
1622
1753
|
}
|
|
1623
|
-
|
|
1754
|
+
writeFileSync3(filePath, result);
|
|
1624
1755
|
}
|
|
1625
1756
|
function removeSection(filePath, slug) {
|
|
1626
|
-
if (!
|
|
1757
|
+
if (!existsSync7(filePath)) return false;
|
|
1627
1758
|
const existing = readFileSync2(filePath, "utf-8");
|
|
1628
1759
|
const start = START_MARKER(slug);
|
|
1629
1760
|
const end = END_MARKER(slug);
|
|
@@ -1635,11 +1766,11 @@ function removeSection(filePath, slug) {
|
|
|
1635
1766
|
while (before.endsWith("\n\n")) before = before.slice(0, -1);
|
|
1636
1767
|
while (after.startsWith("\n\n")) after = after.slice(1);
|
|
1637
1768
|
const result = (before + after).trim();
|
|
1638
|
-
|
|
1769
|
+
writeFileSync3(filePath, result ? result + "\n" : "");
|
|
1639
1770
|
return true;
|
|
1640
1771
|
}
|
|
1641
1772
|
function listSections(filePath) {
|
|
1642
|
-
if (!
|
|
1773
|
+
if (!existsSync7(filePath)) return [];
|
|
1643
1774
|
const content = readFileSync2(filePath, "utf-8");
|
|
1644
1775
|
const regex = /<!-- localskills:start:(.+?) -->/g;
|
|
1645
1776
|
const slugs = [];
|
|
@@ -1660,22 +1791,17 @@ var descriptor3 = {
|
|
|
1660
1791
|
fileExtension: ".md"
|
|
1661
1792
|
};
|
|
1662
1793
|
function detect3() {
|
|
1663
|
-
|
|
1664
|
-
try {
|
|
1665
|
-
execSync2("which codex", { stdio: "ignore" });
|
|
1666
|
-
hasCommand = true;
|
|
1667
|
-
} catch {
|
|
1668
|
-
}
|
|
1794
|
+
const hasCommand = commandExists("codex");
|
|
1669
1795
|
return { global: hasCommand, project: hasCommand };
|
|
1670
1796
|
}
|
|
1671
1797
|
function resolvePath3(slug, scope, projectDir, _contentType) {
|
|
1672
1798
|
if (scope === "global") {
|
|
1673
|
-
return
|
|
1799
|
+
return join6(homedir5(), ".codex", "AGENTS.md");
|
|
1674
1800
|
}
|
|
1675
|
-
return
|
|
1801
|
+
return join6(projectDir || process.cwd(), "AGENTS.md");
|
|
1676
1802
|
}
|
|
1677
1803
|
function transformContent3(content) {
|
|
1678
|
-
return
|
|
1804
|
+
return defaultTransformContent(content);
|
|
1679
1805
|
}
|
|
1680
1806
|
function install3(opts) {
|
|
1681
1807
|
const filePath = resolvePath3(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1701,9 +1827,9 @@ var codexAdapter = {
|
|
|
1701
1827
|
};
|
|
1702
1828
|
|
|
1703
1829
|
// src/lib/installers/windsurf.ts
|
|
1704
|
-
import { existsSync as
|
|
1705
|
-
import { join as
|
|
1706
|
-
import { homedir as
|
|
1830
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1831
|
+
import { join as join7 } from "path";
|
|
1832
|
+
import { homedir as homedir6 } from "os";
|
|
1707
1833
|
var descriptor4 = {
|
|
1708
1834
|
id: "windsurf",
|
|
1709
1835
|
name: "Windsurf",
|
|
@@ -1714,22 +1840,22 @@ var descriptor4 = {
|
|
|
1714
1840
|
fileExtension: ".md"
|
|
1715
1841
|
};
|
|
1716
1842
|
function detect4(projectDir) {
|
|
1717
|
-
const home =
|
|
1843
|
+
const home = homedir6();
|
|
1718
1844
|
const cwd = projectDir || process.cwd();
|
|
1719
1845
|
return {
|
|
1720
|
-
global:
|
|
1721
|
-
project:
|
|
1846
|
+
global: existsSync8(join7(home, ".codeium")),
|
|
1847
|
+
project: existsSync8(join7(cwd, ".windsurf"))
|
|
1722
1848
|
};
|
|
1723
1849
|
}
|
|
1724
1850
|
function resolvePath4(slug, scope, projectDir, _contentType) {
|
|
1725
|
-
const safeName = slug
|
|
1851
|
+
const safeName = safeSlugName(slug);
|
|
1726
1852
|
if (scope === "global") {
|
|
1727
|
-
return
|
|
1853
|
+
return join7(homedir6(), ".codeium", "windsurf", "memories", "global_rules.md");
|
|
1728
1854
|
}
|
|
1729
|
-
return
|
|
1855
|
+
return join7(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
|
|
1730
1856
|
}
|
|
1731
1857
|
function transformContent4(content) {
|
|
1732
|
-
return
|
|
1858
|
+
return defaultTransformContent(content);
|
|
1733
1859
|
}
|
|
1734
1860
|
function install4(opts) {
|
|
1735
1861
|
const targetPath = resolvePath4(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1737,21 +1863,16 @@ function install4(opts) {
|
|
|
1737
1863
|
upsertSection(targetPath, opts.slug, `## ${opts.slug}
|
|
1738
1864
|
|
|
1739
1865
|
${opts.content}`);
|
|
1740
|
-
} else if (opts.method === "symlink") {
|
|
1741
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1742
1866
|
} else {
|
|
1743
|
-
|
|
1744
|
-
writeFileSync5(targetPath, opts.content);
|
|
1867
|
+
installFileOrSymlink(opts, targetPath);
|
|
1745
1868
|
}
|
|
1746
1869
|
return targetPath;
|
|
1747
1870
|
}
|
|
1748
1871
|
function uninstall4(installation, slug) {
|
|
1749
1872
|
if (installation.method === "section") {
|
|
1750
1873
|
removeSection(installation.path, slug);
|
|
1751
|
-
} else
|
|
1752
|
-
|
|
1753
|
-
} else if (existsSync6(installation.path)) {
|
|
1754
|
-
unlinkSync4(installation.path);
|
|
1874
|
+
} else {
|
|
1875
|
+
uninstallFile(installation);
|
|
1755
1876
|
}
|
|
1756
1877
|
}
|
|
1757
1878
|
function defaultMethod4(scope) {
|
|
@@ -1768,8 +1889,8 @@ var windsurfAdapter = {
|
|
|
1768
1889
|
};
|
|
1769
1890
|
|
|
1770
1891
|
// src/lib/installers/cline.ts
|
|
1771
|
-
import { existsSync as
|
|
1772
|
-
import { join as
|
|
1892
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1893
|
+
import { join as join8 } from "path";
|
|
1773
1894
|
var descriptor5 = {
|
|
1774
1895
|
id: "cline",
|
|
1775
1896
|
name: "Cline",
|
|
@@ -1782,38 +1903,28 @@ function detect5(projectDir) {
|
|
|
1782
1903
|
const cwd = projectDir || process.cwd();
|
|
1783
1904
|
return {
|
|
1784
1905
|
global: false,
|
|
1785
|
-
project:
|
|
1906
|
+
project: existsSync9(join8(cwd, ".clinerules"))
|
|
1786
1907
|
};
|
|
1787
1908
|
}
|
|
1788
1909
|
function resolvePath5(slug, scope, projectDir, _contentType) {
|
|
1789
1910
|
if (scope === "global") {
|
|
1790
1911
|
throw new Error("Cline does not support global installation");
|
|
1791
1912
|
}
|
|
1792
|
-
const safeName = slug
|
|
1793
|
-
return
|
|
1913
|
+
const safeName = safeSlugName(slug);
|
|
1914
|
+
return join8(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
|
|
1794
1915
|
}
|
|
1795
1916
|
function transformContent5(content) {
|
|
1796
|
-
return
|
|
1917
|
+
return defaultTransformContent(content);
|
|
1797
1918
|
}
|
|
1798
1919
|
function install5(opts) {
|
|
1799
1920
|
const targetPath = resolvePath5(opts.slug, opts.scope, opts.projectDir);
|
|
1800
|
-
|
|
1801
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1802
|
-
} else {
|
|
1803
|
-
mkdirSync7(join6(targetPath, ".."), { recursive: true });
|
|
1804
|
-
writeFileSync6(targetPath, opts.content);
|
|
1805
|
-
}
|
|
1806
|
-
return targetPath;
|
|
1921
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1807
1922
|
}
|
|
1808
1923
|
function uninstall5(installation, _slug) {
|
|
1809
|
-
|
|
1810
|
-
removeSymlink(installation.path);
|
|
1811
|
-
} else if (existsSync7(installation.path)) {
|
|
1812
|
-
unlinkSync5(installation.path);
|
|
1813
|
-
}
|
|
1924
|
+
uninstallFile(installation);
|
|
1814
1925
|
}
|
|
1815
1926
|
function defaultMethod5() {
|
|
1816
|
-
return
|
|
1927
|
+
return DEFAULT_METHOD;
|
|
1817
1928
|
}
|
|
1818
1929
|
var clineAdapter = {
|
|
1819
1930
|
descriptor: descriptor5,
|
|
@@ -1826,8 +1937,8 @@ var clineAdapter = {
|
|
|
1826
1937
|
};
|
|
1827
1938
|
|
|
1828
1939
|
// src/lib/installers/copilot.ts
|
|
1829
|
-
import { existsSync as
|
|
1830
|
-
import { join as
|
|
1940
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1941
|
+
import { join as join9 } from "path";
|
|
1831
1942
|
var descriptor6 = {
|
|
1832
1943
|
id: "copilot",
|
|
1833
1944
|
name: "GitHub Copilot",
|
|
@@ -1840,17 +1951,17 @@ function detect6(projectDir) {
|
|
|
1840
1951
|
const cwd = projectDir || process.cwd();
|
|
1841
1952
|
return {
|
|
1842
1953
|
global: false,
|
|
1843
|
-
project:
|
|
1954
|
+
project: existsSync10(join9(cwd, ".github"))
|
|
1844
1955
|
};
|
|
1845
1956
|
}
|
|
1846
1957
|
function resolvePath6(slug, scope, projectDir, _contentType) {
|
|
1847
1958
|
if (scope === "global") {
|
|
1848
1959
|
throw new Error("GitHub Copilot does not support global installation");
|
|
1849
1960
|
}
|
|
1850
|
-
return
|
|
1961
|
+
return join9(projectDir || process.cwd(), ".github", "copilot-instructions.md");
|
|
1851
1962
|
}
|
|
1852
1963
|
function transformContent6(content) {
|
|
1853
|
-
return
|
|
1964
|
+
return defaultTransformContent(content);
|
|
1854
1965
|
}
|
|
1855
1966
|
function install6(opts) {
|
|
1856
1967
|
const filePath = resolvePath6(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1876,10 +1987,9 @@ var copilotAdapter = {
|
|
|
1876
1987
|
};
|
|
1877
1988
|
|
|
1878
1989
|
// src/lib/installers/opencode.ts
|
|
1879
|
-
import { existsSync as
|
|
1880
|
-
import { join as
|
|
1881
|
-
import { homedir as
|
|
1882
|
-
import { execSync as execSync3 } from "child_process";
|
|
1990
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1991
|
+
import { join as join10 } from "path";
|
|
1992
|
+
import { homedir as homedir7 } from "os";
|
|
1883
1993
|
var descriptor7 = {
|
|
1884
1994
|
id: "opencode",
|
|
1885
1995
|
name: "OpenCode",
|
|
@@ -1890,46 +2000,31 @@ var descriptor7 = {
|
|
|
1890
2000
|
};
|
|
1891
2001
|
function detect7(projectDir) {
|
|
1892
2002
|
const cwd = projectDir || process.cwd();
|
|
1893
|
-
|
|
1894
|
-
try {
|
|
1895
|
-
execSync3("which opencode", { stdio: "ignore" });
|
|
1896
|
-
hasCommand = true;
|
|
1897
|
-
} catch {
|
|
1898
|
-
}
|
|
2003
|
+
const hasCommand = commandExists("opencode");
|
|
1899
2004
|
return {
|
|
1900
2005
|
global: hasCommand,
|
|
1901
|
-
project: hasCommand ||
|
|
2006
|
+
project: hasCommand || existsSync11(join10(cwd, ".opencode"))
|
|
1902
2007
|
};
|
|
1903
2008
|
}
|
|
1904
2009
|
function resolvePath7(slug, scope, projectDir, _contentType) {
|
|
1905
|
-
const safeName = slug
|
|
2010
|
+
const safeName = safeSlugName(slug);
|
|
1906
2011
|
if (scope === "global") {
|
|
1907
|
-
return
|
|
2012
|
+
return join10(homedir7(), ".config", "opencode", "rules", `${safeName}.md`);
|
|
1908
2013
|
}
|
|
1909
|
-
return
|
|
2014
|
+
return join10(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
|
|
1910
2015
|
}
|
|
1911
2016
|
function transformContent7(content) {
|
|
1912
|
-
return
|
|
2017
|
+
return defaultTransformContent(content);
|
|
1913
2018
|
}
|
|
1914
2019
|
function install7(opts) {
|
|
1915
2020
|
const targetPath = resolvePath7(opts.slug, opts.scope, opts.projectDir);
|
|
1916
|
-
|
|
1917
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1918
|
-
} else {
|
|
1919
|
-
mkdirSync8(join8(targetPath, ".."), { recursive: true });
|
|
1920
|
-
writeFileSync7(targetPath, opts.content);
|
|
1921
|
-
}
|
|
1922
|
-
return targetPath;
|
|
2021
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1923
2022
|
}
|
|
1924
2023
|
function uninstall7(installation, _slug) {
|
|
1925
|
-
|
|
1926
|
-
removeSymlink(installation.path);
|
|
1927
|
-
} else if (existsSync9(installation.path)) {
|
|
1928
|
-
unlinkSync6(installation.path);
|
|
1929
|
-
}
|
|
2024
|
+
uninstallFile(installation);
|
|
1930
2025
|
}
|
|
1931
2026
|
function defaultMethod7() {
|
|
1932
|
-
return
|
|
2027
|
+
return DEFAULT_METHOD;
|
|
1933
2028
|
}
|
|
1934
2029
|
var opencodeAdapter = {
|
|
1935
2030
|
descriptor: descriptor7,
|
|
@@ -1942,9 +2037,8 @@ var opencodeAdapter = {
|
|
|
1942
2037
|
};
|
|
1943
2038
|
|
|
1944
2039
|
// src/lib/installers/aider.ts
|
|
1945
|
-
import { existsSync as
|
|
1946
|
-
import { join as
|
|
1947
|
-
import { execSync as execSync4 } from "child_process";
|
|
2040
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync4, readFileSync as readFileSync3 } from "fs";
|
|
2041
|
+
import { join as join11 } from "path";
|
|
1948
2042
|
var descriptor8 = {
|
|
1949
2043
|
id: "aider",
|
|
1950
2044
|
name: "Aider",
|
|
@@ -1954,29 +2048,24 @@ var descriptor8 = {
|
|
|
1954
2048
|
fileExtension: ".md"
|
|
1955
2049
|
};
|
|
1956
2050
|
function detect8() {
|
|
1957
|
-
|
|
1958
|
-
try {
|
|
1959
|
-
execSync4("which aider", { stdio: "ignore" });
|
|
1960
|
-
hasCommand = true;
|
|
1961
|
-
} catch {
|
|
1962
|
-
}
|
|
2051
|
+
const hasCommand = commandExists("aider");
|
|
1963
2052
|
return { global: false, project: hasCommand };
|
|
1964
2053
|
}
|
|
1965
2054
|
function resolvePath8(slug, scope, projectDir, contentType) {
|
|
1966
2055
|
if (scope === "global") {
|
|
1967
2056
|
throw new Error("Aider does not support global installation");
|
|
1968
2057
|
}
|
|
1969
|
-
const safeName = slug
|
|
2058
|
+
const safeName = safeSlugName(slug);
|
|
1970
2059
|
const subdir = contentType === "rule" ? "rules" : "skills";
|
|
1971
|
-
return
|
|
2060
|
+
return join11(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
|
|
1972
2061
|
}
|
|
1973
2062
|
function transformContent8(content) {
|
|
1974
|
-
return
|
|
2063
|
+
return defaultTransformContent(content);
|
|
1975
2064
|
}
|
|
1976
2065
|
function addAiderRead(projectDir, relativePath) {
|
|
1977
|
-
const configPath =
|
|
2066
|
+
const configPath = join11(projectDir, ".aider.conf.yml");
|
|
1978
2067
|
let content = "";
|
|
1979
|
-
if (
|
|
2068
|
+
if (existsSync12(configPath)) {
|
|
1980
2069
|
content = readFileSync3(configPath, "utf-8");
|
|
1981
2070
|
}
|
|
1982
2071
|
if (content.includes(relativePath)) return;
|
|
@@ -1986,44 +2075,35 @@ function addAiderRead(projectDir, relativePath) {
|
|
|
1986
2075
|
} else {
|
|
1987
2076
|
content = content.trimEnd() + (content ? "\n" : "") + readLine + "\n";
|
|
1988
2077
|
}
|
|
1989
|
-
|
|
2078
|
+
writeFileSync4(configPath, content);
|
|
1990
2079
|
}
|
|
1991
2080
|
function removeAiderRead(projectDir, relativePath) {
|
|
1992
|
-
const configPath =
|
|
1993
|
-
if (!
|
|
2081
|
+
const configPath = join11(projectDir, ".aider.conf.yml");
|
|
2082
|
+
if (!existsSync12(configPath)) return;
|
|
1994
2083
|
let content = readFileSync3(configPath, "utf-8");
|
|
1995
2084
|
const lines = content.split("\n");
|
|
1996
2085
|
const filtered = lines.filter((line) => !line.includes(relativePath));
|
|
1997
|
-
|
|
2086
|
+
writeFileSync4(configPath, filtered.join("\n"));
|
|
1998
2087
|
}
|
|
1999
2088
|
function install8(opts) {
|
|
2000
2089
|
const targetPath = resolvePath8(opts.slug, opts.scope, opts.projectDir, opts.contentType);
|
|
2001
2090
|
const projectDir = opts.projectDir || process.cwd();
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
} else {
|
|
2005
|
-
mkdirSync9(join9(targetPath, ".."), { recursive: true });
|
|
2006
|
-
writeFileSync8(targetPath, opts.content);
|
|
2007
|
-
}
|
|
2008
|
-
const safeName = opts.slug.replace(/\//g, "-");
|
|
2091
|
+
installFileOrSymlink(opts, targetPath);
|
|
2092
|
+
const safeName = safeSlugName(opts.slug);
|
|
2009
2093
|
const subdir = opts.contentType === "rule" ? "rules" : "skills";
|
|
2010
2094
|
const relativePath = `.aider/${subdir}/${safeName}.md`;
|
|
2011
2095
|
addAiderRead(projectDir, relativePath);
|
|
2012
2096
|
return targetPath;
|
|
2013
2097
|
}
|
|
2014
2098
|
function uninstall8(installation, slug) {
|
|
2015
|
-
|
|
2016
|
-
removeSymlink(installation.path);
|
|
2017
|
-
} else if (existsSync10(installation.path)) {
|
|
2018
|
-
unlinkSync7(installation.path);
|
|
2019
|
-
}
|
|
2099
|
+
uninstallFile(installation);
|
|
2020
2100
|
const projectDir = installation.projectDir || process.cwd();
|
|
2021
|
-
const safeName = slug
|
|
2101
|
+
const safeName = safeSlugName(slug);
|
|
2022
2102
|
removeAiderRead(projectDir, `.aider/skills/${safeName}.md`);
|
|
2023
2103
|
removeAiderRead(projectDir, `.aider/rules/${safeName}.md`);
|
|
2024
2104
|
}
|
|
2025
2105
|
function defaultMethod8() {
|
|
2026
|
-
return
|
|
2106
|
+
return DEFAULT_METHOD;
|
|
2027
2107
|
}
|
|
2028
2108
|
var aiderAdapter = {
|
|
2029
2109
|
descriptor: descriptor8,
|
|
@@ -2056,7 +2136,7 @@ function getAllAdapters() {
|
|
|
2056
2136
|
}
|
|
2057
2137
|
|
|
2058
2138
|
// src/lib/cache.ts
|
|
2059
|
-
var CACHE_DIR =
|
|
2139
|
+
var CACHE_DIR = join12(homedir8(), ".localskills", "cache");
|
|
2060
2140
|
function slugToDir(slug) {
|
|
2061
2141
|
if (slug.includes("..") || slug.includes("\0")) {
|
|
2062
2142
|
throw new Error("Invalid slug: contains forbidden characters");
|
|
@@ -2064,7 +2144,7 @@ function slugToDir(slug) {
|
|
|
2064
2144
|
return slug.replace(/\//g, "--");
|
|
2065
2145
|
}
|
|
2066
2146
|
function getCacheDir(slug) {
|
|
2067
|
-
const dir = resolve2(
|
|
2147
|
+
const dir = resolve2(join12(CACHE_DIR, slugToDir(slug)));
|
|
2068
2148
|
if (!dir.startsWith(resolve2(CACHE_DIR) + "/") && dir !== resolve2(CACHE_DIR)) {
|
|
2069
2149
|
throw new Error("Invalid slug: path traversal detected");
|
|
2070
2150
|
}
|
|
@@ -2072,17 +2152,18 @@ function getCacheDir(slug) {
|
|
|
2072
2152
|
}
|
|
2073
2153
|
function store(slug, content, skill, version) {
|
|
2074
2154
|
const dir = getCacheDir(slug);
|
|
2075
|
-
|
|
2076
|
-
|
|
2155
|
+
mkdirSync5(dir, { recursive: true });
|
|
2156
|
+
writeFileSync5(join12(dir, "raw.md"), content);
|
|
2077
2157
|
const meta = {
|
|
2078
2158
|
hash: skill.contentHash,
|
|
2079
2159
|
version,
|
|
2160
|
+
semver: skill.currentSemver ?? null,
|
|
2080
2161
|
name: skill.name,
|
|
2081
2162
|
description: skill.description,
|
|
2082
2163
|
type: skill.type ?? "skill",
|
|
2083
2164
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2084
2165
|
};
|
|
2085
|
-
|
|
2166
|
+
writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2086
2167
|
clearPlatformFiles(slug);
|
|
2087
2168
|
}
|
|
2088
2169
|
function getPlatformFile(slug, platform, skill) {
|
|
@@ -2093,98 +2174,103 @@ function getPlatformFile(slug, platform, skill) {
|
|
|
2093
2174
|
const transformed = adapter.transformContent(raw, skill);
|
|
2094
2175
|
if (platform === "claude") {
|
|
2095
2176
|
if (skill.type === "rule") {
|
|
2096
|
-
const claudeRuleDir =
|
|
2097
|
-
|
|
2098
|
-
const filePath3 =
|
|
2099
|
-
|
|
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);
|
|
2100
2181
|
return filePath3;
|
|
2101
2182
|
}
|
|
2102
|
-
const claudeDir =
|
|
2103
|
-
|
|
2104
|
-
const filePath2 =
|
|
2105
|
-
|
|
2183
|
+
const claudeDir = join12(dir, "claude");
|
|
2184
|
+
mkdirSync5(claudeDir, { recursive: true });
|
|
2185
|
+
const filePath2 = join12(claudeDir, "SKILL.md");
|
|
2186
|
+
writeFileSync5(filePath2, transformed);
|
|
2106
2187
|
return filePath2;
|
|
2107
2188
|
}
|
|
2108
2189
|
const ext = adapter.descriptor.fileExtension;
|
|
2109
|
-
const filePath =
|
|
2110
|
-
|
|
2190
|
+
const filePath = join12(dir, `${platform}${ext}`);
|
|
2191
|
+
writeFileSync5(filePath, transformed);
|
|
2111
2192
|
return filePath;
|
|
2112
2193
|
}
|
|
2113
2194
|
function getRawContent(slug) {
|
|
2114
|
-
const filePath =
|
|
2115
|
-
if (!
|
|
2195
|
+
const filePath = join12(getCacheDir(slug), "raw.md");
|
|
2196
|
+
if (!existsSync13(filePath)) return null;
|
|
2116
2197
|
return readFileSync4(filePath, "utf-8");
|
|
2117
2198
|
}
|
|
2118
2199
|
function purge(slug) {
|
|
2119
2200
|
const dir = getCacheDir(slug);
|
|
2120
|
-
if (
|
|
2121
|
-
|
|
2201
|
+
if (existsSync13(dir)) {
|
|
2202
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
2122
2203
|
}
|
|
2123
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
|
+
}
|
|
2124
2222
|
function clearPlatformFiles(slug) {
|
|
2125
2223
|
const dir = getCacheDir(slug);
|
|
2126
|
-
if (!
|
|
2127
|
-
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"]);
|
|
2128
2226
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2129
2227
|
if (!keep.has(entry.name)) {
|
|
2130
|
-
|
|
2228
|
+
rmSync3(join12(dir, entry.name), { recursive: true, force: true });
|
|
2131
2229
|
}
|
|
2132
2230
|
}
|
|
2133
2231
|
}
|
|
2134
2232
|
|
|
2135
|
-
// src/lib/
|
|
2136
|
-
import {
|
|
2137
|
-
import {
|
|
2138
|
-
import {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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);
|
|
2146
2253
|
}
|
|
2147
|
-
|
|
2148
|
-
function detectInstalledPlatforms(projectDir) {
|
|
2149
|
-
const detected = [];
|
|
2150
|
-
const home = homedir8();
|
|
2151
|
-
const cwd = projectDir || process.cwd();
|
|
2152
|
-
if (existsSync12(join11(home, ".cursor")) || existsSync12(join11(cwd, ".cursor")))
|
|
2153
|
-
detected.push("cursor");
|
|
2154
|
-
if (existsSync12(join11(home, ".claude")) || commandExists("claude"))
|
|
2155
|
-
detected.push("claude");
|
|
2156
|
-
if (commandExists("codex")) detected.push("codex");
|
|
2157
|
-
if (existsSync12(join11(home, ".codeium")) || existsSync12(join11(cwd, ".windsurf")))
|
|
2158
|
-
detected.push("windsurf");
|
|
2159
|
-
if (existsSync12(join11(cwd, ".clinerules"))) detected.push("cline");
|
|
2160
|
-
if (existsSync12(join11(cwd, ".github"))) detected.push("copilot");
|
|
2161
|
-
if (commandExists("opencode") || existsSync12(join11(cwd, ".opencode")))
|
|
2162
|
-
detected.push("opencode");
|
|
2163
|
-
if (commandExists("aider")) detected.push("aider");
|
|
2164
|
-
return detected;
|
|
2254
|
+
return writtenFiles;
|
|
2165
2255
|
}
|
|
2166
2256
|
|
|
2167
2257
|
// src/lib/interactive.ts
|
|
2168
2258
|
async function interactiveInstall(availableSkills, detectedPlatforms) {
|
|
2169
2259
|
We("localskills install");
|
|
2170
|
-
const slug = await Je({
|
|
2260
|
+
const slug = cancelGuard(await Je({
|
|
2171
2261
|
message: "Which skill would you like to install?",
|
|
2172
2262
|
options: availableSkills.map((s) => ({
|
|
2173
2263
|
value: s.slug,
|
|
2174
2264
|
label: s.name,
|
|
2175
2265
|
hint: truncate(s.description, 60)
|
|
2176
2266
|
}))
|
|
2177
|
-
});
|
|
2178
|
-
if (Ct(slug)) {
|
|
2179
|
-
Ne("Cancelled.");
|
|
2180
|
-
process.exit(0);
|
|
2181
|
-
}
|
|
2267
|
+
}));
|
|
2182
2268
|
const rest = await interactiveTargets(detectedPlatforms);
|
|
2183
2269
|
return { slug, ...rest };
|
|
2184
2270
|
}
|
|
2185
2271
|
async function interactiveTargets(detectedPlatforms) {
|
|
2186
2272
|
const allAdapters = getAllAdapters();
|
|
2187
|
-
const platforms = await je({
|
|
2273
|
+
const platforms = cancelGuard(await je({
|
|
2188
2274
|
message: "Which platforms should receive this skill?",
|
|
2189
2275
|
options: allAdapters.map((a) => ({
|
|
2190
2276
|
value: a.descriptor.id,
|
|
@@ -2193,12 +2279,8 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2193
2279
|
})),
|
|
2194
2280
|
initialValues: detectedPlatforms.length > 0 ? detectedPlatforms : void 0,
|
|
2195
2281
|
required: true
|
|
2196
|
-
});
|
|
2197
|
-
|
|
2198
|
-
Ne("Cancelled.");
|
|
2199
|
-
process.exit(0);
|
|
2200
|
-
}
|
|
2201
|
-
const scope = await Je({
|
|
2282
|
+
}));
|
|
2283
|
+
const scope = cancelGuard(await Je({
|
|
2202
2284
|
message: "Install scope?",
|
|
2203
2285
|
options: [
|
|
2204
2286
|
{
|
|
@@ -2213,12 +2295,8 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2213
2295
|
}
|
|
2214
2296
|
],
|
|
2215
2297
|
initialValue: "project"
|
|
2216
|
-
});
|
|
2217
|
-
|
|
2218
|
-
Ne("Cancelled.");
|
|
2219
|
-
process.exit(0);
|
|
2220
|
-
}
|
|
2221
|
-
const method = await Je({
|
|
2298
|
+
}));
|
|
2299
|
+
const method = cancelGuard(await Je({
|
|
2222
2300
|
message: "Install method?",
|
|
2223
2301
|
options: [
|
|
2224
2302
|
{
|
|
@@ -2233,31 +2311,18 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2233
2311
|
}
|
|
2234
2312
|
],
|
|
2235
2313
|
initialValue: "symlink"
|
|
2236
|
-
});
|
|
2237
|
-
|
|
2238
|
-
Ne("Cancelled.");
|
|
2239
|
-
process.exit(0);
|
|
2240
|
-
}
|
|
2241
|
-
return {
|
|
2242
|
-
platforms,
|
|
2243
|
-
scope,
|
|
2244
|
-
method
|
|
2245
|
-
};
|
|
2314
|
+
}));
|
|
2315
|
+
return { platforms, scope, method };
|
|
2246
2316
|
}
|
|
2247
2317
|
async function interactiveUninstall(installedSlugs) {
|
|
2248
2318
|
We("localskills uninstall");
|
|
2249
|
-
|
|
2319
|
+
return cancelGuard(await Je({
|
|
2250
2320
|
message: "Which skill would you like to uninstall?",
|
|
2251
2321
|
options: installedSlugs.map((s) => ({
|
|
2252
2322
|
value: s,
|
|
2253
2323
|
label: s
|
|
2254
2324
|
}))
|
|
2255
|
-
});
|
|
2256
|
-
if (Ct(slug)) {
|
|
2257
|
-
Ne("Cancelled.");
|
|
2258
|
-
process.exit(0);
|
|
2259
|
-
}
|
|
2260
|
-
return slug;
|
|
2325
|
+
}));
|
|
2261
2326
|
}
|
|
2262
2327
|
function truncate(str, max) {
|
|
2263
2328
|
if (str.length <= max) return str;
|
|
@@ -2293,6 +2358,19 @@ function parsePlatforms(raw) {
|
|
|
2293
2358
|
}
|
|
2294
2359
|
return platforms;
|
|
2295
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
|
+
}
|
|
2296
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(
|
|
2297
2375
|
async (slugArg, opts) => {
|
|
2298
2376
|
const client = new ApiClient();
|
|
@@ -2345,10 +2423,17 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2345
2423
|
method = explicitMethod || answers.method;
|
|
2346
2424
|
}
|
|
2347
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);
|
|
2348
2433
|
const spinner = bt2();
|
|
2349
2434
|
spinner.start(`Fetching ${slug}...`);
|
|
2350
2435
|
const res = await client.get(
|
|
2351
|
-
`/api/skills/${encodeURIComponent(slug)}/content`
|
|
2436
|
+
`/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
|
|
2352
2437
|
);
|
|
2353
2438
|
if (!res.success || !res.data) {
|
|
2354
2439
|
spinner.stop("Failed.");
|
|
@@ -2360,9 +2445,62 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2360
2445
|
process.exit(1);
|
|
2361
2446
|
return;
|
|
2362
2447
|
}
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
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)}`);
|
|
2366
2504
|
store(cacheKey, content, skill, version);
|
|
2367
2505
|
const contentType = skill.type ?? "skill";
|
|
2368
2506
|
const installations = [];
|
|
@@ -2402,17 +2540,15 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2402
2540
|
const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
|
|
2403
2541
|
results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
|
|
2404
2542
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
name: skill.name,
|
|
2409
|
-
type: contentType,
|
|
2410
|
-
hash: skill.contentHash,
|
|
2543
|
+
config.installed_skills[cacheKey] = buildSkillRecord(
|
|
2544
|
+
cacheKey,
|
|
2545
|
+
skill,
|
|
2411
2546
|
version,
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2547
|
+
resolvedSemver,
|
|
2548
|
+
requestedRange,
|
|
2549
|
+
config.installed_skills[cacheKey]?.installations,
|
|
2550
|
+
installations
|
|
2551
|
+
);
|
|
2416
2552
|
saveConfig(config);
|
|
2417
2553
|
for (const r of results) {
|
|
2418
2554
|
R2.success(r);
|
|
@@ -2471,33 +2607,62 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
|
|
|
2471
2607
|
|
|
2472
2608
|
// src/commands/list.ts
|
|
2473
2609
|
import { Command as Command4 } from "commander";
|
|
2474
|
-
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) => {
|
|
2475
2611
|
const client = new ApiClient();
|
|
2476
|
-
if (!
|
|
2477
|
-
console.error("
|
|
2478
|
-
process.exit(1);
|
|
2479
|
-
}
|
|
2480
|
-
const path = opts.public ? "/api/skills?visibility=public" : "/api/skills";
|
|
2481
|
-
const res = await client.get(path);
|
|
2482
|
-
if (!res.success || !res.data) {
|
|
2483
|
-
console.error(`Error: ${res.error || "Failed to fetch skills"}`);
|
|
2612
|
+
if ((opts.tag || opts.search) && !opts.public) {
|
|
2613
|
+
console.error("The --tag and --search flags require --public.");
|
|
2484
2614
|
process.exit(1);
|
|
2485
|
-
return;
|
|
2486
2615
|
}
|
|
2487
|
-
if (
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
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(`
|
|
2497
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(`
|
|
2660
|
+
${res.data.length} skill(s) found.`);
|
|
2661
|
+
}
|
|
2498
2662
|
});
|
|
2499
2663
|
|
|
2500
2664
|
// src/commands/pull.ts
|
|
2665
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
2501
2666
|
import { Command as Command5 } from "commander";
|
|
2502
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) => {
|
|
2503
2668
|
const config = loadConfig();
|
|
@@ -2517,53 +2682,70 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
|
|
|
2517
2682
|
R2.warn(`${slug} \u2014 not found in config, skipping.`);
|
|
2518
2683
|
continue;
|
|
2519
2684
|
}
|
|
2685
|
+
const versionQuery = buildVersionQuery(installed.semverRange);
|
|
2520
2686
|
spinner.start(`Checking ${slug}...`);
|
|
2521
2687
|
const res = await client.get(
|
|
2522
|
-
`/api/skills/${encodeURIComponent(slug)}/content`
|
|
2688
|
+
`/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
|
|
2523
2689
|
);
|
|
2524
2690
|
if (!res.success || !res.data) {
|
|
2525
2691
|
spinner.stop(`${slug} \u2014 failed: ${res.error || "not found"}`);
|
|
2526
2692
|
continue;
|
|
2527
2693
|
}
|
|
2528
|
-
const
|
|
2694
|
+
const resData = res.data;
|
|
2695
|
+
const format = resData.format ?? "text";
|
|
2696
|
+
const { skill, version } = resData;
|
|
2529
2697
|
if (skill.contentHash === installed.hash) {
|
|
2530
2698
|
spinner.stop(`${slug} \u2014 up to date`);
|
|
2531
2699
|
skipped++;
|
|
2532
2700
|
continue;
|
|
2533
2701
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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);
|
|
2539
2711
|
}
|
|
2540
|
-
|
|
2541
|
-
const
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
slug,
|
|
2546
|
-
|
|
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}
|
|
2547
2727
|
|
|
2548
2728
|
${transformed}`
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
+
}
|
|
2560
2741
|
}
|
|
2561
2742
|
}
|
|
2562
2743
|
installed.hash = skill.contentHash;
|
|
2563
2744
|
installed.version = version;
|
|
2745
|
+
installed.semver = resData.semver ?? null;
|
|
2564
2746
|
installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2565
2747
|
updated++;
|
|
2566
|
-
spinner.stop(`${slug} \u2014 updated to
|
|
2748
|
+
spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version)}`);
|
|
2567
2749
|
}
|
|
2568
2750
|
saveConfig(config);
|
|
2569
2751
|
Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
|
|
@@ -2571,44 +2753,44 @@ ${transformed}`
|
|
|
2571
2753
|
|
|
2572
2754
|
// src/commands/publish.ts
|
|
2573
2755
|
import { Command as Command6 } from "commander";
|
|
2574
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
2575
|
-
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";
|
|
2576
2758
|
import { homedir as homedir10 } from "os";
|
|
2577
2759
|
|
|
2578
2760
|
// src/lib/scanner.ts
|
|
2579
|
-
import { existsSync as
|
|
2580
|
-
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";
|
|
2581
2763
|
import { homedir as homedir9 } from "os";
|
|
2582
|
-
import { readlinkSync
|
|
2764
|
+
import { readlinkSync, lstatSync as lstatSync2 } from "fs";
|
|
2583
2765
|
function scanForSkills(projectDir) {
|
|
2584
2766
|
const home = homedir9();
|
|
2585
2767
|
const cwd = projectDir || process.cwd();
|
|
2586
2768
|
const results = [];
|
|
2587
|
-
scanDirectory(
|
|
2588
|
-
scanDirectory(
|
|
2589
|
-
scanClaudeSkills(
|
|
2590
|
-
scanClaudeSkills(
|
|
2591
|
-
scanDirectory(
|
|
2592
|
-
scanDirectory(
|
|
2593
|
-
scanSingleFile(
|
|
2594
|
-
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);
|
|
2595
2777
|
scanSingleFile(
|
|
2596
|
-
|
|
2778
|
+
join14(home, ".codeium", "windsurf", "memories", "global_rules.md"),
|
|
2597
2779
|
"windsurf",
|
|
2598
2780
|
"global",
|
|
2599
2781
|
results
|
|
2600
2782
|
);
|
|
2601
|
-
scanDirectory(
|
|
2602
|
-
scanDirectory(
|
|
2783
|
+
scanDirectory(join14(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
|
|
2784
|
+
scanDirectory(join14(cwd, ".clinerules"), ".md", "cline", "project", results);
|
|
2603
2785
|
scanSingleFile(
|
|
2604
|
-
|
|
2786
|
+
join14(cwd, ".github", "copilot-instructions.md"),
|
|
2605
2787
|
"copilot",
|
|
2606
2788
|
"project",
|
|
2607
2789
|
results
|
|
2608
2790
|
);
|
|
2609
|
-
scanDirectory(
|
|
2610
|
-
scanDirectory(
|
|
2611
|
-
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);
|
|
2612
2794
|
return results;
|
|
2613
2795
|
}
|
|
2614
2796
|
function filterTracked(detected, config) {
|
|
@@ -2618,13 +2800,13 @@ function filterTracked(detected, config) {
|
|
|
2618
2800
|
trackedPaths.add(inst.path);
|
|
2619
2801
|
}
|
|
2620
2802
|
}
|
|
2621
|
-
const cacheDir =
|
|
2803
|
+
const cacheDir = join14(homedir9(), ".localskills", "cache");
|
|
2622
2804
|
return detected.filter((skill) => {
|
|
2623
2805
|
if (trackedPaths.has(skill.filePath)) return false;
|
|
2624
2806
|
try {
|
|
2625
2807
|
const stat = lstatSync2(skill.filePath);
|
|
2626
2808
|
if (stat.isSymbolicLink()) {
|
|
2627
|
-
const target =
|
|
2809
|
+
const target = readlinkSync(skill.filePath);
|
|
2628
2810
|
if (target.startsWith(cacheDir)) return false;
|
|
2629
2811
|
}
|
|
2630
2812
|
} catch {
|
|
@@ -2635,11 +2817,9 @@ function filterTracked(detected, config) {
|
|
|
2635
2817
|
function slugFromFilename(filename) {
|
|
2636
2818
|
return basename(filename, extname(filename));
|
|
2637
2819
|
}
|
|
2638
|
-
|
|
2639
|
-
return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2640
|
-
}
|
|
2820
|
+
var nameFromSlug = titleFromSlug;
|
|
2641
2821
|
function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
|
|
2642
|
-
if (!
|
|
2822
|
+
if (!existsSync14(dir)) return;
|
|
2643
2823
|
let entries;
|
|
2644
2824
|
try {
|
|
2645
2825
|
entries = readdirSync2(dir);
|
|
@@ -2648,7 +2828,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
2648
2828
|
}
|
|
2649
2829
|
for (const entry of entries) {
|
|
2650
2830
|
if (!entry.endsWith(ext)) continue;
|
|
2651
|
-
const filePath =
|
|
2831
|
+
const filePath = join14(dir, entry);
|
|
2652
2832
|
try {
|
|
2653
2833
|
const raw = readFileSync5(filePath, "utf-8");
|
|
2654
2834
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -2668,7 +2848,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
2668
2848
|
}
|
|
2669
2849
|
}
|
|
2670
2850
|
function scanClaudeSkills(skillsDir, scope, results) {
|
|
2671
|
-
if (!
|
|
2851
|
+
if (!existsSync14(skillsDir)) return;
|
|
2672
2852
|
let entries;
|
|
2673
2853
|
try {
|
|
2674
2854
|
entries = readdirSync2(skillsDir);
|
|
@@ -2676,8 +2856,8 @@ function scanClaudeSkills(skillsDir, scope, results) {
|
|
|
2676
2856
|
return;
|
|
2677
2857
|
}
|
|
2678
2858
|
for (const entry of entries) {
|
|
2679
|
-
const skillFile =
|
|
2680
|
-
if (!
|
|
2859
|
+
const skillFile = join14(skillsDir, entry, "SKILL.md");
|
|
2860
|
+
if (!existsSync14(skillFile)) continue;
|
|
2681
2861
|
try {
|
|
2682
2862
|
const raw = readFileSync5(skillFile, "utf-8");
|
|
2683
2863
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -2696,7 +2876,7 @@ function scanClaudeSkills(skillsDir, scope, results) {
|
|
|
2696
2876
|
}
|
|
2697
2877
|
}
|
|
2698
2878
|
function scanSingleFile(filePath, platform, scope, results) {
|
|
2699
|
-
if (!
|
|
2879
|
+
if (!existsSync14(filePath)) return;
|
|
2700
2880
|
let raw;
|
|
2701
2881
|
try {
|
|
2702
2882
|
raw = readFileSync5(filePath, "utf-8");
|
|
@@ -2747,10 +2927,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2747
2927
|
).option("--type <type>", "Content type: skill or rule", "skill").option("-m, --message <message>", "Version message").action(
|
|
2748
2928
|
async (fileArg, opts) => {
|
|
2749
2929
|
const client = new ApiClient();
|
|
2750
|
-
|
|
2751
|
-
console.error("Not authenticated. Run `localskills login` first.");
|
|
2752
|
-
process.exit(1);
|
|
2753
|
-
}
|
|
2930
|
+
requireAuth(client);
|
|
2754
2931
|
const teamsRes = await client.get("/api/tenants");
|
|
2755
2932
|
if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
|
|
2756
2933
|
console.error(
|
|
@@ -2761,8 +2938,8 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2761
2938
|
}
|
|
2762
2939
|
const teams = teamsRes.data;
|
|
2763
2940
|
if (fileArg) {
|
|
2764
|
-
const filePath =
|
|
2765
|
-
if (!
|
|
2941
|
+
const filePath = resolve4(fileArg);
|
|
2942
|
+
if (!existsSync15(filePath)) {
|
|
2766
2943
|
console.error(`File not found: ${filePath}`);
|
|
2767
2944
|
process.exit(1);
|
|
2768
2945
|
return;
|
|
@@ -2775,7 +2952,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2775
2952
|
return;
|
|
2776
2953
|
}
|
|
2777
2954
|
const defaultSlug = basename2(filePath, extname2(filePath));
|
|
2778
|
-
const defaultName = defaultSlug
|
|
2955
|
+
const defaultName = titleFromSlug(defaultSlug);
|
|
2779
2956
|
const skillName = opts.name || defaultName;
|
|
2780
2957
|
const contentType = validateContentType(opts.type || "skill");
|
|
2781
2958
|
const visibility = validateVisibility(opts.visibility || "private");
|
|
@@ -2801,7 +2978,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2801
2978
|
Le("Nothing to publish.");
|
|
2802
2979
|
return;
|
|
2803
2980
|
}
|
|
2804
|
-
const
|
|
2981
|
+
const skills = cancelGuard(await je({
|
|
2805
2982
|
message: "Select items to publish",
|
|
2806
2983
|
options: detected.map((s) => ({
|
|
2807
2984
|
value: s,
|
|
@@ -2809,28 +2986,19 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2809
2986
|
hint: `${s.platform}/${s.scope}/${s.contentType} ${shortenPath(s.filePath)}`
|
|
2810
2987
|
})),
|
|
2811
2988
|
required: true
|
|
2812
|
-
});
|
|
2813
|
-
if (Ct(selected)) {
|
|
2814
|
-
Ne("Cancelled.");
|
|
2815
|
-
process.exit(0);
|
|
2816
|
-
}
|
|
2817
|
-
const skills = selected;
|
|
2989
|
+
}));
|
|
2818
2990
|
const tenantId = await resolveTeam(teams, opts.team);
|
|
2819
2991
|
for (const skill of skills) {
|
|
2820
2992
|
R2.step(`Publishing ${skill.suggestedName}...`);
|
|
2821
|
-
const name = await Ze({
|
|
2993
|
+
const name = cancelGuard(await Ze({
|
|
2822
2994
|
message: "Skill name?",
|
|
2823
2995
|
initialValue: skill.suggestedName,
|
|
2824
2996
|
validate: (v) => {
|
|
2825
2997
|
if (!v || v.length < 1) return "Name is required";
|
|
2826
2998
|
if (v.length > 100) return "Name must be 100 characters or less";
|
|
2827
2999
|
}
|
|
2828
|
-
});
|
|
2829
|
-
|
|
2830
|
-
Ne("Cancelled.");
|
|
2831
|
-
process.exit(0);
|
|
2832
|
-
}
|
|
2833
|
-
const visibility = await Je({
|
|
3000
|
+
}));
|
|
3001
|
+
const visibility = cancelGuard(await Je({
|
|
2834
3002
|
message: "Visibility?",
|
|
2835
3003
|
options: [
|
|
2836
3004
|
{ value: "private", label: "Private", hint: "Only team members" },
|
|
@@ -2838,23 +3006,15 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2838
3006
|
{ value: "unlisted", label: "Unlisted", hint: "Accessible via direct link" }
|
|
2839
3007
|
],
|
|
2840
3008
|
initialValue: "private"
|
|
2841
|
-
});
|
|
2842
|
-
|
|
2843
|
-
Ne("Cancelled.");
|
|
2844
|
-
process.exit(0);
|
|
2845
|
-
}
|
|
2846
|
-
const contentType = await Je({
|
|
3009
|
+
}));
|
|
3010
|
+
const contentType = cancelGuard(await Je({
|
|
2847
3011
|
message: "Type?",
|
|
2848
3012
|
options: [
|
|
2849
3013
|
{ value: "skill", label: "Skill", hint: "Reusable agent instructions" },
|
|
2850
3014
|
{ value: "rule", label: "Rule", hint: "Governance constraints" }
|
|
2851
3015
|
],
|
|
2852
3016
|
initialValue: skill.contentType
|
|
2853
|
-
});
|
|
2854
|
-
if (Ct(contentType)) {
|
|
2855
|
-
Ne("Cancelled.");
|
|
2856
|
-
process.exit(0);
|
|
2857
|
-
}
|
|
3017
|
+
}));
|
|
2858
3018
|
await uploadSkill(client, {
|
|
2859
3019
|
name,
|
|
2860
3020
|
content: skill.content,
|
|
@@ -2879,19 +3039,14 @@ async function resolveTeam(teams, teamFlag) {
|
|
|
2879
3039
|
if (teams.length === 1) {
|
|
2880
3040
|
return teams[0].id;
|
|
2881
3041
|
}
|
|
2882
|
-
|
|
3042
|
+
return cancelGuard(await Je({
|
|
2883
3043
|
message: "Which team?",
|
|
2884
3044
|
options: teams.map((t) => ({
|
|
2885
3045
|
value: t.id,
|
|
2886
3046
|
label: t.name,
|
|
2887
3047
|
hint: t.slug
|
|
2888
3048
|
}))
|
|
2889
|
-
});
|
|
2890
|
-
if (Ct(selected)) {
|
|
2891
|
-
Ne("Cancelled.");
|
|
2892
|
-
process.exit(0);
|
|
2893
|
-
}
|
|
2894
|
-
return selected;
|
|
3049
|
+
}));
|
|
2895
3050
|
}
|
|
2896
3051
|
async function uploadSkill(client, params) {
|
|
2897
3052
|
const spinner = bt2();
|
|
@@ -2936,8 +3091,200 @@ function shortenPath(filePath) {
|
|
|
2936
3091
|
return filePath;
|
|
2937
3092
|
}
|
|
2938
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
|
+
|
|
2939
3286
|
// src/index.ts
|
|
2940
|
-
var program = new
|
|
3287
|
+
var program = new Command9();
|
|
2941
3288
|
program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0");
|
|
2942
3289
|
program.addCommand(loginCommand);
|
|
2943
3290
|
program.addCommand(logoutCommand);
|
|
@@ -2947,4 +3294,6 @@ program.addCommand(uninstallCommand);
|
|
|
2947
3294
|
program.addCommand(listCommand);
|
|
2948
3295
|
program.addCommand(pullCommand);
|
|
2949
3296
|
program.addCommand(publishCommand);
|
|
3297
|
+
program.addCommand(pushCommand);
|
|
3298
|
+
program.addCommand(shareCommand);
|
|
2950
3299
|
program.parse();
|