@localskills/cli 0.3.0 → 0.8.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 +1051 -427
- package/package.json +3 -2
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 Command10 } from "commander";
|
|
165
165
|
|
|
166
166
|
// src/commands/auth.ts
|
|
167
167
|
import { Command } from "commander";
|
|
@@ -521,6 +521,23 @@ var x = class {
|
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
523
|
};
|
|
524
|
+
var kt = class extends x {
|
|
525
|
+
get cursor() {
|
|
526
|
+
return this.value ? 0 : 1;
|
|
527
|
+
}
|
|
528
|
+
get _value() {
|
|
529
|
+
return this.cursor === 0;
|
|
530
|
+
}
|
|
531
|
+
constructor(e2) {
|
|
532
|
+
super(e2, false), this.value = !!e2.initialValue, this.on("userInput", () => {
|
|
533
|
+
this.value = this._value;
|
|
534
|
+
}), this.on("confirm", (s) => {
|
|
535
|
+
this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = s, this.state = "submit", this.close();
|
|
536
|
+
}), this.on("cursor", () => {
|
|
537
|
+
this.value = !this.value;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
};
|
|
524
541
|
var Lt = class extends x {
|
|
525
542
|
options;
|
|
526
543
|
cursor = 0;
|
|
@@ -876,6 +893,33 @@ var X2 = (t) => {
|
|
|
876
893
|
for (const A of f) for (const w of A) B2.push(w);
|
|
877
894
|
return h && B2.push(g), B2;
|
|
878
895
|
};
|
|
896
|
+
var Re = (t) => {
|
|
897
|
+
const r = t.active ?? "Yes", s = t.inactive ?? "No";
|
|
898
|
+
return new kt({ active: r, inactive: s, signal: t.signal, input: t.input, output: t.output, initialValue: t.initialValue ?? true, render() {
|
|
899
|
+
const i = t.withGuide ?? _.withGuide, a = `${i ? `${import_picocolors2.default.gray(d)}
|
|
900
|
+
` : ""}${W2(this.state)} ${t.message}
|
|
901
|
+
`, o = this.value ? r : s;
|
|
902
|
+
switch (this.state) {
|
|
903
|
+
case "submit": {
|
|
904
|
+
const u = i ? `${import_picocolors2.default.gray(d)} ` : "";
|
|
905
|
+
return `${a}${u}${import_picocolors2.default.dim(o)}`;
|
|
906
|
+
}
|
|
907
|
+
case "cancel": {
|
|
908
|
+
const u = i ? `${import_picocolors2.default.gray(d)} ` : "";
|
|
909
|
+
return `${a}${u}${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o))}${i ? `
|
|
910
|
+
${import_picocolors2.default.gray(d)}` : ""}`;
|
|
911
|
+
}
|
|
912
|
+
default: {
|
|
913
|
+
const u = i ? `${import_picocolors2.default.cyan(d)} ` : "", l = i ? import_picocolors2.default.cyan(x2) : "";
|
|
914
|
+
return `${a}${u}${this.value ? `${import_picocolors2.default.green(Q2)} ${r}` : `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(r)}`}${t.vertical ? i ? `
|
|
915
|
+
${import_picocolors2.default.cyan(d)} ` : `
|
|
916
|
+
` : ` ${import_picocolors2.default.dim("/")} `}${this.value ? `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.green(Q2)} ${s}`}
|
|
917
|
+
${l}
|
|
918
|
+
`;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} }).prompt();
|
|
922
|
+
};
|
|
879
923
|
var R2 = { message: (t = [], { symbol: r = import_picocolors2.default.gray(d), secondarySymbol: s = import_picocolors2.default.gray(d), output: i = process.stdout, spacing: a = 1, withGuide: o } = {}) => {
|
|
880
924
|
const u = [], l = o ?? _.withGuide, n = l ? s : "", c = l ? `${r} ` : "", g = l ? `${s} ` : "";
|
|
881
925
|
for (let p = 0; p < a; p++) u.push(n);
|
|
@@ -1101,36 +1145,124 @@ ${l}
|
|
|
1101
1145
|
} }).prompt();
|
|
1102
1146
|
|
|
1103
1147
|
// src/lib/config.ts
|
|
1104
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
1148
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from "fs";
|
|
1105
1149
|
import { join } from "path";
|
|
1106
1150
|
import { homedir } from "os";
|
|
1107
1151
|
var CONFIG_DIR = join(homedir(), ".localskills");
|
|
1108
1152
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
1109
|
-
var
|
|
1110
|
-
|
|
1111
|
-
api_url: "https://localskills.sh",
|
|
1153
|
+
var DEFAULT_PROFILE_NAME = "default";
|
|
1154
|
+
var DEFAULT_PROFILE = {
|
|
1112
1155
|
token: null,
|
|
1113
1156
|
installed_skills: {},
|
|
1114
1157
|
defaults: {
|
|
1115
1158
|
scope: "project",
|
|
1116
1159
|
method: "symlink"
|
|
1160
|
+
},
|
|
1161
|
+
anonymous_key: null
|
|
1162
|
+
};
|
|
1163
|
+
var DEFAULT_API_URL = "https://localskills.sh";
|
|
1164
|
+
var ProfileNotFoundError = class extends Error {
|
|
1165
|
+
constructor(profileName, availableProfiles, source) {
|
|
1166
|
+
const sourceLabel = source === "flag" ? "" : source === "env" ? " (from LOCALSKILLS_PROFILE)" : "";
|
|
1167
|
+
super(`Profile "${profileName}"${sourceLabel} does not exist. Available profiles: ${availableProfiles.join(", ")}`);
|
|
1168
|
+
this.profileName = profileName;
|
|
1169
|
+
this.availableProfiles = availableProfiles;
|
|
1170
|
+
this.source = source;
|
|
1171
|
+
this.name = "ProfileNotFoundError";
|
|
1117
1172
|
}
|
|
1118
1173
|
};
|
|
1119
|
-
|
|
1174
|
+
var _profileOverride;
|
|
1175
|
+
function setProfileOverride(name) {
|
|
1176
|
+
_profileOverride = name;
|
|
1177
|
+
}
|
|
1178
|
+
function resolveProfileName(config) {
|
|
1179
|
+
if (_profileOverride) {
|
|
1180
|
+
if (!config.profiles[_profileOverride]) {
|
|
1181
|
+
throw new ProfileNotFoundError(
|
|
1182
|
+
_profileOverride,
|
|
1183
|
+
Object.keys(config.profiles),
|
|
1184
|
+
"flag"
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return _profileOverride;
|
|
1188
|
+
}
|
|
1189
|
+
const envProfile = process.env.LOCALSKILLS_PROFILE;
|
|
1190
|
+
if (envProfile) {
|
|
1191
|
+
if (!config.profiles[envProfile]) {
|
|
1192
|
+
throw new ProfileNotFoundError(
|
|
1193
|
+
envProfile,
|
|
1194
|
+
Object.keys(config.profiles),
|
|
1195
|
+
"env"
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
return envProfile;
|
|
1199
|
+
}
|
|
1200
|
+
return config.active_profile;
|
|
1201
|
+
}
|
|
1202
|
+
function getActiveProfileName() {
|
|
1203
|
+
const full = loadFullConfig();
|
|
1204
|
+
return resolveProfileName(full);
|
|
1205
|
+
}
|
|
1206
|
+
function validateV3(config) {
|
|
1207
|
+
if (!config.profiles || typeof config.profiles !== "object") {
|
|
1208
|
+
config.profiles = {
|
|
1209
|
+
[DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
if (!config.profiles[DEFAULT_PROFILE_NAME]) {
|
|
1213
|
+
config.profiles[DEFAULT_PROFILE_NAME] = { ...DEFAULT_PROFILE, installed_skills: {} };
|
|
1214
|
+
}
|
|
1215
|
+
if (!config.active_profile || !config.profiles[config.active_profile]) {
|
|
1216
|
+
config.active_profile = DEFAULT_PROFILE_NAME;
|
|
1217
|
+
}
|
|
1218
|
+
return config;
|
|
1219
|
+
}
|
|
1220
|
+
function loadFullConfig() {
|
|
1120
1221
|
if (!existsSync(CONFIG_PATH)) {
|
|
1121
|
-
return {
|
|
1222
|
+
return {
|
|
1223
|
+
config_version: 3,
|
|
1224
|
+
api_url: DEFAULT_API_URL,
|
|
1225
|
+
active_profile: DEFAULT_PROFILE_NAME,
|
|
1226
|
+
profiles: {
|
|
1227
|
+
[DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1122
1230
|
}
|
|
1123
1231
|
try {
|
|
1124
1232
|
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
1233
|
+
if (raw.config_version === 3) {
|
|
1234
|
+
return validateV3(raw);
|
|
1235
|
+
}
|
|
1236
|
+
let v2;
|
|
1125
1237
|
if (!raw.config_version || raw.config_version < 2) {
|
|
1126
|
-
|
|
1238
|
+
v2 = migrateV1toV2(raw);
|
|
1239
|
+
} else {
|
|
1240
|
+
v2 = raw;
|
|
1127
1241
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
return
|
|
1242
|
+
const v3 = migrateV2toV3(v2);
|
|
1243
|
+
saveFullConfig(v3);
|
|
1244
|
+
return v3;
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
if (err instanceof SyntaxError) {
|
|
1247
|
+
console.error(`Warning: Config file is corrupt (${CONFIG_PATH}). Backing up and starting fresh.`);
|
|
1248
|
+
try {
|
|
1249
|
+
copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
|
|
1250
|
+
console.error(` Backup saved to ${CONFIG_PATH}.bak`);
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
config_version: 3,
|
|
1255
|
+
api_url: DEFAULT_API_URL,
|
|
1256
|
+
active_profile: DEFAULT_PROFILE_NAME,
|
|
1257
|
+
profiles: {
|
|
1258
|
+
[DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
throw err;
|
|
1131
1263
|
}
|
|
1132
1264
|
}
|
|
1133
|
-
function
|
|
1265
|
+
function saveFullConfig(config) {
|
|
1134
1266
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1135
1267
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
|
|
1136
1268
|
mode: 384
|
|
@@ -1141,10 +1273,35 @@ function saveConfig(config) {
|
|
|
1141
1273
|
} catch {
|
|
1142
1274
|
}
|
|
1143
1275
|
}
|
|
1276
|
+
function loadConfig() {
|
|
1277
|
+
const full = loadFullConfig();
|
|
1278
|
+
const profileName = resolveProfileName(full);
|
|
1279
|
+
const profile = full.profiles[profileName] ?? { ...DEFAULT_PROFILE, installed_skills: {} };
|
|
1280
|
+
return {
|
|
1281
|
+
config_version: 2,
|
|
1282
|
+
api_url: full.api_url,
|
|
1283
|
+
token: profile.token,
|
|
1284
|
+
installed_skills: profile.installed_skills,
|
|
1285
|
+
defaults: profile.defaults,
|
|
1286
|
+
anonymous_key: profile.anonymous_key ?? null
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function saveConfig(config) {
|
|
1290
|
+
const full = loadFullConfig();
|
|
1291
|
+
const profileName = resolveProfileName(full);
|
|
1292
|
+
full.api_url = config.api_url;
|
|
1293
|
+
full.profiles[profileName] = {
|
|
1294
|
+
token: config.token,
|
|
1295
|
+
installed_skills: config.installed_skills,
|
|
1296
|
+
defaults: config.defaults,
|
|
1297
|
+
anonymous_key: config.anonymous_key ?? null
|
|
1298
|
+
};
|
|
1299
|
+
saveFullConfig(full);
|
|
1300
|
+
}
|
|
1144
1301
|
function migrateV1toV2(v1) {
|
|
1145
1302
|
const v2 = {
|
|
1146
1303
|
config_version: 2,
|
|
1147
|
-
api_url: v1.api_url ||
|
|
1304
|
+
api_url: v1.api_url || DEFAULT_API_URL,
|
|
1148
1305
|
token: v1.token,
|
|
1149
1306
|
installed_skills: {},
|
|
1150
1307
|
defaults: {
|
|
@@ -1159,6 +1316,8 @@ function migrateV1toV2(v1) {
|
|
|
1159
1316
|
name: skill.slug,
|
|
1160
1317
|
hash: skill.hash,
|
|
1161
1318
|
version: 0,
|
|
1319
|
+
semver: null,
|
|
1320
|
+
semverRange: null,
|
|
1162
1321
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1163
1322
|
installations: [
|
|
1164
1323
|
{
|
|
@@ -1171,9 +1330,23 @@ function migrateV1toV2(v1) {
|
|
|
1171
1330
|
]
|
|
1172
1331
|
};
|
|
1173
1332
|
}
|
|
1174
|
-
saveConfig(v2);
|
|
1175
1333
|
return v2;
|
|
1176
1334
|
}
|
|
1335
|
+
function migrateV2toV3(v2) {
|
|
1336
|
+
return {
|
|
1337
|
+
config_version: 3,
|
|
1338
|
+
api_url: v2.api_url || DEFAULT_API_URL,
|
|
1339
|
+
active_profile: DEFAULT_PROFILE_NAME,
|
|
1340
|
+
profiles: {
|
|
1341
|
+
[DEFAULT_PROFILE_NAME]: {
|
|
1342
|
+
token: v2.token,
|
|
1343
|
+
installed_skills: v2.installed_skills,
|
|
1344
|
+
defaults: v2.defaults,
|
|
1345
|
+
anonymous_key: v2.anonymous_key ?? null
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1177
1350
|
function getToken() {
|
|
1178
1351
|
return loadConfig().token;
|
|
1179
1352
|
}
|
|
@@ -1187,6 +1360,14 @@ function clearToken() {
|
|
|
1187
1360
|
config.token = null;
|
|
1188
1361
|
saveConfig(config);
|
|
1189
1362
|
}
|
|
1363
|
+
function getAnonymousKey() {
|
|
1364
|
+
return loadConfig().anonymous_key ?? null;
|
|
1365
|
+
}
|
|
1366
|
+
function setAnonymousKey(key) {
|
|
1367
|
+
const config = loadConfig();
|
|
1368
|
+
config.anonymous_key = key;
|
|
1369
|
+
saveConfig(config);
|
|
1370
|
+
}
|
|
1190
1371
|
|
|
1191
1372
|
// src/lib/api-client.ts
|
|
1192
1373
|
var ApiClient = class {
|
|
@@ -1229,25 +1410,16 @@ var ApiClient = class {
|
|
|
1229
1410
|
});
|
|
1230
1411
|
return this.handleResponse(res);
|
|
1231
1412
|
}
|
|
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
|
-
});
|
|
1413
|
+
async fetchBinary(url) {
|
|
1414
|
+
const headers = {};
|
|
1415
|
+
if (this.token) {
|
|
1416
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
1417
|
+
}
|
|
1418
|
+
const res = await fetch(url, { headers });
|
|
1419
|
+
if (!res.ok) {
|
|
1420
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
1421
|
+
}
|
|
1422
|
+
return Buffer.from(await res.arrayBuffer());
|
|
1251
1423
|
}
|
|
1252
1424
|
isAuthenticated() {
|
|
1253
1425
|
return this.token !== null;
|
|
@@ -1285,9 +1457,29 @@ function openBrowser(url) {
|
|
|
1285
1457
|
}
|
|
1286
1458
|
}
|
|
1287
1459
|
function sleep(ms) {
|
|
1288
|
-
return new Promise((
|
|
1460
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1289
1461
|
}
|
|
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) => {
|
|
1462
|
+
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) => {
|
|
1463
|
+
if (opts.oidcToken) {
|
|
1464
|
+
if (!opts.team) {
|
|
1465
|
+
console.error("Error: --team is required with --oidc-token");
|
|
1466
|
+
process.exit(1);
|
|
1467
|
+
}
|
|
1468
|
+
const client2 = new ApiClient();
|
|
1469
|
+
const res = await client2.post(
|
|
1470
|
+
"/api/oidc/token",
|
|
1471
|
+
{ token: opts.oidcToken, team: opts.team }
|
|
1472
|
+
);
|
|
1473
|
+
if (!res.success || !res.data) {
|
|
1474
|
+
console.error(`OIDC login failed: ${res.error || "unknown error"}`);
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
setToken(res.data.token);
|
|
1478
|
+
console.log(
|
|
1479
|
+
`Authenticated via OIDC (expires ${new Date(res.data.expiresAt).toISOString()})`
|
|
1480
|
+
);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1291
1483
|
if (opts.token) {
|
|
1292
1484
|
setToken(opts.token);
|
|
1293
1485
|
const client2 = new ApiClient();
|
|
@@ -1382,22 +1574,75 @@ var whoamiCommand = new Command("whoami").description("Show current user info").
|
|
|
1382
1574
|
|
|
1383
1575
|
// src/commands/install.ts
|
|
1384
1576
|
import { Command as Command2 } from "commander";
|
|
1577
|
+
import { mkdirSync as mkdirSync7 } from "fs";
|
|
1578
|
+
|
|
1579
|
+
// ../../packages/shared/dist/utils/semver.js
|
|
1580
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
1581
|
+
var RANGE_RE = /^(\^|~|>=)?\d+\.\d+\.\d+$|^\*$/;
|
|
1582
|
+
function parseSemVer(v) {
|
|
1583
|
+
if (!SEMVER_RE.test(v))
|
|
1584
|
+
return null;
|
|
1585
|
+
const [major, minor, patch] = v.split(".").map(Number);
|
|
1586
|
+
if (major > 999999 || minor > 999999 || patch > 999999)
|
|
1587
|
+
return null;
|
|
1588
|
+
return { major, minor, patch };
|
|
1589
|
+
}
|
|
1590
|
+
function isValidSemVer(v) {
|
|
1591
|
+
return parseSemVer(v) !== null;
|
|
1592
|
+
}
|
|
1593
|
+
function isValidSemVerRange(range) {
|
|
1594
|
+
return RANGE_RE.test(range);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// ../../packages/shared/dist/utils/index.js
|
|
1598
|
+
function titleFromSlug(slug) {
|
|
1599
|
+
return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/lib/cli-helpers.ts
|
|
1603
|
+
function requireAuth(client) {
|
|
1604
|
+
if (!client.isAuthenticated()) {
|
|
1605
|
+
console.error("Not authenticated. Run `localskills login` first.");
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function buildVersionQuery(range) {
|
|
1610
|
+
if (!range) return "";
|
|
1611
|
+
if (isValidSemVer(range)) {
|
|
1612
|
+
return `?semver=${encodeURIComponent(range)}`;
|
|
1613
|
+
}
|
|
1614
|
+
if (isValidSemVerRange(range)) {
|
|
1615
|
+
return `?range=${encodeURIComponent(range)}`;
|
|
1616
|
+
}
|
|
1617
|
+
console.error(`Invalid version specifier: ${range}`);
|
|
1618
|
+
process.exit(1);
|
|
1619
|
+
}
|
|
1620
|
+
function formatVersionLabel(semver, version) {
|
|
1621
|
+
return semver ? `v${semver}` : `v${version}`;
|
|
1622
|
+
}
|
|
1623
|
+
function cancelGuard(value) {
|
|
1624
|
+
if (Ct(value)) {
|
|
1625
|
+
Ne("Cancelled.");
|
|
1626
|
+
process.exit(0);
|
|
1627
|
+
}
|
|
1628
|
+
return value;
|
|
1629
|
+
}
|
|
1385
1630
|
|
|
1386
1631
|
// src/lib/cache.ts
|
|
1387
1632
|
import {
|
|
1388
|
-
existsSync as
|
|
1389
|
-
mkdirSync as
|
|
1633
|
+
existsSync as existsSync13,
|
|
1634
|
+
mkdirSync as mkdirSync5,
|
|
1390
1635
|
readFileSync as readFileSync4,
|
|
1391
1636
|
readdirSync,
|
|
1392
|
-
writeFileSync as
|
|
1393
|
-
rmSync as
|
|
1637
|
+
writeFileSync as writeFileSync5,
|
|
1638
|
+
rmSync as rmSync3
|
|
1394
1639
|
} from "fs";
|
|
1395
|
-
import { join as
|
|
1396
|
-
import { homedir as
|
|
1640
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
1641
|
+
import { homedir as homedir8 } from "os";
|
|
1397
1642
|
|
|
1398
1643
|
// src/lib/installers/cursor.ts
|
|
1399
|
-
import { existsSync as
|
|
1400
|
-
import { join as
|
|
1644
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1645
|
+
import { join as join3 } from "path";
|
|
1401
1646
|
import { homedir as homedir2 } from "os";
|
|
1402
1647
|
|
|
1403
1648
|
// src/lib/content-transform.ts
|
|
@@ -1437,10 +1682,13 @@ function stripFrontmatter(content) {
|
|
|
1437
1682
|
return content;
|
|
1438
1683
|
}
|
|
1439
1684
|
|
|
1685
|
+
// src/lib/installers/common.ts
|
|
1686
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
1687
|
+
import { join as join2 } from "path";
|
|
1688
|
+
|
|
1440
1689
|
// src/lib/symlink.ts
|
|
1441
1690
|
import {
|
|
1442
1691
|
symlinkSync,
|
|
1443
|
-
readlinkSync,
|
|
1444
1692
|
unlinkSync,
|
|
1445
1693
|
lstatSync,
|
|
1446
1694
|
existsSync as existsSync2,
|
|
@@ -1477,6 +1725,31 @@ function isSymlink(path) {
|
|
|
1477
1725
|
}
|
|
1478
1726
|
}
|
|
1479
1727
|
|
|
1728
|
+
// src/lib/installers/common.ts
|
|
1729
|
+
function safeSlugName(slug) {
|
|
1730
|
+
return slug.replace(/\//g, "-");
|
|
1731
|
+
}
|
|
1732
|
+
var DEFAULT_METHOD = "symlink";
|
|
1733
|
+
function installFileOrSymlink(opts, targetPath) {
|
|
1734
|
+
if (opts.method === "symlink") {
|
|
1735
|
+
createSymlink(opts.cachePath, targetPath);
|
|
1736
|
+
} else {
|
|
1737
|
+
mkdirSync3(join2(targetPath, ".."), { recursive: true });
|
|
1738
|
+
writeFileSync2(targetPath, opts.content);
|
|
1739
|
+
}
|
|
1740
|
+
return targetPath;
|
|
1741
|
+
}
|
|
1742
|
+
function uninstallFile(installation) {
|
|
1743
|
+
if (installation.method === "symlink") {
|
|
1744
|
+
removeSymlink(installation.path);
|
|
1745
|
+
} else if (existsSync3(installation.path)) {
|
|
1746
|
+
unlinkSync2(installation.path);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
function defaultTransformContent(content) {
|
|
1750
|
+
return toPlainMD(content);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1480
1753
|
// src/lib/installers/cursor.ts
|
|
1481
1754
|
var descriptor = {
|
|
1482
1755
|
id: "cursor",
|
|
@@ -1490,40 +1763,30 @@ function detect(projectDir) {
|
|
|
1490
1763
|
const home = homedir2();
|
|
1491
1764
|
const cwd = projectDir || process.cwd();
|
|
1492
1765
|
return {
|
|
1493
|
-
global:
|
|
1494
|
-
project:
|
|
1766
|
+
global: existsSync4(join3(home, ".cursor")),
|
|
1767
|
+
project: existsSync4(join3(cwd, ".cursor"))
|
|
1495
1768
|
};
|
|
1496
1769
|
}
|
|
1497
1770
|
function resolvePath(slug, scope, projectDir, _contentType) {
|
|
1498
|
-
const safeName = slug
|
|
1771
|
+
const safeName = safeSlugName(slug);
|
|
1499
1772
|
if (scope === "global") {
|
|
1500
|
-
return
|
|
1773
|
+
return join3(homedir2(), ".cursor", "rules", `${safeName}.mdc`);
|
|
1501
1774
|
}
|
|
1502
1775
|
const dir = projectDir || process.cwd();
|
|
1503
|
-
return
|
|
1776
|
+
return join3(dir, ".cursor", "rules", `${safeName}.mdc`);
|
|
1504
1777
|
}
|
|
1505
1778
|
function transformContent(content, skill) {
|
|
1506
1779
|
return toCursorMDC(content, skill);
|
|
1507
1780
|
}
|
|
1508
1781
|
function install(opts) {
|
|
1509
1782
|
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;
|
|
1783
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1517
1784
|
}
|
|
1518
1785
|
function uninstall(installation, _slug) {
|
|
1519
|
-
|
|
1520
|
-
removeSymlink(installation.path);
|
|
1521
|
-
} else if (existsSync3(installation.path)) {
|
|
1522
|
-
unlinkSync2(installation.path);
|
|
1523
|
-
}
|
|
1786
|
+
uninstallFile(installation);
|
|
1524
1787
|
}
|
|
1525
1788
|
function defaultMethod() {
|
|
1526
|
-
return
|
|
1789
|
+
return DEFAULT_METHOD;
|
|
1527
1790
|
}
|
|
1528
1791
|
var cursorAdapter = {
|
|
1529
1792
|
descriptor,
|
|
@@ -1536,8 +1799,8 @@ var cursorAdapter = {
|
|
|
1536
1799
|
};
|
|
1537
1800
|
|
|
1538
1801
|
// src/lib/installers/claude.ts
|
|
1539
|
-
import { existsSync as
|
|
1540
|
-
import { join as
|
|
1802
|
+
import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
|
|
1803
|
+
import { join as join4 } from "path";
|
|
1541
1804
|
import { homedir as homedir3 } from "os";
|
|
1542
1805
|
var descriptor2 = {
|
|
1543
1806
|
id: "claude",
|
|
@@ -1551,17 +1814,17 @@ function detect2(projectDir) {
|
|
|
1551
1814
|
const home = homedir3();
|
|
1552
1815
|
const cwd = projectDir || process.cwd();
|
|
1553
1816
|
return {
|
|
1554
|
-
global:
|
|
1555
|
-
project:
|
|
1817
|
+
global: existsSync5(join4(home, ".claude")),
|
|
1818
|
+
project: existsSync5(join4(cwd, ".claude"))
|
|
1556
1819
|
};
|
|
1557
1820
|
}
|
|
1558
1821
|
function resolvePath2(slug, scope, projectDir, contentType) {
|
|
1559
|
-
const safeName = slug
|
|
1560
|
-
const base = scope === "global" ?
|
|
1822
|
+
const safeName = safeSlugName(slug);
|
|
1823
|
+
const base = scope === "global" ? join4(homedir3(), ".claude") : join4(projectDir || process.cwd(), ".claude");
|
|
1561
1824
|
if (contentType === "rule") {
|
|
1562
|
-
return
|
|
1825
|
+
return join4(base, "rules", `${safeName}.md`);
|
|
1563
1826
|
}
|
|
1564
|
-
return
|
|
1827
|
+
return join4(base, "skills", safeName, "SKILL.md");
|
|
1565
1828
|
}
|
|
1566
1829
|
function transformContent2(content, skill) {
|
|
1567
1830
|
if (skill.type === "rule") {
|
|
@@ -1571,32 +1834,21 @@ function transformContent2(content, skill) {
|
|
|
1571
1834
|
}
|
|
1572
1835
|
function install2(opts) {
|
|
1573
1836
|
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;
|
|
1837
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1582
1838
|
}
|
|
1583
1839
|
function uninstall2(installation, _slug) {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
} else if (existsSync4(installation.path)) {
|
|
1587
|
-
unlinkSync3(installation.path);
|
|
1588
|
-
}
|
|
1589
|
-
const parentDir = join3(installation.path, "..");
|
|
1840
|
+
uninstallFile(installation);
|
|
1841
|
+
const parentDir = join4(installation.path, "..");
|
|
1590
1842
|
try {
|
|
1591
1843
|
const { readdirSync: readdirSync3 } = __require("fs");
|
|
1592
|
-
if (
|
|
1593
|
-
|
|
1844
|
+
if (existsSync5(parentDir) && readdirSync3(parentDir).length === 0) {
|
|
1845
|
+
rmSync2(parentDir, { recursive: true });
|
|
1594
1846
|
}
|
|
1595
1847
|
} catch {
|
|
1596
1848
|
}
|
|
1597
1849
|
}
|
|
1598
1850
|
function defaultMethod2() {
|
|
1599
|
-
return
|
|
1851
|
+
return DEFAULT_METHOD;
|
|
1600
1852
|
}
|
|
1601
1853
|
var claudeAdapter = {
|
|
1602
1854
|
descriptor: descriptor2,
|
|
@@ -1609,19 +1861,50 @@ var claudeAdapter = {
|
|
|
1609
1861
|
};
|
|
1610
1862
|
|
|
1611
1863
|
// src/lib/installers/codex.ts
|
|
1612
|
-
import { join as
|
|
1864
|
+
import { join as join6 } from "path";
|
|
1865
|
+
import { homedir as homedir5 } from "os";
|
|
1866
|
+
|
|
1867
|
+
// src/lib/detect.ts
|
|
1868
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1869
|
+
import { execFileSync } from "child_process";
|
|
1613
1870
|
import { homedir as homedir4 } from "os";
|
|
1614
|
-
import {
|
|
1871
|
+
import { join as join5 } from "path";
|
|
1872
|
+
function commandExists(cmd) {
|
|
1873
|
+
try {
|
|
1874
|
+
execFileSync("which", [cmd], { stdio: "ignore" });
|
|
1875
|
+
return true;
|
|
1876
|
+
} catch {
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function detectInstalledPlatforms(projectDir) {
|
|
1881
|
+
const detected = [];
|
|
1882
|
+
const home = homedir4();
|
|
1883
|
+
const cwd = projectDir || process.cwd();
|
|
1884
|
+
if (existsSync6(join5(home, ".cursor")) || existsSync6(join5(cwd, ".cursor")))
|
|
1885
|
+
detected.push("cursor");
|
|
1886
|
+
if (existsSync6(join5(home, ".claude")) || commandExists("claude"))
|
|
1887
|
+
detected.push("claude");
|
|
1888
|
+
if (commandExists("codex")) detected.push("codex");
|
|
1889
|
+
if (existsSync6(join5(home, ".codeium")) || existsSync6(join5(cwd, ".windsurf")))
|
|
1890
|
+
detected.push("windsurf");
|
|
1891
|
+
if (existsSync6(join5(cwd, ".clinerules"))) detected.push("cline");
|
|
1892
|
+
if (existsSync6(join5(cwd, ".github"))) detected.push("copilot");
|
|
1893
|
+
if (commandExists("opencode") || existsSync6(join5(cwd, ".opencode")))
|
|
1894
|
+
detected.push("opencode");
|
|
1895
|
+
if (commandExists("aider")) detected.push("aider");
|
|
1896
|
+
return detected;
|
|
1897
|
+
}
|
|
1615
1898
|
|
|
1616
1899
|
// src/lib/marked-sections.ts
|
|
1617
|
-
import { existsSync as
|
|
1900
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1618
1901
|
import { dirname as dirname2 } from "path";
|
|
1619
1902
|
var START_MARKER = (slug) => `<!-- localskills:start:${slug} -->`;
|
|
1620
1903
|
var END_MARKER = (slug) => `<!-- localskills:end:${slug} -->`;
|
|
1621
1904
|
function upsertSection(filePath, slug, content) {
|
|
1622
|
-
|
|
1905
|
+
mkdirSync4(dirname2(filePath), { recursive: true });
|
|
1623
1906
|
let existing = "";
|
|
1624
|
-
if (
|
|
1907
|
+
if (existsSync7(filePath)) {
|
|
1625
1908
|
existing = readFileSync2(filePath, "utf-8");
|
|
1626
1909
|
}
|
|
1627
1910
|
const start = START_MARKER(slug);
|
|
@@ -1638,10 +1921,10 @@ ${end}`;
|
|
|
1638
1921
|
const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
|
|
1639
1922
|
result = existing + separator + section + "\n";
|
|
1640
1923
|
}
|
|
1641
|
-
|
|
1924
|
+
writeFileSync3(filePath, result);
|
|
1642
1925
|
}
|
|
1643
1926
|
function removeSection(filePath, slug) {
|
|
1644
|
-
if (!
|
|
1927
|
+
if (!existsSync7(filePath)) return false;
|
|
1645
1928
|
const existing = readFileSync2(filePath, "utf-8");
|
|
1646
1929
|
const start = START_MARKER(slug);
|
|
1647
1930
|
const end = END_MARKER(slug);
|
|
@@ -1653,11 +1936,11 @@ function removeSection(filePath, slug) {
|
|
|
1653
1936
|
while (before.endsWith("\n\n")) before = before.slice(0, -1);
|
|
1654
1937
|
while (after.startsWith("\n\n")) after = after.slice(1);
|
|
1655
1938
|
const result = (before + after).trim();
|
|
1656
|
-
|
|
1939
|
+
writeFileSync3(filePath, result ? result + "\n" : "");
|
|
1657
1940
|
return true;
|
|
1658
1941
|
}
|
|
1659
1942
|
function listSections(filePath) {
|
|
1660
|
-
if (!
|
|
1943
|
+
if (!existsSync7(filePath)) return [];
|
|
1661
1944
|
const content = readFileSync2(filePath, "utf-8");
|
|
1662
1945
|
const regex = /<!-- localskills:start:(.+?) -->/g;
|
|
1663
1946
|
const slugs = [];
|
|
@@ -1678,22 +1961,17 @@ var descriptor3 = {
|
|
|
1678
1961
|
fileExtension: ".md"
|
|
1679
1962
|
};
|
|
1680
1963
|
function detect3() {
|
|
1681
|
-
|
|
1682
|
-
try {
|
|
1683
|
-
execSync("which codex", { stdio: "ignore" });
|
|
1684
|
-
hasCommand = true;
|
|
1685
|
-
} catch {
|
|
1686
|
-
}
|
|
1964
|
+
const hasCommand = commandExists("codex");
|
|
1687
1965
|
return { global: hasCommand, project: hasCommand };
|
|
1688
1966
|
}
|
|
1689
1967
|
function resolvePath3(slug, scope, projectDir, _contentType) {
|
|
1690
1968
|
if (scope === "global") {
|
|
1691
|
-
return
|
|
1969
|
+
return join6(homedir5(), ".codex", "AGENTS.md");
|
|
1692
1970
|
}
|
|
1693
|
-
return
|
|
1971
|
+
return join6(projectDir || process.cwd(), "AGENTS.md");
|
|
1694
1972
|
}
|
|
1695
1973
|
function transformContent3(content) {
|
|
1696
|
-
return
|
|
1974
|
+
return defaultTransformContent(content);
|
|
1697
1975
|
}
|
|
1698
1976
|
function install3(opts) {
|
|
1699
1977
|
const filePath = resolvePath3(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1719,9 +1997,9 @@ var codexAdapter = {
|
|
|
1719
1997
|
};
|
|
1720
1998
|
|
|
1721
1999
|
// src/lib/installers/windsurf.ts
|
|
1722
|
-
import { existsSync as
|
|
1723
|
-
import { join as
|
|
1724
|
-
import { homedir as
|
|
2000
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2001
|
+
import { join as join7 } from "path";
|
|
2002
|
+
import { homedir as homedir6 } from "os";
|
|
1725
2003
|
var descriptor4 = {
|
|
1726
2004
|
id: "windsurf",
|
|
1727
2005
|
name: "Windsurf",
|
|
@@ -1732,22 +2010,22 @@ var descriptor4 = {
|
|
|
1732
2010
|
fileExtension: ".md"
|
|
1733
2011
|
};
|
|
1734
2012
|
function detect4(projectDir) {
|
|
1735
|
-
const home =
|
|
2013
|
+
const home = homedir6();
|
|
1736
2014
|
const cwd = projectDir || process.cwd();
|
|
1737
2015
|
return {
|
|
1738
|
-
global:
|
|
1739
|
-
project:
|
|
2016
|
+
global: existsSync8(join7(home, ".codeium")),
|
|
2017
|
+
project: existsSync8(join7(cwd, ".windsurf"))
|
|
1740
2018
|
};
|
|
1741
2019
|
}
|
|
1742
2020
|
function resolvePath4(slug, scope, projectDir, _contentType) {
|
|
1743
|
-
const safeName = slug
|
|
2021
|
+
const safeName = safeSlugName(slug);
|
|
1744
2022
|
if (scope === "global") {
|
|
1745
|
-
return
|
|
2023
|
+
return join7(homedir6(), ".codeium", "windsurf", "memories", "global_rules.md");
|
|
1746
2024
|
}
|
|
1747
|
-
return
|
|
2025
|
+
return join7(projectDir || process.cwd(), ".windsurf", "rules", `${safeName}.md`);
|
|
1748
2026
|
}
|
|
1749
2027
|
function transformContent4(content) {
|
|
1750
|
-
return
|
|
2028
|
+
return defaultTransformContent(content);
|
|
1751
2029
|
}
|
|
1752
2030
|
function install4(opts) {
|
|
1753
2031
|
const targetPath = resolvePath4(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1755,21 +2033,16 @@ function install4(opts) {
|
|
|
1755
2033
|
upsertSection(targetPath, opts.slug, `## ${opts.slug}
|
|
1756
2034
|
|
|
1757
2035
|
${opts.content}`);
|
|
1758
|
-
} else if (opts.method === "symlink") {
|
|
1759
|
-
createSymlink(opts.cachePath, targetPath);
|
|
1760
2036
|
} else {
|
|
1761
|
-
|
|
1762
|
-
writeFileSync5(targetPath, opts.content);
|
|
2037
|
+
installFileOrSymlink(opts, targetPath);
|
|
1763
2038
|
}
|
|
1764
2039
|
return targetPath;
|
|
1765
2040
|
}
|
|
1766
2041
|
function uninstall4(installation, slug) {
|
|
1767
2042
|
if (installation.method === "section") {
|
|
1768
2043
|
removeSection(installation.path, slug);
|
|
1769
|
-
} else
|
|
1770
|
-
|
|
1771
|
-
} else if (existsSync6(installation.path)) {
|
|
1772
|
-
unlinkSync4(installation.path);
|
|
2044
|
+
} else {
|
|
2045
|
+
uninstallFile(installation);
|
|
1773
2046
|
}
|
|
1774
2047
|
}
|
|
1775
2048
|
function defaultMethod4(scope) {
|
|
@@ -1786,8 +2059,8 @@ var windsurfAdapter = {
|
|
|
1786
2059
|
};
|
|
1787
2060
|
|
|
1788
2061
|
// src/lib/installers/cline.ts
|
|
1789
|
-
import { existsSync as
|
|
1790
|
-
import { join as
|
|
2062
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2063
|
+
import { join as join8 } from "path";
|
|
1791
2064
|
var descriptor5 = {
|
|
1792
2065
|
id: "cline",
|
|
1793
2066
|
name: "Cline",
|
|
@@ -1800,38 +2073,28 @@ function detect5(projectDir) {
|
|
|
1800
2073
|
const cwd = projectDir || process.cwd();
|
|
1801
2074
|
return {
|
|
1802
2075
|
global: false,
|
|
1803
|
-
project:
|
|
2076
|
+
project: existsSync9(join8(cwd, ".clinerules"))
|
|
1804
2077
|
};
|
|
1805
2078
|
}
|
|
1806
2079
|
function resolvePath5(slug, scope, projectDir, _contentType) {
|
|
1807
2080
|
if (scope === "global") {
|
|
1808
2081
|
throw new Error("Cline does not support global installation");
|
|
1809
2082
|
}
|
|
1810
|
-
const safeName = slug
|
|
1811
|
-
return
|
|
2083
|
+
const safeName = safeSlugName(slug);
|
|
2084
|
+
return join8(projectDir || process.cwd(), ".clinerules", `${safeName}.md`);
|
|
1812
2085
|
}
|
|
1813
2086
|
function transformContent5(content) {
|
|
1814
|
-
return
|
|
2087
|
+
return defaultTransformContent(content);
|
|
1815
2088
|
}
|
|
1816
2089
|
function install5(opts) {
|
|
1817
2090
|
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;
|
|
2091
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1825
2092
|
}
|
|
1826
2093
|
function uninstall5(installation, _slug) {
|
|
1827
|
-
|
|
1828
|
-
removeSymlink(installation.path);
|
|
1829
|
-
} else if (existsSync7(installation.path)) {
|
|
1830
|
-
unlinkSync5(installation.path);
|
|
1831
|
-
}
|
|
2094
|
+
uninstallFile(installation);
|
|
1832
2095
|
}
|
|
1833
2096
|
function defaultMethod5() {
|
|
1834
|
-
return
|
|
2097
|
+
return DEFAULT_METHOD;
|
|
1835
2098
|
}
|
|
1836
2099
|
var clineAdapter = {
|
|
1837
2100
|
descriptor: descriptor5,
|
|
@@ -1844,8 +2107,8 @@ var clineAdapter = {
|
|
|
1844
2107
|
};
|
|
1845
2108
|
|
|
1846
2109
|
// src/lib/installers/copilot.ts
|
|
1847
|
-
import { existsSync as
|
|
1848
|
-
import { join as
|
|
2110
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2111
|
+
import { join as join9 } from "path";
|
|
1849
2112
|
var descriptor6 = {
|
|
1850
2113
|
id: "copilot",
|
|
1851
2114
|
name: "GitHub Copilot",
|
|
@@ -1858,17 +2121,17 @@ function detect6(projectDir) {
|
|
|
1858
2121
|
const cwd = projectDir || process.cwd();
|
|
1859
2122
|
return {
|
|
1860
2123
|
global: false,
|
|
1861
|
-
project:
|
|
2124
|
+
project: existsSync10(join9(cwd, ".github"))
|
|
1862
2125
|
};
|
|
1863
2126
|
}
|
|
1864
2127
|
function resolvePath6(slug, scope, projectDir, _contentType) {
|
|
1865
2128
|
if (scope === "global") {
|
|
1866
2129
|
throw new Error("GitHub Copilot does not support global installation");
|
|
1867
2130
|
}
|
|
1868
|
-
return
|
|
2131
|
+
return join9(projectDir || process.cwd(), ".github", "copilot-instructions.md");
|
|
1869
2132
|
}
|
|
1870
2133
|
function transformContent6(content) {
|
|
1871
|
-
return
|
|
2134
|
+
return defaultTransformContent(content);
|
|
1872
2135
|
}
|
|
1873
2136
|
function install6(opts) {
|
|
1874
2137
|
const filePath = resolvePath6(opts.slug, opts.scope, opts.projectDir);
|
|
@@ -1894,10 +2157,9 @@ var copilotAdapter = {
|
|
|
1894
2157
|
};
|
|
1895
2158
|
|
|
1896
2159
|
// 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";
|
|
2160
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2161
|
+
import { join as join10 } from "path";
|
|
2162
|
+
import { homedir as homedir7 } from "os";
|
|
1901
2163
|
var descriptor7 = {
|
|
1902
2164
|
id: "opencode",
|
|
1903
2165
|
name: "OpenCode",
|
|
@@ -1908,46 +2170,31 @@ var descriptor7 = {
|
|
|
1908
2170
|
};
|
|
1909
2171
|
function detect7(projectDir) {
|
|
1910
2172
|
const cwd = projectDir || process.cwd();
|
|
1911
|
-
|
|
1912
|
-
try {
|
|
1913
|
-
execSync2("which opencode", { stdio: "ignore" });
|
|
1914
|
-
hasCommand = true;
|
|
1915
|
-
} catch {
|
|
1916
|
-
}
|
|
2173
|
+
const hasCommand = commandExists("opencode");
|
|
1917
2174
|
return {
|
|
1918
2175
|
global: hasCommand,
|
|
1919
|
-
project: hasCommand ||
|
|
2176
|
+
project: hasCommand || existsSync11(join10(cwd, ".opencode"))
|
|
1920
2177
|
};
|
|
1921
2178
|
}
|
|
1922
2179
|
function resolvePath7(slug, scope, projectDir, _contentType) {
|
|
1923
|
-
const safeName = slug
|
|
2180
|
+
const safeName = safeSlugName(slug);
|
|
1924
2181
|
if (scope === "global") {
|
|
1925
|
-
return
|
|
2182
|
+
return join10(homedir7(), ".config", "opencode", "rules", `${safeName}.md`);
|
|
1926
2183
|
}
|
|
1927
|
-
return
|
|
2184
|
+
return join10(projectDir || process.cwd(), ".opencode", "rules", `${safeName}.md`);
|
|
1928
2185
|
}
|
|
1929
2186
|
function transformContent7(content) {
|
|
1930
|
-
return
|
|
2187
|
+
return defaultTransformContent(content);
|
|
1931
2188
|
}
|
|
1932
2189
|
function install7(opts) {
|
|
1933
2190
|
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;
|
|
2191
|
+
return installFileOrSymlink(opts, targetPath);
|
|
1941
2192
|
}
|
|
1942
2193
|
function uninstall7(installation, _slug) {
|
|
1943
|
-
|
|
1944
|
-
removeSymlink(installation.path);
|
|
1945
|
-
} else if (existsSync9(installation.path)) {
|
|
1946
|
-
unlinkSync6(installation.path);
|
|
1947
|
-
}
|
|
2194
|
+
uninstallFile(installation);
|
|
1948
2195
|
}
|
|
1949
2196
|
function defaultMethod7() {
|
|
1950
|
-
return
|
|
2197
|
+
return DEFAULT_METHOD;
|
|
1951
2198
|
}
|
|
1952
2199
|
var opencodeAdapter = {
|
|
1953
2200
|
descriptor: descriptor7,
|
|
@@ -1960,9 +2207,8 @@ var opencodeAdapter = {
|
|
|
1960
2207
|
};
|
|
1961
2208
|
|
|
1962
2209
|
// src/lib/installers/aider.ts
|
|
1963
|
-
import { existsSync as
|
|
1964
|
-
import { join as
|
|
1965
|
-
import { execSync as execSync3 } from "child_process";
|
|
2210
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync4, readFileSync as readFileSync3 } from "fs";
|
|
2211
|
+
import { join as join11 } from "path";
|
|
1966
2212
|
var descriptor8 = {
|
|
1967
2213
|
id: "aider",
|
|
1968
2214
|
name: "Aider",
|
|
@@ -1972,29 +2218,24 @@ var descriptor8 = {
|
|
|
1972
2218
|
fileExtension: ".md"
|
|
1973
2219
|
};
|
|
1974
2220
|
function detect8() {
|
|
1975
|
-
|
|
1976
|
-
try {
|
|
1977
|
-
execSync3("which aider", { stdio: "ignore" });
|
|
1978
|
-
hasCommand = true;
|
|
1979
|
-
} catch {
|
|
1980
|
-
}
|
|
2221
|
+
const hasCommand = commandExists("aider");
|
|
1981
2222
|
return { global: false, project: hasCommand };
|
|
1982
2223
|
}
|
|
1983
2224
|
function resolvePath8(slug, scope, projectDir, contentType) {
|
|
1984
2225
|
if (scope === "global") {
|
|
1985
2226
|
throw new Error("Aider does not support global installation");
|
|
1986
2227
|
}
|
|
1987
|
-
const safeName = slug
|
|
2228
|
+
const safeName = safeSlugName(slug);
|
|
1988
2229
|
const subdir = contentType === "rule" ? "rules" : "skills";
|
|
1989
|
-
return
|
|
2230
|
+
return join11(projectDir || process.cwd(), ".aider", subdir, `${safeName}.md`);
|
|
1990
2231
|
}
|
|
1991
2232
|
function transformContent8(content) {
|
|
1992
|
-
return
|
|
2233
|
+
return defaultTransformContent(content);
|
|
1993
2234
|
}
|
|
1994
2235
|
function addAiderRead(projectDir, relativePath) {
|
|
1995
|
-
const configPath =
|
|
2236
|
+
const configPath = join11(projectDir, ".aider.conf.yml");
|
|
1996
2237
|
let content = "";
|
|
1997
|
-
if (
|
|
2238
|
+
if (existsSync12(configPath)) {
|
|
1998
2239
|
content = readFileSync3(configPath, "utf-8");
|
|
1999
2240
|
}
|
|
2000
2241
|
if (content.includes(relativePath)) return;
|
|
@@ -2004,44 +2245,35 @@ function addAiderRead(projectDir, relativePath) {
|
|
|
2004
2245
|
} else {
|
|
2005
2246
|
content = content.trimEnd() + (content ? "\n" : "") + readLine + "\n";
|
|
2006
2247
|
}
|
|
2007
|
-
|
|
2248
|
+
writeFileSync4(configPath, content);
|
|
2008
2249
|
}
|
|
2009
2250
|
function removeAiderRead(projectDir, relativePath) {
|
|
2010
|
-
const configPath =
|
|
2011
|
-
if (!
|
|
2251
|
+
const configPath = join11(projectDir, ".aider.conf.yml");
|
|
2252
|
+
if (!existsSync12(configPath)) return;
|
|
2012
2253
|
let content = readFileSync3(configPath, "utf-8");
|
|
2013
2254
|
const lines = content.split("\n");
|
|
2014
2255
|
const filtered = lines.filter((line) => !line.includes(relativePath));
|
|
2015
|
-
|
|
2256
|
+
writeFileSync4(configPath, filtered.join("\n"));
|
|
2016
2257
|
}
|
|
2017
2258
|
function install8(opts) {
|
|
2018
2259
|
const targetPath = resolvePath8(opts.slug, opts.scope, opts.projectDir, opts.contentType);
|
|
2019
2260
|
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, "-");
|
|
2261
|
+
installFileOrSymlink(opts, targetPath);
|
|
2262
|
+
const safeName = safeSlugName(opts.slug);
|
|
2027
2263
|
const subdir = opts.contentType === "rule" ? "rules" : "skills";
|
|
2028
2264
|
const relativePath = `.aider/${subdir}/${safeName}.md`;
|
|
2029
2265
|
addAiderRead(projectDir, relativePath);
|
|
2030
2266
|
return targetPath;
|
|
2031
2267
|
}
|
|
2032
2268
|
function uninstall8(installation, slug) {
|
|
2033
|
-
|
|
2034
|
-
removeSymlink(installation.path);
|
|
2035
|
-
} else if (existsSync10(installation.path)) {
|
|
2036
|
-
unlinkSync7(installation.path);
|
|
2037
|
-
}
|
|
2269
|
+
uninstallFile(installation);
|
|
2038
2270
|
const projectDir = installation.projectDir || process.cwd();
|
|
2039
|
-
const safeName = slug
|
|
2271
|
+
const safeName = safeSlugName(slug);
|
|
2040
2272
|
removeAiderRead(projectDir, `.aider/skills/${safeName}.md`);
|
|
2041
2273
|
removeAiderRead(projectDir, `.aider/rules/${safeName}.md`);
|
|
2042
2274
|
}
|
|
2043
2275
|
function defaultMethod8() {
|
|
2044
|
-
return
|
|
2276
|
+
return DEFAULT_METHOD;
|
|
2045
2277
|
}
|
|
2046
2278
|
var aiderAdapter = {
|
|
2047
2279
|
descriptor: descriptor8,
|
|
@@ -2074,7 +2306,7 @@ function getAllAdapters() {
|
|
|
2074
2306
|
}
|
|
2075
2307
|
|
|
2076
2308
|
// src/lib/cache.ts
|
|
2077
|
-
var CACHE_DIR =
|
|
2309
|
+
var CACHE_DIR = join12(homedir8(), ".localskills", "cache");
|
|
2078
2310
|
function slugToDir(slug) {
|
|
2079
2311
|
if (slug.includes("..") || slug.includes("\0")) {
|
|
2080
2312
|
throw new Error("Invalid slug: contains forbidden characters");
|
|
@@ -2082,7 +2314,7 @@ function slugToDir(slug) {
|
|
|
2082
2314
|
return slug.replace(/\//g, "--");
|
|
2083
2315
|
}
|
|
2084
2316
|
function getCacheDir(slug) {
|
|
2085
|
-
const dir = resolve2(
|
|
2317
|
+
const dir = resolve2(join12(CACHE_DIR, slugToDir(slug)));
|
|
2086
2318
|
if (!dir.startsWith(resolve2(CACHE_DIR) + "/") && dir !== resolve2(CACHE_DIR)) {
|
|
2087
2319
|
throw new Error("Invalid slug: path traversal detected");
|
|
2088
2320
|
}
|
|
@@ -2090,17 +2322,18 @@ function getCacheDir(slug) {
|
|
|
2090
2322
|
}
|
|
2091
2323
|
function store(slug, content, skill, version) {
|
|
2092
2324
|
const dir = getCacheDir(slug);
|
|
2093
|
-
|
|
2094
|
-
|
|
2325
|
+
mkdirSync5(dir, { recursive: true });
|
|
2326
|
+
writeFileSync5(join12(dir, "raw.md"), content);
|
|
2095
2327
|
const meta = {
|
|
2096
2328
|
hash: skill.contentHash,
|
|
2097
2329
|
version,
|
|
2330
|
+
semver: skill.currentSemver ?? null,
|
|
2098
2331
|
name: skill.name,
|
|
2099
2332
|
description: skill.description,
|
|
2100
2333
|
type: skill.type ?? "skill",
|
|
2101
2334
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2102
2335
|
};
|
|
2103
|
-
|
|
2336
|
+
writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2104
2337
|
clearPlatformFiles(slug);
|
|
2105
2338
|
}
|
|
2106
2339
|
function getPlatformFile(slug, platform, skill) {
|
|
@@ -2111,98 +2344,103 @@ function getPlatformFile(slug, platform, skill) {
|
|
|
2111
2344
|
const transformed = adapter.transformContent(raw, skill);
|
|
2112
2345
|
if (platform === "claude") {
|
|
2113
2346
|
if (skill.type === "rule") {
|
|
2114
|
-
const claudeRuleDir =
|
|
2115
|
-
|
|
2116
|
-
const filePath3 =
|
|
2117
|
-
|
|
2347
|
+
const claudeRuleDir = join12(dir, "claude-rule");
|
|
2348
|
+
mkdirSync5(claudeRuleDir, { recursive: true });
|
|
2349
|
+
const filePath3 = join12(claudeRuleDir, `${slug.replace(/\//g, "-")}.md`);
|
|
2350
|
+
writeFileSync5(filePath3, transformed);
|
|
2118
2351
|
return filePath3;
|
|
2119
2352
|
}
|
|
2120
|
-
const claudeDir =
|
|
2121
|
-
|
|
2122
|
-
const filePath2 =
|
|
2123
|
-
|
|
2353
|
+
const claudeDir = join12(dir, "claude");
|
|
2354
|
+
mkdirSync5(claudeDir, { recursive: true });
|
|
2355
|
+
const filePath2 = join12(claudeDir, "SKILL.md");
|
|
2356
|
+
writeFileSync5(filePath2, transformed);
|
|
2124
2357
|
return filePath2;
|
|
2125
2358
|
}
|
|
2126
2359
|
const ext = adapter.descriptor.fileExtension;
|
|
2127
|
-
const filePath =
|
|
2128
|
-
|
|
2360
|
+
const filePath = join12(dir, `${platform}${ext}`);
|
|
2361
|
+
writeFileSync5(filePath, transformed);
|
|
2129
2362
|
return filePath;
|
|
2130
2363
|
}
|
|
2131
2364
|
function getRawContent(slug) {
|
|
2132
|
-
const filePath =
|
|
2133
|
-
if (!
|
|
2365
|
+
const filePath = join12(getCacheDir(slug), "raw.md");
|
|
2366
|
+
if (!existsSync13(filePath)) return null;
|
|
2134
2367
|
return readFileSync4(filePath, "utf-8");
|
|
2135
2368
|
}
|
|
2136
2369
|
function purge(slug) {
|
|
2137
2370
|
const dir = getCacheDir(slug);
|
|
2138
|
-
if (
|
|
2139
|
-
|
|
2371
|
+
if (existsSync13(dir)) {
|
|
2372
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
2140
2373
|
}
|
|
2141
2374
|
}
|
|
2375
|
+
function storePackage(slug, zipBuffer, manifest, skill, version) {
|
|
2376
|
+
const dir = getCacheDir(slug);
|
|
2377
|
+
mkdirSync5(dir, { recursive: true });
|
|
2378
|
+
writeFileSync5(join12(dir, "package.zip"), zipBuffer);
|
|
2379
|
+
writeFileSync5(join12(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
2380
|
+
const meta = {
|
|
2381
|
+
hash: skill.contentHash,
|
|
2382
|
+
version,
|
|
2383
|
+
semver: skill.currentSemver ?? null,
|
|
2384
|
+
name: skill.name,
|
|
2385
|
+
description: skill.description,
|
|
2386
|
+
type: skill.type ?? "skill",
|
|
2387
|
+
format: "package",
|
|
2388
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2389
|
+
};
|
|
2390
|
+
writeFileSync5(join12(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2391
|
+
}
|
|
2142
2392
|
function clearPlatformFiles(slug) {
|
|
2143
2393
|
const dir = getCacheDir(slug);
|
|
2144
|
-
if (!
|
|
2145
|
-
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json"]);
|
|
2394
|
+
if (!existsSync13(dir)) return;
|
|
2395
|
+
const keep = /* @__PURE__ */ new Set(["raw.md", "meta.json", "package.zip", "manifest.json"]);
|
|
2146
2396
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2147
2397
|
if (!keep.has(entry.name)) {
|
|
2148
|
-
|
|
2398
|
+
rmSync3(join12(dir, entry.name), { recursive: true, force: true });
|
|
2149
2399
|
}
|
|
2150
2400
|
}
|
|
2151
2401
|
}
|
|
2152
2402
|
|
|
2153
|
-
// src/lib/
|
|
2154
|
-
import {
|
|
2155
|
-
import {
|
|
2156
|
-
import {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2403
|
+
// src/lib/extract.ts
|
|
2404
|
+
import { unzipSync } from "fflate";
|
|
2405
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
2406
|
+
import { join as join13, dirname as dirname3, resolve as resolve3 } from "path";
|
|
2407
|
+
function extractPackage(zipBuffer, targetDir) {
|
|
2408
|
+
const resolvedTarget = resolve3(targetDir);
|
|
2409
|
+
const extracted = unzipSync(new Uint8Array(zipBuffer));
|
|
2410
|
+
const writtenFiles = [];
|
|
2411
|
+
for (const [path, data] of Object.entries(extracted)) {
|
|
2412
|
+
if (path.endsWith("/")) continue;
|
|
2413
|
+
if (path.includes("..") || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
const fullPath = resolve3(join13(targetDir, path));
|
|
2417
|
+
if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
mkdirSync6(dirname3(fullPath), { recursive: true });
|
|
2421
|
+
writeFileSync6(fullPath, Buffer.from(data));
|
|
2422
|
+
writtenFiles.push(path);
|
|
2164
2423
|
}
|
|
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;
|
|
2424
|
+
return writtenFiles;
|
|
2183
2425
|
}
|
|
2184
2426
|
|
|
2185
2427
|
// src/lib/interactive.ts
|
|
2186
2428
|
async function interactiveInstall(availableSkills, detectedPlatforms) {
|
|
2187
2429
|
We("localskills install");
|
|
2188
|
-
const slug = await Je({
|
|
2430
|
+
const slug = cancelGuard(await Je({
|
|
2189
2431
|
message: "Which skill would you like to install?",
|
|
2190
2432
|
options: availableSkills.map((s) => ({
|
|
2191
2433
|
value: s.slug,
|
|
2192
2434
|
label: s.name,
|
|
2193
2435
|
hint: truncate(s.description, 60)
|
|
2194
2436
|
}))
|
|
2195
|
-
});
|
|
2196
|
-
if (Ct(slug)) {
|
|
2197
|
-
Ne("Cancelled.");
|
|
2198
|
-
process.exit(0);
|
|
2199
|
-
}
|
|
2437
|
+
}));
|
|
2200
2438
|
const rest = await interactiveTargets(detectedPlatforms);
|
|
2201
2439
|
return { slug, ...rest };
|
|
2202
2440
|
}
|
|
2203
2441
|
async function interactiveTargets(detectedPlatforms) {
|
|
2204
2442
|
const allAdapters = getAllAdapters();
|
|
2205
|
-
const platforms = await je({
|
|
2443
|
+
const platforms = cancelGuard(await je({
|
|
2206
2444
|
message: "Which platforms should receive this skill?",
|
|
2207
2445
|
options: allAdapters.map((a) => ({
|
|
2208
2446
|
value: a.descriptor.id,
|
|
@@ -2211,12 +2449,8 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2211
2449
|
})),
|
|
2212
2450
|
initialValues: detectedPlatforms.length > 0 ? detectedPlatforms : void 0,
|
|
2213
2451
|
required: true
|
|
2214
|
-
});
|
|
2215
|
-
|
|
2216
|
-
Ne("Cancelled.");
|
|
2217
|
-
process.exit(0);
|
|
2218
|
-
}
|
|
2219
|
-
const scope = await Je({
|
|
2452
|
+
}));
|
|
2453
|
+
const scope = cancelGuard(await Je({
|
|
2220
2454
|
message: "Install scope?",
|
|
2221
2455
|
options: [
|
|
2222
2456
|
{
|
|
@@ -2231,12 +2465,8 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2231
2465
|
}
|
|
2232
2466
|
],
|
|
2233
2467
|
initialValue: "project"
|
|
2234
|
-
});
|
|
2235
|
-
|
|
2236
|
-
Ne("Cancelled.");
|
|
2237
|
-
process.exit(0);
|
|
2238
|
-
}
|
|
2239
|
-
const method = await Je({
|
|
2468
|
+
}));
|
|
2469
|
+
const method = cancelGuard(await Je({
|
|
2240
2470
|
message: "Install method?",
|
|
2241
2471
|
options: [
|
|
2242
2472
|
{
|
|
@@ -2251,31 +2481,18 @@ async function interactiveTargets(detectedPlatforms) {
|
|
|
2251
2481
|
}
|
|
2252
2482
|
],
|
|
2253
2483
|
initialValue: "symlink"
|
|
2254
|
-
});
|
|
2255
|
-
|
|
2256
|
-
Ne("Cancelled.");
|
|
2257
|
-
process.exit(0);
|
|
2258
|
-
}
|
|
2259
|
-
return {
|
|
2260
|
-
platforms,
|
|
2261
|
-
scope,
|
|
2262
|
-
method
|
|
2263
|
-
};
|
|
2484
|
+
}));
|
|
2485
|
+
return { platforms, scope, method };
|
|
2264
2486
|
}
|
|
2265
2487
|
async function interactiveUninstall(installedSlugs) {
|
|
2266
2488
|
We("localskills uninstall");
|
|
2267
|
-
|
|
2489
|
+
return cancelGuard(await Je({
|
|
2268
2490
|
message: "Which skill would you like to uninstall?",
|
|
2269
2491
|
options: installedSlugs.map((s) => ({
|
|
2270
2492
|
value: s,
|
|
2271
2493
|
label: s
|
|
2272
2494
|
}))
|
|
2273
|
-
});
|
|
2274
|
-
if (Ct(slug)) {
|
|
2275
|
-
Ne("Cancelled.");
|
|
2276
|
-
process.exit(0);
|
|
2277
|
-
}
|
|
2278
|
-
return slug;
|
|
2495
|
+
}));
|
|
2279
2496
|
}
|
|
2280
2497
|
function truncate(str, max) {
|
|
2281
2498
|
if (str.length <= max) return str;
|
|
@@ -2311,6 +2528,19 @@ function parsePlatforms(raw) {
|
|
|
2311
2528
|
}
|
|
2312
2529
|
return platforms;
|
|
2313
2530
|
}
|
|
2531
|
+
function buildSkillRecord(cacheKey, skill, version, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
|
|
2532
|
+
return {
|
|
2533
|
+
slug: cacheKey,
|
|
2534
|
+
name: skill.name,
|
|
2535
|
+
type: skill.type ?? "skill",
|
|
2536
|
+
hash: skill.contentHash,
|
|
2537
|
+
version,
|
|
2538
|
+
semver: resolvedSemver ?? null,
|
|
2539
|
+
semverRange: requestedRange ?? null,
|
|
2540
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2541
|
+
installations: existingInstallations ? [...existingInstallations, ...newInstallations] : newInstallations
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2314
2544
|
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
2545
|
async (slugArg, opts) => {
|
|
2316
2546
|
const client = new ApiClient();
|
|
@@ -2363,10 +2593,17 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2363
2593
|
method = explicitMethod || answers.method;
|
|
2364
2594
|
}
|
|
2365
2595
|
}
|
|
2596
|
+
let requestedRange = null;
|
|
2597
|
+
if (slug.includes("@")) {
|
|
2598
|
+
const atIdx = slug.lastIndexOf("@");
|
|
2599
|
+
requestedRange = slug.substring(atIdx + 1);
|
|
2600
|
+
slug = slug.substring(0, atIdx);
|
|
2601
|
+
}
|
|
2602
|
+
const versionQuery = buildVersionQuery(requestedRange);
|
|
2366
2603
|
const spinner = bt2();
|
|
2367
2604
|
spinner.start(`Fetching ${slug}...`);
|
|
2368
2605
|
const res = await client.get(
|
|
2369
|
-
`/api/skills/${encodeURIComponent(slug)}/content`
|
|
2606
|
+
`/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
|
|
2370
2607
|
);
|
|
2371
2608
|
if (!res.success || !res.data) {
|
|
2372
2609
|
spinner.stop("Failed.");
|
|
@@ -2378,9 +2615,62 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2378
2615
|
process.exit(1);
|
|
2379
2616
|
return;
|
|
2380
2617
|
}
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
const cacheKey = skill.publicId || slug;
|
|
2618
|
+
const resData = res.data;
|
|
2619
|
+
const format = resData.format ?? "text";
|
|
2620
|
+
const cacheKey = resData.skill.publicId || slug;
|
|
2621
|
+
if (format === "package") {
|
|
2622
|
+
const { skill: skill2, downloadUrl, manifest, version: version2, semver: resolvedSemver2 } = resData;
|
|
2623
|
+
spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version2)} (package, ${manifest.files.length} files)`);
|
|
2624
|
+
const dlSpinner = bt2();
|
|
2625
|
+
dlSpinner.start("Downloading package...");
|
|
2626
|
+
const zipBuffer = await client.fetchBinary(downloadUrl);
|
|
2627
|
+
dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
|
|
2628
|
+
storePackage(cacheKey, zipBuffer, manifest, skill2, version2);
|
|
2629
|
+
const installations2 = [];
|
|
2630
|
+
const results2 = [];
|
|
2631
|
+
for (const platformId of platforms) {
|
|
2632
|
+
const adapter = getAdapter(platformId);
|
|
2633
|
+
const desc = adapter.descriptor;
|
|
2634
|
+
if (scope === "global" && !desc.supportsGlobal) {
|
|
2635
|
+
R2.warn(`${desc.name} does not support global \u2014 skipping.`);
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
if (scope === "project" && !desc.supportsProject) {
|
|
2639
|
+
R2.warn(`${desc.name} does not support project \u2014 skipping.`);
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
const targetPath = adapter.resolvePath(cacheKey, scope, projectDir, skill2.type ?? "skill");
|
|
2643
|
+
mkdirSync7(targetPath, { recursive: true });
|
|
2644
|
+
const written = extractPackage(zipBuffer, targetPath);
|
|
2645
|
+
const installation = {
|
|
2646
|
+
platform: platformId,
|
|
2647
|
+
scope,
|
|
2648
|
+
method: "copy",
|
|
2649
|
+
path: targetPath,
|
|
2650
|
+
projectDir,
|
|
2651
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2652
|
+
};
|
|
2653
|
+
installations2.push(installation);
|
|
2654
|
+
results2.push(`${desc.name} \u2192 ${targetPath} (${written.length} files extracted)`);
|
|
2655
|
+
}
|
|
2656
|
+
config.installed_skills[cacheKey] = buildSkillRecord(
|
|
2657
|
+
cacheKey,
|
|
2658
|
+
skill2,
|
|
2659
|
+
version2,
|
|
2660
|
+
resolvedSemver2,
|
|
2661
|
+
requestedRange,
|
|
2662
|
+
config.installed_skills[cacheKey]?.installations,
|
|
2663
|
+
installations2
|
|
2664
|
+
);
|
|
2665
|
+
saveConfig(config);
|
|
2666
|
+
for (const r of results2) {
|
|
2667
|
+
R2.success(r);
|
|
2668
|
+
}
|
|
2669
|
+
Le(`Done! Installed to ${installations2.length} target(s).`);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const { skill, content, version, semver: resolvedSemver } = resData;
|
|
2673
|
+
spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version)}`);
|
|
2384
2674
|
store(cacheKey, content, skill, version);
|
|
2385
2675
|
const contentType = skill.type ?? "skill";
|
|
2386
2676
|
const installations = [];
|
|
@@ -2420,17 +2710,15 @@ var installCommand = new Command2("install").description("Install a skill locall
|
|
|
2420
2710
|
const methodLabel = actualMethod === "symlink" ? "symlinked" : actualMethod === "section" ? "section" : "copied";
|
|
2421
2711
|
results.push(`${desc.name} \u2192 ${installedPath} (${methodLabel})`);
|
|
2422
2712
|
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
name: skill.name,
|
|
2427
|
-
type: contentType,
|
|
2428
|
-
hash: skill.contentHash,
|
|
2713
|
+
config.installed_skills[cacheKey] = buildSkillRecord(
|
|
2714
|
+
cacheKey,
|
|
2715
|
+
skill,
|
|
2429
2716
|
version,
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2717
|
+
resolvedSemver,
|
|
2718
|
+
requestedRange,
|
|
2719
|
+
config.installed_skills[cacheKey]?.installations,
|
|
2720
|
+
installations
|
|
2721
|
+
);
|
|
2434
2722
|
saveConfig(config);
|
|
2435
2723
|
for (const r of results) {
|
|
2436
2724
|
R2.success(r);
|
|
@@ -2489,33 +2777,62 @@ var uninstallCommand = new Command3("uninstall").description("Uninstall a skill"
|
|
|
2489
2777
|
|
|
2490
2778
|
// src/commands/list.ts
|
|
2491
2779
|
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) => {
|
|
2780
|
+
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
2781
|
const client = new ApiClient();
|
|
2494
|
-
if (!
|
|
2495
|
-
console.error("
|
|
2782
|
+
if ((opts.tag || opts.search) && !opts.public) {
|
|
2783
|
+
console.error("The --tag and --search flags require --public.");
|
|
2496
2784
|
process.exit(1);
|
|
2497
2785
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2786
|
+
if (opts.public) {
|
|
2787
|
+
const params = new URLSearchParams();
|
|
2788
|
+
if (opts.tag) params.set("tag", opts.tag);
|
|
2789
|
+
if (opts.search) params.set("q", opts.search);
|
|
2790
|
+
const qs = params.toString();
|
|
2791
|
+
const path = qs ? `/api/explore?${qs}` : "/api/explore";
|
|
2792
|
+
const res = await client.get(path);
|
|
2793
|
+
if (!res.success || !res.data) {
|
|
2794
|
+
console.error(`Error: ${res.error || "Failed to fetch skills"}`);
|
|
2795
|
+
process.exit(1);
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (res.data.length === 0) {
|
|
2799
|
+
console.log("No skills found.");
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
console.log("Public skills:\n");
|
|
2803
|
+
for (const skill of res.data) {
|
|
2804
|
+
const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
|
|
2805
|
+
const tags = skill.tags.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
|
|
2806
|
+
console.log(` ${skill.slug} ${ver}${tags} \u2014 ${skill.description || skill.name}`);
|
|
2807
|
+
}
|
|
2808
|
+
console.log(`
|
|
2515
2809
|
${res.data.length} skill(s) found.`);
|
|
2810
|
+
} else {
|
|
2811
|
+
requireAuth(client);
|
|
2812
|
+
const res = await client.get("/api/skills");
|
|
2813
|
+
if (!res.success || !res.data) {
|
|
2814
|
+
console.error(`Error: ${res.error || "Failed to fetch skills"}`);
|
|
2815
|
+
process.exit(1);
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
if (res.data.length === 0) {
|
|
2819
|
+
console.log("No skills found.");
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
console.log("Available skills:\n");
|
|
2823
|
+
for (const skill of res.data) {
|
|
2824
|
+
const vis = skill.visibility === "public" ? "" : ` [${skill.visibility}]`;
|
|
2825
|
+
const ver = formatVersionLabel(skill.currentSemver, skill.currentVersion);
|
|
2826
|
+
const tags = skill.tags?.length > 0 ? ` [${skill.tags.join(", ")}]` : "";
|
|
2827
|
+
console.log(` ${skill.slug} ${ver}${vis}${tags} \u2014 ${skill.description || skill.name}`);
|
|
2828
|
+
}
|
|
2829
|
+
console.log(`
|
|
2830
|
+
${res.data.length} skill(s) found.`);
|
|
2831
|
+
}
|
|
2516
2832
|
});
|
|
2517
2833
|
|
|
2518
2834
|
// src/commands/pull.ts
|
|
2835
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
2519
2836
|
import { Command as Command5 } from "commander";
|
|
2520
2837
|
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
2838
|
const config = loadConfig();
|
|
@@ -2535,53 +2852,70 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
|
|
|
2535
2852
|
R2.warn(`${slug} \u2014 not found in config, skipping.`);
|
|
2536
2853
|
continue;
|
|
2537
2854
|
}
|
|
2855
|
+
const versionQuery = buildVersionQuery(installed.semverRange);
|
|
2538
2856
|
spinner.start(`Checking ${slug}...`);
|
|
2539
2857
|
const res = await client.get(
|
|
2540
|
-
`/api/skills/${encodeURIComponent(slug)}/content`
|
|
2858
|
+
`/api/skills/${encodeURIComponent(slug)}/content${versionQuery}`
|
|
2541
2859
|
);
|
|
2542
2860
|
if (!res.success || !res.data) {
|
|
2543
2861
|
spinner.stop(`${slug} \u2014 failed: ${res.error || "not found"}`);
|
|
2544
2862
|
continue;
|
|
2545
2863
|
}
|
|
2546
|
-
const
|
|
2864
|
+
const resData = res.data;
|
|
2865
|
+
const format = resData.format ?? "text";
|
|
2866
|
+
const { skill, version } = resData;
|
|
2547
2867
|
if (skill.contentHash === installed.hash) {
|
|
2548
2868
|
spinner.stop(`${slug} \u2014 up to date`);
|
|
2549
2869
|
skipped++;
|
|
2550
2870
|
continue;
|
|
2551
2871
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2872
|
+
if (format === "package") {
|
|
2873
|
+
const { downloadUrl, manifest } = resData;
|
|
2874
|
+
const zipBuffer = await client.fetchBinary(downloadUrl);
|
|
2875
|
+
storePackage(slug, zipBuffer, manifest, skill, version);
|
|
2876
|
+
for (const installation of installed.installations) {
|
|
2877
|
+
const adapter = getAdapter(installation.platform);
|
|
2878
|
+
const targetPath = adapter.resolvePath(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
|
|
2879
|
+
mkdirSync8(targetPath, { recursive: true });
|
|
2880
|
+
extractPackage(zipBuffer, targetPath);
|
|
2557
2881
|
}
|
|
2558
|
-
|
|
2559
|
-
const
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
slug,
|
|
2564
|
-
|
|
2882
|
+
} else {
|
|
2883
|
+
const { content } = resData;
|
|
2884
|
+
store(slug, content, skill, version);
|
|
2885
|
+
for (const installation of installed.installations) {
|
|
2886
|
+
if (installation.method === "symlink") {
|
|
2887
|
+
getPlatformFile(slug, installation.platform, skill);
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
const adapter = getAdapter(installation.platform);
|
|
2891
|
+
const transformed = adapter.transformContent(content, skill);
|
|
2892
|
+
if (installation.method === "section") {
|
|
2893
|
+
upsertSection(
|
|
2894
|
+
installation.path,
|
|
2895
|
+
slug,
|
|
2896
|
+
`## ${slug}
|
|
2565
2897
|
|
|
2566
2898
|
${transformed}`
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2899
|
+
);
|
|
2900
|
+
} else {
|
|
2901
|
+
const cachePath = getPlatformFile(slug, installation.platform, skill);
|
|
2902
|
+
adapter.install({
|
|
2903
|
+
slug,
|
|
2904
|
+
content: transformed,
|
|
2905
|
+
scope: installation.scope,
|
|
2906
|
+
method: "copy",
|
|
2907
|
+
cachePath,
|
|
2908
|
+
projectDir: installation.projectDir
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2578
2911
|
}
|
|
2579
2912
|
}
|
|
2580
2913
|
installed.hash = skill.contentHash;
|
|
2581
2914
|
installed.version = version;
|
|
2915
|
+
installed.semver = resData.semver ?? null;
|
|
2582
2916
|
installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2583
2917
|
updated++;
|
|
2584
|
-
spinner.stop(`${slug} \u2014 updated to
|
|
2918
|
+
spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version)}`);
|
|
2585
2919
|
}
|
|
2586
2920
|
saveConfig(config);
|
|
2587
2921
|
Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
|
|
@@ -2589,44 +2923,44 @@ ${transformed}`
|
|
|
2589
2923
|
|
|
2590
2924
|
// src/commands/publish.ts
|
|
2591
2925
|
import { Command as Command6 } from "commander";
|
|
2592
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
2593
|
-
import { resolve as
|
|
2926
|
+
import { readFileSync as readFileSync6, existsSync as existsSync15 } from "fs";
|
|
2927
|
+
import { resolve as resolve4, basename as basename2, extname as extname2 } from "path";
|
|
2594
2928
|
import { homedir as homedir10 } from "os";
|
|
2595
2929
|
|
|
2596
2930
|
// src/lib/scanner.ts
|
|
2597
|
-
import { existsSync as
|
|
2598
|
-
import { join as
|
|
2931
|
+
import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
2932
|
+
import { join as join14, basename, extname } from "path";
|
|
2599
2933
|
import { homedir as homedir9 } from "os";
|
|
2600
|
-
import { readlinkSync
|
|
2934
|
+
import { readlinkSync, lstatSync as lstatSync2 } from "fs";
|
|
2601
2935
|
function scanForSkills(projectDir) {
|
|
2602
2936
|
const home = homedir9();
|
|
2603
2937
|
const cwd = projectDir || process.cwd();
|
|
2604
2938
|
const results = [];
|
|
2605
|
-
scanDirectory(
|
|
2606
|
-
scanDirectory(
|
|
2607
|
-
scanClaudeSkills(
|
|
2608
|
-
scanClaudeSkills(
|
|
2609
|
-
scanDirectory(
|
|
2610
|
-
scanDirectory(
|
|
2611
|
-
scanSingleFile(
|
|
2612
|
-
scanSingleFile(
|
|
2939
|
+
scanDirectory(join14(home, ".cursor", "rules"), ".mdc", "cursor", "global", results);
|
|
2940
|
+
scanDirectory(join14(cwd, ".cursor", "rules"), ".mdc", "cursor", "project", results);
|
|
2941
|
+
scanClaudeSkills(join14(home, ".claude", "skills"), "global", results);
|
|
2942
|
+
scanClaudeSkills(join14(cwd, ".claude", "skills"), "project", results);
|
|
2943
|
+
scanDirectory(join14(home, ".claude", "rules"), ".md", "claude", "global", results, "rule");
|
|
2944
|
+
scanDirectory(join14(cwd, ".claude", "rules"), ".md", "claude", "project", results, "rule");
|
|
2945
|
+
scanSingleFile(join14(home, ".codex", "AGENTS.md"), "codex", "global", results);
|
|
2946
|
+
scanSingleFile(join14(cwd, "AGENTS.md"), "codex", "project", results);
|
|
2613
2947
|
scanSingleFile(
|
|
2614
|
-
|
|
2948
|
+
join14(home, ".codeium", "windsurf", "memories", "global_rules.md"),
|
|
2615
2949
|
"windsurf",
|
|
2616
2950
|
"global",
|
|
2617
2951
|
results
|
|
2618
2952
|
);
|
|
2619
|
-
scanDirectory(
|
|
2620
|
-
scanDirectory(
|
|
2953
|
+
scanDirectory(join14(cwd, ".windsurf", "rules"), ".md", "windsurf", "project", results);
|
|
2954
|
+
scanDirectory(join14(cwd, ".clinerules"), ".md", "cline", "project", results);
|
|
2621
2955
|
scanSingleFile(
|
|
2622
|
-
|
|
2956
|
+
join14(cwd, ".github", "copilot-instructions.md"),
|
|
2623
2957
|
"copilot",
|
|
2624
2958
|
"project",
|
|
2625
2959
|
results
|
|
2626
2960
|
);
|
|
2627
|
-
scanDirectory(
|
|
2628
|
-
scanDirectory(
|
|
2629
|
-
scanDirectory(
|
|
2961
|
+
scanDirectory(join14(home, ".config", "opencode", "rules"), ".md", "opencode", "global", results);
|
|
2962
|
+
scanDirectory(join14(cwd, ".opencode", "rules"), ".md", "opencode", "project", results);
|
|
2963
|
+
scanDirectory(join14(cwd, ".aider", "skills"), ".md", "aider", "project", results);
|
|
2630
2964
|
return results;
|
|
2631
2965
|
}
|
|
2632
2966
|
function filterTracked(detected, config) {
|
|
@@ -2636,13 +2970,13 @@ function filterTracked(detected, config) {
|
|
|
2636
2970
|
trackedPaths.add(inst.path);
|
|
2637
2971
|
}
|
|
2638
2972
|
}
|
|
2639
|
-
const cacheDir =
|
|
2973
|
+
const cacheDir = join14(homedir9(), ".localskills", "cache");
|
|
2640
2974
|
return detected.filter((skill) => {
|
|
2641
2975
|
if (trackedPaths.has(skill.filePath)) return false;
|
|
2642
2976
|
try {
|
|
2643
2977
|
const stat = lstatSync2(skill.filePath);
|
|
2644
2978
|
if (stat.isSymbolicLink()) {
|
|
2645
|
-
const target =
|
|
2979
|
+
const target = readlinkSync(skill.filePath);
|
|
2646
2980
|
if (target.startsWith(cacheDir)) return false;
|
|
2647
2981
|
}
|
|
2648
2982
|
} catch {
|
|
@@ -2653,11 +2987,9 @@ function filterTracked(detected, config) {
|
|
|
2653
2987
|
function slugFromFilename(filename) {
|
|
2654
2988
|
return basename(filename, extname(filename));
|
|
2655
2989
|
}
|
|
2656
|
-
|
|
2657
|
-
return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2658
|
-
}
|
|
2990
|
+
var nameFromSlug = titleFromSlug;
|
|
2659
2991
|
function scanDirectory(dir, ext, platform, scope, results, contentType = "skill") {
|
|
2660
|
-
if (!
|
|
2992
|
+
if (!existsSync14(dir)) return;
|
|
2661
2993
|
let entries;
|
|
2662
2994
|
try {
|
|
2663
2995
|
entries = readdirSync2(dir);
|
|
@@ -2666,7 +2998,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
2666
2998
|
}
|
|
2667
2999
|
for (const entry of entries) {
|
|
2668
3000
|
if (!entry.endsWith(ext)) continue;
|
|
2669
|
-
const filePath =
|
|
3001
|
+
const filePath = join14(dir, entry);
|
|
2670
3002
|
try {
|
|
2671
3003
|
const raw = readFileSync5(filePath, "utf-8");
|
|
2672
3004
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -2686,7 +3018,7 @@ function scanDirectory(dir, ext, platform, scope, results, contentType = "skill"
|
|
|
2686
3018
|
}
|
|
2687
3019
|
}
|
|
2688
3020
|
function scanClaudeSkills(skillsDir, scope, results) {
|
|
2689
|
-
if (!
|
|
3021
|
+
if (!existsSync14(skillsDir)) return;
|
|
2690
3022
|
let entries;
|
|
2691
3023
|
try {
|
|
2692
3024
|
entries = readdirSync2(skillsDir);
|
|
@@ -2694,8 +3026,8 @@ function scanClaudeSkills(skillsDir, scope, results) {
|
|
|
2694
3026
|
return;
|
|
2695
3027
|
}
|
|
2696
3028
|
for (const entry of entries) {
|
|
2697
|
-
const skillFile =
|
|
2698
|
-
if (!
|
|
3029
|
+
const skillFile = join14(skillsDir, entry, "SKILL.md");
|
|
3030
|
+
if (!existsSync14(skillFile)) continue;
|
|
2699
3031
|
try {
|
|
2700
3032
|
const raw = readFileSync5(skillFile, "utf-8");
|
|
2701
3033
|
const content = stripFrontmatter(raw).trim();
|
|
@@ -2714,7 +3046,7 @@ function scanClaudeSkills(skillsDir, scope, results) {
|
|
|
2714
3046
|
}
|
|
2715
3047
|
}
|
|
2716
3048
|
function scanSingleFile(filePath, platform, scope, results) {
|
|
2717
|
-
if (!
|
|
3049
|
+
if (!existsSync14(filePath)) return;
|
|
2718
3050
|
let raw;
|
|
2719
3051
|
try {
|
|
2720
3052
|
raw = readFileSync5(filePath, "utf-8");
|
|
@@ -2765,10 +3097,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2765
3097
|
).option("--type <type>", "Content type: skill or rule", "skill").option("-m, --message <message>", "Version message").action(
|
|
2766
3098
|
async (fileArg, opts) => {
|
|
2767
3099
|
const client = new ApiClient();
|
|
2768
|
-
|
|
2769
|
-
console.error("Not authenticated. Run `localskills login` first.");
|
|
2770
|
-
process.exit(1);
|
|
2771
|
-
}
|
|
3100
|
+
requireAuth(client);
|
|
2772
3101
|
const teamsRes = await client.get("/api/tenants");
|
|
2773
3102
|
if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
|
|
2774
3103
|
console.error(
|
|
@@ -2779,8 +3108,8 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2779
3108
|
}
|
|
2780
3109
|
const teams = teamsRes.data;
|
|
2781
3110
|
if (fileArg) {
|
|
2782
|
-
const filePath =
|
|
2783
|
-
if (!
|
|
3111
|
+
const filePath = resolve4(fileArg);
|
|
3112
|
+
if (!existsSync15(filePath)) {
|
|
2784
3113
|
console.error(`File not found: ${filePath}`);
|
|
2785
3114
|
process.exit(1);
|
|
2786
3115
|
return;
|
|
@@ -2793,7 +3122,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2793
3122
|
return;
|
|
2794
3123
|
}
|
|
2795
3124
|
const defaultSlug = basename2(filePath, extname2(filePath));
|
|
2796
|
-
const defaultName = defaultSlug
|
|
3125
|
+
const defaultName = titleFromSlug(defaultSlug);
|
|
2797
3126
|
const skillName = opts.name || defaultName;
|
|
2798
3127
|
const contentType = validateContentType(opts.type || "skill");
|
|
2799
3128
|
const visibility = validateVisibility(opts.visibility || "private");
|
|
@@ -2819,7 +3148,7 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2819
3148
|
Le("Nothing to publish.");
|
|
2820
3149
|
return;
|
|
2821
3150
|
}
|
|
2822
|
-
const
|
|
3151
|
+
const skills = cancelGuard(await je({
|
|
2823
3152
|
message: "Select items to publish",
|
|
2824
3153
|
options: detected.map((s) => ({
|
|
2825
3154
|
value: s,
|
|
@@ -2827,28 +3156,19 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2827
3156
|
hint: `${s.platform}/${s.scope}/${s.contentType} ${shortenPath(s.filePath)}`
|
|
2828
3157
|
})),
|
|
2829
3158
|
required: true
|
|
2830
|
-
});
|
|
2831
|
-
if (Ct(selected)) {
|
|
2832
|
-
Ne("Cancelled.");
|
|
2833
|
-
process.exit(0);
|
|
2834
|
-
}
|
|
2835
|
-
const skills = selected;
|
|
3159
|
+
}));
|
|
2836
3160
|
const tenantId = await resolveTeam(teams, opts.team);
|
|
2837
3161
|
for (const skill of skills) {
|
|
2838
3162
|
R2.step(`Publishing ${skill.suggestedName}...`);
|
|
2839
|
-
const name = await Ze({
|
|
3163
|
+
const name = cancelGuard(await Ze({
|
|
2840
3164
|
message: "Skill name?",
|
|
2841
3165
|
initialValue: skill.suggestedName,
|
|
2842
3166
|
validate: (v) => {
|
|
2843
3167
|
if (!v || v.length < 1) return "Name is required";
|
|
2844
3168
|
if (v.length > 100) return "Name must be 100 characters or less";
|
|
2845
3169
|
}
|
|
2846
|
-
});
|
|
2847
|
-
|
|
2848
|
-
Ne("Cancelled.");
|
|
2849
|
-
process.exit(0);
|
|
2850
|
-
}
|
|
2851
|
-
const visibility = await Je({
|
|
3170
|
+
}));
|
|
3171
|
+
const visibility = cancelGuard(await Je({
|
|
2852
3172
|
message: "Visibility?",
|
|
2853
3173
|
options: [
|
|
2854
3174
|
{ value: "private", label: "Private", hint: "Only team members" },
|
|
@@ -2856,23 +3176,15 @@ var publishCommand = new Command6("publish").description("Publish local skill fi
|
|
|
2856
3176
|
{ value: "unlisted", label: "Unlisted", hint: "Accessible via direct link" }
|
|
2857
3177
|
],
|
|
2858
3178
|
initialValue: "private"
|
|
2859
|
-
});
|
|
2860
|
-
|
|
2861
|
-
Ne("Cancelled.");
|
|
2862
|
-
process.exit(0);
|
|
2863
|
-
}
|
|
2864
|
-
const contentType = await Je({
|
|
3179
|
+
}));
|
|
3180
|
+
const contentType = cancelGuard(await Je({
|
|
2865
3181
|
message: "Type?",
|
|
2866
3182
|
options: [
|
|
2867
3183
|
{ value: "skill", label: "Skill", hint: "Reusable agent instructions" },
|
|
2868
3184
|
{ value: "rule", label: "Rule", hint: "Governance constraints" }
|
|
2869
3185
|
],
|
|
2870
3186
|
initialValue: skill.contentType
|
|
2871
|
-
});
|
|
2872
|
-
if (Ct(contentType)) {
|
|
2873
|
-
Ne("Cancelled.");
|
|
2874
|
-
process.exit(0);
|
|
2875
|
-
}
|
|
3187
|
+
}));
|
|
2876
3188
|
await uploadSkill(client, {
|
|
2877
3189
|
name,
|
|
2878
3190
|
content: skill.content,
|
|
@@ -2897,19 +3209,14 @@ async function resolveTeam(teams, teamFlag) {
|
|
|
2897
3209
|
if (teams.length === 1) {
|
|
2898
3210
|
return teams[0].id;
|
|
2899
3211
|
}
|
|
2900
|
-
|
|
3212
|
+
return cancelGuard(await Je({
|
|
2901
3213
|
message: "Which team?",
|
|
2902
3214
|
options: teams.map((t) => ({
|
|
2903
3215
|
value: t.id,
|
|
2904
3216
|
label: t.name,
|
|
2905
3217
|
hint: t.slug
|
|
2906
3218
|
}))
|
|
2907
|
-
});
|
|
2908
|
-
if (Ct(selected)) {
|
|
2909
|
-
Ne("Cancelled.");
|
|
2910
|
-
process.exit(0);
|
|
2911
|
-
}
|
|
2912
|
-
return selected;
|
|
3219
|
+
}));
|
|
2913
3220
|
}
|
|
2914
3221
|
async function uploadSkill(client, params) {
|
|
2915
3222
|
const spinner = bt2();
|
|
@@ -2954,9 +3261,316 @@ function shortenPath(filePath) {
|
|
|
2954
3261
|
return filePath;
|
|
2955
3262
|
}
|
|
2956
3263
|
|
|
3264
|
+
// src/commands/push.ts
|
|
3265
|
+
import { Command as Command7 } from "commander";
|
|
3266
|
+
import { readFileSync as readFileSync7, existsSync as existsSync16 } from "fs";
|
|
3267
|
+
import { resolve as resolve5 } from "path";
|
|
3268
|
+
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(
|
|
3269
|
+
async (fileArg, opts) => {
|
|
3270
|
+
const client = new ApiClient();
|
|
3271
|
+
requireAuth(client);
|
|
3272
|
+
const filePath = resolve5(fileArg);
|
|
3273
|
+
if (!existsSync16(filePath)) {
|
|
3274
|
+
console.error(`File not found: ${filePath}`);
|
|
3275
|
+
process.exit(1);
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3279
|
+
const content = stripFrontmatter(raw).trim();
|
|
3280
|
+
if (!content) {
|
|
3281
|
+
console.error("File is empty after stripping frontmatter.");
|
|
3282
|
+
process.exit(1);
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
const bumpFlags = [opts.patch, opts.minor, opts.major].filter(Boolean);
|
|
3286
|
+
if (opts.version && bumpFlags.length > 0) {
|
|
3287
|
+
console.error("Cannot specify both --version and --patch/--minor/--major");
|
|
3288
|
+
process.exit(1);
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
let body = {
|
|
3292
|
+
content,
|
|
3293
|
+
message: opts.message
|
|
3294
|
+
};
|
|
3295
|
+
if (opts.version) {
|
|
3296
|
+
if (!isValidSemVer(opts.version)) {
|
|
3297
|
+
console.error(`Invalid semver format: ${opts.version}. Expected X.Y.Z`);
|
|
3298
|
+
process.exit(1);
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
body.semver = opts.version;
|
|
3302
|
+
} else if (opts.major) {
|
|
3303
|
+
body.bump = "major";
|
|
3304
|
+
} else if (opts.minor) {
|
|
3305
|
+
body.bump = "minor";
|
|
3306
|
+
} else if (opts.patch) {
|
|
3307
|
+
body.bump = "patch";
|
|
3308
|
+
}
|
|
3309
|
+
const spinner = bt2();
|
|
3310
|
+
spinner.start("Pushing new version...");
|
|
3311
|
+
const res = await client.post(
|
|
3312
|
+
`/api/skills/${encodeURIComponent(opts.skill)}/versions`,
|
|
3313
|
+
body
|
|
3314
|
+
);
|
|
3315
|
+
if (!res.success || !res.data) {
|
|
3316
|
+
spinner.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3317
|
+
process.exit(1);
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
const v = res.data;
|
|
3321
|
+
spinner.stop(`Pushed ${formatVersionLabel(v.semver, v.version)}`);
|
|
3322
|
+
Le("Done!");
|
|
3323
|
+
}
|
|
3324
|
+
);
|
|
3325
|
+
|
|
3326
|
+
// src/commands/share.ts
|
|
3327
|
+
import { Command as Command8 } from "commander";
|
|
3328
|
+
import { readFileSync as readFileSync8, existsSync as existsSync17 } from "fs";
|
|
3329
|
+
import { resolve as resolve6, basename as basename3, extname as extname3 } from "path";
|
|
3330
|
+
import { generateKeyPairSync } from "crypto";
|
|
3331
|
+
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) => {
|
|
3332
|
+
We("localskills share");
|
|
3333
|
+
await ensureAnonymousIdentity();
|
|
3334
|
+
const client = new ApiClient();
|
|
3335
|
+
if (fileArg) {
|
|
3336
|
+
const filePath = resolve6(fileArg);
|
|
3337
|
+
if (!existsSync17(filePath)) {
|
|
3338
|
+
R2.error(`File not found: ${filePath}`);
|
|
3339
|
+
process.exit(1);
|
|
3340
|
+
}
|
|
3341
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
3342
|
+
const content = stripFrontmatter(raw).trim();
|
|
3343
|
+
if (!content) {
|
|
3344
|
+
R2.error("File is empty after stripping frontmatter.");
|
|
3345
|
+
process.exit(1);
|
|
3346
|
+
}
|
|
3347
|
+
const defaultSlug = basename3(filePath, extname3(filePath));
|
|
3348
|
+
const defaultName = titleFromSlug(defaultSlug);
|
|
3349
|
+
const skillName = opts.name || defaultName;
|
|
3350
|
+
const contentType = opts.type === "rule" ? "rule" : "skill";
|
|
3351
|
+
await uploadAnonymousSkill(client, { name: skillName, content, type: contentType });
|
|
3352
|
+
} else {
|
|
3353
|
+
const spinner = bt2();
|
|
3354
|
+
spinner.start("Scanning for skills...");
|
|
3355
|
+
const config = loadConfig();
|
|
3356
|
+
const allDetected = scanForSkills();
|
|
3357
|
+
const detected = filterTracked(allDetected, config);
|
|
3358
|
+
spinner.stop(
|
|
3359
|
+
detected.length > 0 ? `Found ${detected.length} skill file${detected.length !== 1 ? "s" : ""}.` : "No skill files found."
|
|
3360
|
+
);
|
|
3361
|
+
if (detected.length === 0) {
|
|
3362
|
+
Le("Nothing to share. Pass a file path: localskills share <file>");
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
const selected = cancelGuard(
|
|
3366
|
+
await Je({
|
|
3367
|
+
message: "Select a skill to share",
|
|
3368
|
+
options: detected.map((s) => ({
|
|
3369
|
+
value: s,
|
|
3370
|
+
label: s.suggestedName,
|
|
3371
|
+
hint: `${s.platform} \xB7 ${s.contentType}`
|
|
3372
|
+
}))
|
|
3373
|
+
})
|
|
3374
|
+
);
|
|
3375
|
+
const name = cancelGuard(
|
|
3376
|
+
await Ze({
|
|
3377
|
+
message: "Skill name?",
|
|
3378
|
+
initialValue: selected.suggestedName,
|
|
3379
|
+
validate: (v) => {
|
|
3380
|
+
if (!v || v.length < 1) return "Name is required";
|
|
3381
|
+
if (v.length > 100) return "Name must be 100 characters or less";
|
|
3382
|
+
}
|
|
3383
|
+
})
|
|
3384
|
+
);
|
|
3385
|
+
await uploadAnonymousSkill(client, {
|
|
3386
|
+
name,
|
|
3387
|
+
content: selected.content,
|
|
3388
|
+
type: selected.contentType
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
Le("Done!");
|
|
3392
|
+
});
|
|
3393
|
+
async function ensureAnonymousIdentity() {
|
|
3394
|
+
const config = loadConfig();
|
|
3395
|
+
if (config.token) {
|
|
3396
|
+
const client = new ApiClient();
|
|
3397
|
+
const res2 = await client.get("/api/cli/auth");
|
|
3398
|
+
if (res2.success) return;
|
|
3399
|
+
}
|
|
3400
|
+
let keyPair = getAnonymousKey();
|
|
3401
|
+
if (!keyPair) {
|
|
3402
|
+
const s2 = bt2();
|
|
3403
|
+
s2.start("Generating anonymous identity...");
|
|
3404
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
3405
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
3406
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" }
|
|
3407
|
+
});
|
|
3408
|
+
const rawPubKey = publicKey.subarray(publicKey.length - 32);
|
|
3409
|
+
const rawPrivKey = privateKey.subarray(privateKey.length - 32);
|
|
3410
|
+
keyPair = {
|
|
3411
|
+
publicKey: rawPubKey.toString("base64"),
|
|
3412
|
+
privateKey: rawPrivKey.toString("base64")
|
|
3413
|
+
};
|
|
3414
|
+
setAnonymousKey(keyPair);
|
|
3415
|
+
s2.stop("Identity created.");
|
|
3416
|
+
}
|
|
3417
|
+
const s = bt2();
|
|
3418
|
+
s.start("Connecting to localskills.sh...");
|
|
3419
|
+
const tempClient = new ApiClient();
|
|
3420
|
+
const res = await tempClient.post("/api/cli/auth/anonymous", {
|
|
3421
|
+
publicKey: keyPair.publicKey,
|
|
3422
|
+
algorithm: "Ed25519"
|
|
3423
|
+
});
|
|
3424
|
+
if (!res.success || !res.data) {
|
|
3425
|
+
s.stop(`Registration failed: ${res.error || "Unknown error"}`);
|
|
3426
|
+
process.exit(1);
|
|
3427
|
+
}
|
|
3428
|
+
setToken(res.data.token);
|
|
3429
|
+
s.stop(`Connected as ${res.data.username}`);
|
|
3430
|
+
}
|
|
3431
|
+
async function uploadAnonymousSkill(client, params) {
|
|
3432
|
+
const s = bt2();
|
|
3433
|
+
s.start(`Sharing "${params.name}"...`);
|
|
3434
|
+
const teamsRes = await client.get("/api/tenants");
|
|
3435
|
+
if (!teamsRes.success || !teamsRes.data || teamsRes.data.length === 0) {
|
|
3436
|
+
s.stop("Failed to find your team. Try running `localskills share` again.");
|
|
3437
|
+
process.exit(1);
|
|
3438
|
+
}
|
|
3439
|
+
const tenantId = teamsRes.data[0].id;
|
|
3440
|
+
const res = await client.post("/api/skills", {
|
|
3441
|
+
name: params.name,
|
|
3442
|
+
content: params.content,
|
|
3443
|
+
tenantId,
|
|
3444
|
+
visibility: "unlisted",
|
|
3445
|
+
type: params.type
|
|
3446
|
+
});
|
|
3447
|
+
if (!res.success || !res.data) {
|
|
3448
|
+
s.stop(`Failed: ${res.error || "Unknown error"}`);
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
s.stop("Shared!");
|
|
3452
|
+
R2.success(`URL: https://localskills.sh/s/${res.data.publicId}`);
|
|
3453
|
+
R2.info(`Install: localskills install ${res.data.publicId}`);
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// src/commands/profile.ts
|
|
3457
|
+
import { Command as Command9 } from "commander";
|
|
3458
|
+
var PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
3459
|
+
var MAX_PROFILE_NAME_LENGTH = 32;
|
|
3460
|
+
var profileCommand = new Command9("profile").description("Manage CLI profiles for multiple accounts");
|
|
3461
|
+
profileCommand.command("list").description("List all profiles").action(() => {
|
|
3462
|
+
const config = loadFullConfig();
|
|
3463
|
+
const persisted = config.active_profile;
|
|
3464
|
+
let resolved;
|
|
3465
|
+
try {
|
|
3466
|
+
resolved = getActiveProfileName();
|
|
3467
|
+
} catch (err) {
|
|
3468
|
+
if (err instanceof ProfileNotFoundError) {
|
|
3469
|
+
console.error(`Warning: ${err.message}
|
|
3470
|
+
`);
|
|
3471
|
+
resolved = persisted;
|
|
3472
|
+
} else {
|
|
3473
|
+
throw err;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
const names = Object.keys(config.profiles);
|
|
3477
|
+
console.log("Profiles:\n");
|
|
3478
|
+
for (const name of names) {
|
|
3479
|
+
const profile = config.profiles[name];
|
|
3480
|
+
let marker = "";
|
|
3481
|
+
if (name === resolved && name === persisted) {
|
|
3482
|
+
marker = " (active)";
|
|
3483
|
+
} else if (name === resolved) {
|
|
3484
|
+
marker = " (active via override)";
|
|
3485
|
+
} else if (name === persisted) {
|
|
3486
|
+
marker = " (default)";
|
|
3487
|
+
}
|
|
3488
|
+
const auth = profile.token ? "authenticated" : "not authenticated";
|
|
3489
|
+
const skillCount = Object.keys(profile.installed_skills).length;
|
|
3490
|
+
console.log(` ${name}${marker} \u2014 ${auth}, ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
profileCommand.command("create <name>").description("Create a new profile").action((name) => {
|
|
3494
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
3495
|
+
console.error(
|
|
3496
|
+
`Error: Invalid profile name "${name}". Profile names must use lowercase letters, numbers, and hyphens, and must start with a letter or number.`
|
|
3497
|
+
);
|
|
3498
|
+
process.exit(1);
|
|
3499
|
+
}
|
|
3500
|
+
if (name.length > MAX_PROFILE_NAME_LENGTH) {
|
|
3501
|
+
console.error(`Error: Profile name must be ${MAX_PROFILE_NAME_LENGTH} characters or fewer.`);
|
|
3502
|
+
process.exit(1);
|
|
3503
|
+
}
|
|
3504
|
+
const config = loadFullConfig();
|
|
3505
|
+
if (config.profiles[name]) {
|
|
3506
|
+
console.error(`Error: Profile "${name}" already exists.`);
|
|
3507
|
+
process.exit(1);
|
|
3508
|
+
}
|
|
3509
|
+
config.profiles[name] = {
|
|
3510
|
+
token: null,
|
|
3511
|
+
installed_skills: {},
|
|
3512
|
+
defaults: {
|
|
3513
|
+
scope: "project",
|
|
3514
|
+
method: "symlink"
|
|
3515
|
+
},
|
|
3516
|
+
anonymous_key: null
|
|
3517
|
+
};
|
|
3518
|
+
saveFullConfig(config);
|
|
3519
|
+
console.log(`Profile "${name}" created.`);
|
|
3520
|
+
});
|
|
3521
|
+
profileCommand.command("switch <name>").description("Switch the active profile").action((name) => {
|
|
3522
|
+
const config = loadFullConfig();
|
|
3523
|
+
if (!config.profiles[name]) {
|
|
3524
|
+
console.error(`Error: Profile "${name}" does not exist.`);
|
|
3525
|
+
console.error(`Available profiles: ${Object.keys(config.profiles).join(", ")}`);
|
|
3526
|
+
process.exit(1);
|
|
3527
|
+
}
|
|
3528
|
+
config.active_profile = name;
|
|
3529
|
+
saveFullConfig(config);
|
|
3530
|
+
console.log(`Switched to profile "${name}".`);
|
|
3531
|
+
});
|
|
3532
|
+
profileCommand.command("delete <name>").description("Delete a profile").option("-f, --force", "Skip confirmation prompt").action(async (name, opts) => {
|
|
3533
|
+
if (name === DEFAULT_PROFILE_NAME) {
|
|
3534
|
+
console.error(`Error: Cannot delete the "${DEFAULT_PROFILE_NAME}" profile.`);
|
|
3535
|
+
process.exit(1);
|
|
3536
|
+
}
|
|
3537
|
+
const config = loadFullConfig();
|
|
3538
|
+
if (!config.profiles[name]) {
|
|
3539
|
+
console.error(`Error: Profile "${name}" does not exist.`);
|
|
3540
|
+
process.exit(1);
|
|
3541
|
+
}
|
|
3542
|
+
if (name === config.active_profile) {
|
|
3543
|
+
console.error(`Error: Cannot delete the active profile "${name}". Run \`localskills profile switch <other>\` first.`);
|
|
3544
|
+
process.exit(1);
|
|
3545
|
+
}
|
|
3546
|
+
if (!opts.force) {
|
|
3547
|
+
const profile = config.profiles[name];
|
|
3548
|
+
const skillCount = Object.keys(profile.installed_skills).length;
|
|
3549
|
+
const details = [
|
|
3550
|
+
profile.token ? "authenticated session" : null,
|
|
3551
|
+
skillCount > 0 ? `${skillCount} installed skill${skillCount !== 1 ? "s" : ""}` : null
|
|
3552
|
+
].filter(Boolean);
|
|
3553
|
+
const detailStr = details.length > 0 ? ` (has ${details.join(", ")})` : "";
|
|
3554
|
+
const confirmed = await Re({
|
|
3555
|
+
message: `Delete profile "${name}"${detailStr}? This cannot be undone.`
|
|
3556
|
+
});
|
|
3557
|
+
if (Ct(confirmed) || !confirmed) {
|
|
3558
|
+
console.log("Cancelled.");
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
delete config.profiles[name];
|
|
3563
|
+
saveFullConfig(config);
|
|
3564
|
+
console.log(`Profile "${name}" deleted.`);
|
|
3565
|
+
});
|
|
3566
|
+
|
|
2957
3567
|
// src/index.ts
|
|
2958
|
-
var program = new
|
|
2959
|
-
program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0");
|
|
3568
|
+
var program = new Command10();
|
|
3569
|
+
program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0").option("--profile <name>", "Use a specific profile");
|
|
3570
|
+
program.hook("preAction", (thisCommand) => {
|
|
3571
|
+
const opts = thisCommand.opts();
|
|
3572
|
+
setProfileOverride(opts.profile);
|
|
3573
|
+
});
|
|
2960
3574
|
program.addCommand(loginCommand);
|
|
2961
3575
|
program.addCommand(logoutCommand);
|
|
2962
3576
|
program.addCommand(whoamiCommand);
|
|
@@ -2965,4 +3579,14 @@ program.addCommand(uninstallCommand);
|
|
|
2965
3579
|
program.addCommand(listCommand);
|
|
2966
3580
|
program.addCommand(pullCommand);
|
|
2967
3581
|
program.addCommand(publishCommand);
|
|
2968
|
-
program.
|
|
3582
|
+
program.addCommand(pushCommand);
|
|
3583
|
+
program.addCommand(shareCommand);
|
|
3584
|
+
program.addCommand(profileCommand);
|
|
3585
|
+
program.parseAsync().catch((err) => {
|
|
3586
|
+
if (err instanceof ProfileNotFoundError) {
|
|
3587
|
+
console.error(`Error: ${err.message}`);
|
|
3588
|
+
} else {
|
|
3589
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
3590
|
+
}
|
|
3591
|
+
process.exit(1);
|
|
3592
|
+
});
|