@localskills/cli 0.7.0 → 0.9.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.
Files changed (2) hide show
  1. package/dist/index.js +427 -38
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -161,7 +161,8 @@ var require_src = __commonJS({
161
161
  });
162
162
 
163
163
  // src/index.ts
164
- import { Command as Command9 } from "commander";
164
+ import { createRequire } from "module";
165
+ import { Command as Command10 } from "commander";
165
166
 
166
167
  // src/commands/auth.ts
167
168
  import { Command } from "commander";
@@ -521,6 +522,23 @@ var x = class {
521
522
  }
522
523
  }
523
524
  };
525
+ var kt = class extends x {
526
+ get cursor() {
527
+ return this.value ? 0 : 1;
528
+ }
529
+ get _value() {
530
+ return this.cursor === 0;
531
+ }
532
+ constructor(e2) {
533
+ super(e2, false), this.value = !!e2.initialValue, this.on("userInput", () => {
534
+ this.value = this._value;
535
+ }), this.on("confirm", (s) => {
536
+ this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = s, this.state = "submit", this.close();
537
+ }), this.on("cursor", () => {
538
+ this.value = !this.value;
539
+ });
540
+ }
541
+ };
524
542
  var Lt = class extends x {
525
543
  options;
526
544
  cursor = 0;
@@ -876,6 +894,33 @@ var X2 = (t) => {
876
894
  for (const A of f) for (const w of A) B2.push(w);
877
895
  return h && B2.push(g), B2;
878
896
  };
897
+ var Re = (t) => {
898
+ const r = t.active ?? "Yes", s = t.inactive ?? "No";
899
+ return new kt({ active: r, inactive: s, signal: t.signal, input: t.input, output: t.output, initialValue: t.initialValue ?? true, render() {
900
+ const i = t.withGuide ?? _.withGuide, a = `${i ? `${import_picocolors2.default.gray(d)}
901
+ ` : ""}${W2(this.state)} ${t.message}
902
+ `, o = this.value ? r : s;
903
+ switch (this.state) {
904
+ case "submit": {
905
+ const u = i ? `${import_picocolors2.default.gray(d)} ` : "";
906
+ return `${a}${u}${import_picocolors2.default.dim(o)}`;
907
+ }
908
+ case "cancel": {
909
+ const u = i ? `${import_picocolors2.default.gray(d)} ` : "";
910
+ return `${a}${u}${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o))}${i ? `
911
+ ${import_picocolors2.default.gray(d)}` : ""}`;
912
+ }
913
+ default: {
914
+ const u = i ? `${import_picocolors2.default.cyan(d)} ` : "", l = i ? import_picocolors2.default.cyan(x2) : "";
915
+ return `${a}${u}${this.value ? `${import_picocolors2.default.green(Q2)} ${r}` : `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(r)}`}${t.vertical ? i ? `
916
+ ${import_picocolors2.default.cyan(d)} ` : `
917
+ ` : ` ${import_picocolors2.default.dim("/")} `}${this.value ? `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.green(Q2)} ${s}`}
918
+ ${l}
919
+ `;
920
+ }
921
+ }
922
+ } }).prompt();
923
+ };
879
924
  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
925
  const u = [], l = o ?? _.withGuide, n = l ? s : "", c = l ? `${r} ` : "", g = l ? `${s} ` : "";
881
926
  for (let p = 0; p < a; p++) u.push(n);
@@ -1101,14 +1146,13 @@ ${l}
1101
1146
  } }).prompt();
1102
1147
 
1103
1148
  // src/lib/config.ts
1104
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
1149
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from "fs";
1105
1150
  import { join } from "path";
1106
1151
  import { homedir } from "os";
1107
1152
  var CONFIG_DIR = join(homedir(), ".localskills");
1108
1153
  var CONFIG_PATH = join(CONFIG_DIR, "config.json");
1109
- var DEFAULT_CONFIG = {
1110
- config_version: 2,
1111
- api_url: "https://localskills.sh",
1154
+ var DEFAULT_PROFILE_NAME = "default";
1155
+ var DEFAULT_PROFILE = {
1112
1156
  token: null,
1113
1157
  installed_skills: {},
1114
1158
  defaults: {
@@ -1117,21 +1161,109 @@ var DEFAULT_CONFIG = {
1117
1161
  },
1118
1162
  anonymous_key: null
1119
1163
  };
1120
- function loadConfig() {
1164
+ var DEFAULT_API_URL = "https://localskills.sh";
1165
+ var ProfileNotFoundError = class extends Error {
1166
+ constructor(profileName, availableProfiles, source) {
1167
+ const sourceLabel = source === "flag" ? "" : source === "env" ? " (from LOCALSKILLS_PROFILE)" : "";
1168
+ super(`Profile "${profileName}"${sourceLabel} does not exist. Available profiles: ${availableProfiles.join(", ")}`);
1169
+ this.profileName = profileName;
1170
+ this.availableProfiles = availableProfiles;
1171
+ this.source = source;
1172
+ this.name = "ProfileNotFoundError";
1173
+ }
1174
+ };
1175
+ var _profileOverride;
1176
+ function setProfileOverride(name) {
1177
+ _profileOverride = name;
1178
+ }
1179
+ function resolveProfileName(config) {
1180
+ if (_profileOverride) {
1181
+ if (!config.profiles[_profileOverride]) {
1182
+ throw new ProfileNotFoundError(
1183
+ _profileOverride,
1184
+ Object.keys(config.profiles),
1185
+ "flag"
1186
+ );
1187
+ }
1188
+ return _profileOverride;
1189
+ }
1190
+ const envProfile = process.env.LOCALSKILLS_PROFILE;
1191
+ if (envProfile) {
1192
+ if (!config.profiles[envProfile]) {
1193
+ throw new ProfileNotFoundError(
1194
+ envProfile,
1195
+ Object.keys(config.profiles),
1196
+ "env"
1197
+ );
1198
+ }
1199
+ return envProfile;
1200
+ }
1201
+ return config.active_profile;
1202
+ }
1203
+ function getActiveProfileName() {
1204
+ const full = loadFullConfig();
1205
+ return resolveProfileName(full);
1206
+ }
1207
+ function validateV3(config) {
1208
+ if (!config.profiles || typeof config.profiles !== "object") {
1209
+ config.profiles = {
1210
+ [DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
1211
+ };
1212
+ }
1213
+ if (!config.profiles[DEFAULT_PROFILE_NAME]) {
1214
+ config.profiles[DEFAULT_PROFILE_NAME] = { ...DEFAULT_PROFILE, installed_skills: {} };
1215
+ }
1216
+ if (!config.active_profile || !config.profiles[config.active_profile]) {
1217
+ config.active_profile = DEFAULT_PROFILE_NAME;
1218
+ }
1219
+ return config;
1220
+ }
1221
+ function loadFullConfig() {
1121
1222
  if (!existsSync(CONFIG_PATH)) {
1122
- return { ...DEFAULT_CONFIG, installed_skills: {} };
1223
+ return {
1224
+ config_version: 3,
1225
+ api_url: DEFAULT_API_URL,
1226
+ active_profile: DEFAULT_PROFILE_NAME,
1227
+ profiles: {
1228
+ [DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
1229
+ }
1230
+ };
1123
1231
  }
1124
1232
  try {
1125
1233
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1234
+ if (raw.config_version === 3) {
1235
+ return validateV3(raw);
1236
+ }
1237
+ let v2;
1126
1238
  if (!raw.config_version || raw.config_version < 2) {
1127
- return migrateV1toV2(raw);
1239
+ v2 = migrateV1toV2(raw);
1240
+ } else {
1241
+ v2 = raw;
1242
+ }
1243
+ const v3 = migrateV2toV3(v2);
1244
+ saveFullConfig(v3);
1245
+ return v3;
1246
+ } catch (err) {
1247
+ if (err instanceof SyntaxError) {
1248
+ console.error(`Warning: Config file is corrupt (${CONFIG_PATH}). Backing up and starting fresh.`);
1249
+ try {
1250
+ copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
1251
+ console.error(` Backup saved to ${CONFIG_PATH}.bak`);
1252
+ } catch {
1253
+ }
1254
+ return {
1255
+ config_version: 3,
1256
+ api_url: DEFAULT_API_URL,
1257
+ active_profile: DEFAULT_PROFILE_NAME,
1258
+ profiles: {
1259
+ [DEFAULT_PROFILE_NAME]: { ...DEFAULT_PROFILE, installed_skills: {} }
1260
+ }
1261
+ };
1128
1262
  }
1129
- return { ...DEFAULT_CONFIG, ...raw };
1130
- } catch {
1131
- return { ...DEFAULT_CONFIG, installed_skills: {} };
1263
+ throw err;
1132
1264
  }
1133
1265
  }
1134
- function saveConfig(config) {
1266
+ function saveFullConfig(config) {
1135
1267
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1136
1268
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
1137
1269
  mode: 384
@@ -1142,10 +1274,35 @@ function saveConfig(config) {
1142
1274
  } catch {
1143
1275
  }
1144
1276
  }
1277
+ function loadConfig() {
1278
+ const full = loadFullConfig();
1279
+ const profileName = resolveProfileName(full);
1280
+ const profile = full.profiles[profileName] ?? { ...DEFAULT_PROFILE, installed_skills: {} };
1281
+ return {
1282
+ config_version: 2,
1283
+ api_url: full.api_url,
1284
+ token: profile.token,
1285
+ installed_skills: profile.installed_skills,
1286
+ defaults: profile.defaults,
1287
+ anonymous_key: profile.anonymous_key ?? null
1288
+ };
1289
+ }
1290
+ function saveConfig(config) {
1291
+ const full = loadFullConfig();
1292
+ const profileName = resolveProfileName(full);
1293
+ full.api_url = config.api_url;
1294
+ full.profiles[profileName] = {
1295
+ token: config.token,
1296
+ installed_skills: config.installed_skills,
1297
+ defaults: config.defaults,
1298
+ anonymous_key: config.anonymous_key ?? null
1299
+ };
1300
+ saveFullConfig(full);
1301
+ }
1145
1302
  function migrateV1toV2(v1) {
1146
1303
  const v2 = {
1147
1304
  config_version: 2,
1148
- api_url: v1.api_url || DEFAULT_CONFIG.api_url,
1305
+ api_url: v1.api_url || DEFAULT_API_URL,
1149
1306
  token: v1.token,
1150
1307
  installed_skills: {},
1151
1308
  defaults: {
@@ -1174,9 +1331,23 @@ function migrateV1toV2(v1) {
1174
1331
  ]
1175
1332
  };
1176
1333
  }
1177
- saveConfig(v2);
1178
1334
  return v2;
1179
1335
  }
1336
+ function migrateV2toV3(v2) {
1337
+ return {
1338
+ config_version: 3,
1339
+ api_url: v2.api_url || DEFAULT_API_URL,
1340
+ active_profile: DEFAULT_PROFILE_NAME,
1341
+ profiles: {
1342
+ [DEFAULT_PROFILE_NAME]: {
1343
+ token: v2.token,
1344
+ installed_skills: v2.installed_skills,
1345
+ defaults: v2.defaults,
1346
+ anonymous_key: v2.anonymous_key ?? null
1347
+ }
1348
+ }
1349
+ };
1350
+ }
1180
1351
  function getToken() {
1181
1352
  return loadConfig().token;
1182
1353
  }
@@ -1198,6 +1369,11 @@ function setAnonymousKey(key) {
1198
1369
  config.anonymous_key = key;
1199
1370
  saveConfig(config);
1200
1371
  }
1372
+ function setLastUpdateCheck(ts) {
1373
+ const config = loadFullConfig();
1374
+ config.last_update_check = ts;
1375
+ saveFullConfig(config);
1376
+ }
1201
1377
 
1202
1378
  // src/lib/api-client.ts
1203
1379
  var ApiClient = class {
@@ -1423,6 +1599,22 @@ function isValidSemVer(v) {
1423
1599
  function isValidSemVerRange(range) {
1424
1600
  return RANGE_RE.test(range);
1425
1601
  }
1602
+ function compareSemVer(a, b) {
1603
+ if (a.major !== b.major)
1604
+ return a.major > b.major ? 1 : -1;
1605
+ if (a.minor !== b.minor)
1606
+ return a.minor > b.minor ? 1 : -1;
1607
+ if (a.patch !== b.patch)
1608
+ return a.patch > b.patch ? 1 : -1;
1609
+ return 0;
1610
+ }
1611
+ function isGreaterThan(next, prev) {
1612
+ const a = parseSemVer(next);
1613
+ const b = parseSemVer(prev);
1614
+ if (!a || !b)
1615
+ return false;
1616
+ return compareSemVer(a, b) === 1;
1617
+ }
1426
1618
 
1427
1619
  // ../../packages/shared/dist/utils/index.js
1428
1620
  function titleFromSlug(slug) {
@@ -1447,8 +1639,8 @@ function buildVersionQuery(range) {
1447
1639
  console.error(`Invalid version specifier: ${range}`);
1448
1640
  process.exit(1);
1449
1641
  }
1450
- function formatVersionLabel(semver, version) {
1451
- return semver ? `v${semver}` : `v${version}`;
1642
+ function formatVersionLabel(semver, version2) {
1643
+ return semver ? `v${semver}` : `v${version2}`;
1452
1644
  }
1453
1645
  function cancelGuard(value) {
1454
1646
  if (Ct(value)) {
@@ -2150,13 +2342,13 @@ function getCacheDir(slug) {
2150
2342
  }
2151
2343
  return dir;
2152
2344
  }
2153
- function store(slug, content, skill, version) {
2345
+ function store(slug, content, skill, version2) {
2154
2346
  const dir = getCacheDir(slug);
2155
2347
  mkdirSync5(dir, { recursive: true });
2156
2348
  writeFileSync5(join12(dir, "raw.md"), content);
2157
2349
  const meta = {
2158
2350
  hash: skill.contentHash,
2159
- version,
2351
+ version: version2,
2160
2352
  semver: skill.currentSemver ?? null,
2161
2353
  name: skill.name,
2162
2354
  description: skill.description,
@@ -2202,14 +2394,14 @@ function purge(slug) {
2202
2394
  rmSync3(dir, { recursive: true, force: true });
2203
2395
  }
2204
2396
  }
2205
- function storePackage(slug, zipBuffer, manifest, skill, version) {
2397
+ function storePackage(slug, zipBuffer, manifest, skill, version2) {
2206
2398
  const dir = getCacheDir(slug);
2207
2399
  mkdirSync5(dir, { recursive: true });
2208
2400
  writeFileSync5(join12(dir, "package.zip"), zipBuffer);
2209
2401
  writeFileSync5(join12(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
2210
2402
  const meta = {
2211
2403
  hash: skill.contentHash,
2212
- version,
2404
+ version: version2,
2213
2405
  semver: skill.currentSemver ?? null,
2214
2406
  name: skill.name,
2215
2407
  description: skill.description,
@@ -2358,13 +2550,13 @@ function parsePlatforms(raw) {
2358
2550
  }
2359
2551
  return platforms;
2360
2552
  }
2361
- function buildSkillRecord(cacheKey, skill, version, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
2553
+ function buildSkillRecord(cacheKey, skill, version2, resolvedSemver, requestedRange, existingInstallations, newInstallations) {
2362
2554
  return {
2363
2555
  slug: cacheKey,
2364
2556
  name: skill.name,
2365
2557
  type: skill.type ?? "skill",
2366
2558
  hash: skill.contentHash,
2367
- version,
2559
+ version: version2,
2368
2560
  semver: resolvedSemver ?? null,
2369
2561
  semverRange: requestedRange ?? null,
2370
2562
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2449,13 +2641,13 @@ var installCommand = new Command2("install").description("Install a skill locall
2449
2641
  const format = resData.format ?? "text";
2450
2642
  const cacheKey = resData.skill.publicId || slug;
2451
2643
  if (format === "package") {
2452
- const { skill: skill2, downloadUrl, manifest, version: version2, semver: resolvedSemver2 } = resData;
2453
- spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version2)} (package, ${manifest.files.length} files)`);
2644
+ const { skill: skill2, downloadUrl, manifest, version: version3, semver: resolvedSemver2 } = resData;
2645
+ spinner.stop(`Fetched ${skill2.name} ${formatVersionLabel(resolvedSemver2, version3)} (package, ${manifest.files.length} files)`);
2454
2646
  const dlSpinner = bt2();
2455
2647
  dlSpinner.start("Downloading package...");
2456
2648
  const zipBuffer = await client.fetchBinary(downloadUrl);
2457
2649
  dlSpinner.stop(`Downloaded ${(zipBuffer.length / 1024).toFixed(1)} KB`);
2458
- storePackage(cacheKey, zipBuffer, manifest, skill2, version2);
2650
+ storePackage(cacheKey, zipBuffer, manifest, skill2, version3);
2459
2651
  const installations2 = [];
2460
2652
  const results2 = [];
2461
2653
  for (const platformId of platforms) {
@@ -2486,7 +2678,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2486
2678
  config.installed_skills[cacheKey] = buildSkillRecord(
2487
2679
  cacheKey,
2488
2680
  skill2,
2489
- version2,
2681
+ version3,
2490
2682
  resolvedSemver2,
2491
2683
  requestedRange,
2492
2684
  config.installed_skills[cacheKey]?.installations,
@@ -2499,9 +2691,9 @@ var installCommand = new Command2("install").description("Install a skill locall
2499
2691
  Le(`Done! Installed to ${installations2.length} target(s).`);
2500
2692
  return;
2501
2693
  }
2502
- const { skill, content, version, semver: resolvedSemver } = resData;
2503
- spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version)}`);
2504
- store(cacheKey, content, skill, version);
2694
+ const { skill, content, version: version2, semver: resolvedSemver } = resData;
2695
+ spinner.stop(`Fetched ${skill.name} ${formatVersionLabel(resolvedSemver, version2)}`);
2696
+ store(cacheKey, content, skill, version2);
2505
2697
  const contentType = skill.type ?? "skill";
2506
2698
  const installations = [];
2507
2699
  const results = [];
@@ -2543,7 +2735,7 @@ var installCommand = new Command2("install").description("Install a skill locall
2543
2735
  config.installed_skills[cacheKey] = buildSkillRecord(
2544
2736
  cacheKey,
2545
2737
  skill,
2546
- version,
2738
+ version2,
2547
2739
  resolvedSemver,
2548
2740
  requestedRange,
2549
2741
  config.installed_skills[cacheKey]?.installations,
@@ -2693,7 +2885,7 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
2693
2885
  }
2694
2886
  const resData = res.data;
2695
2887
  const format = resData.format ?? "text";
2696
- const { skill, version } = resData;
2888
+ const { skill, version: version2 } = resData;
2697
2889
  if (skill.contentHash === installed.hash) {
2698
2890
  spinner.stop(`${slug} \u2014 up to date`);
2699
2891
  skipped++;
@@ -2702,7 +2894,7 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
2702
2894
  if (format === "package") {
2703
2895
  const { downloadUrl, manifest } = resData;
2704
2896
  const zipBuffer = await client.fetchBinary(downloadUrl);
2705
- storePackage(slug, zipBuffer, manifest, skill, version);
2897
+ storePackage(slug, zipBuffer, manifest, skill, version2);
2706
2898
  for (const installation of installed.installations) {
2707
2899
  const adapter = getAdapter(installation.platform);
2708
2900
  const targetPath = adapter.resolvePath(slug, installation.scope, installation.projectDir, skill.type ?? "skill");
@@ -2711,7 +2903,7 @@ var pullCommand = new Command5("pull").description("Pull latest versions of all
2711
2903
  }
2712
2904
  } else {
2713
2905
  const { content } = resData;
2714
- store(slug, content, skill, version);
2906
+ store(slug, content, skill, version2);
2715
2907
  for (const installation of installed.installations) {
2716
2908
  if (installation.method === "symlink") {
2717
2909
  getPlatformFile(slug, installation.platform, skill);
@@ -2741,11 +2933,11 @@ ${transformed}`
2741
2933
  }
2742
2934
  }
2743
2935
  installed.hash = skill.contentHash;
2744
- installed.version = version;
2936
+ installed.version = version2;
2745
2937
  installed.semver = resData.semver ?? null;
2746
2938
  installed.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
2747
2939
  updated++;
2748
- spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version)}`);
2940
+ spinner.stop(`${slug} \u2014 updated to ${formatVersionLabel(res.data.semver, version2)}`);
2749
2941
  }
2750
2942
  saveConfig(config);
2751
2943
  Le(`Pull complete. ${updated} updated, ${skipped} up to date.`);
@@ -3283,9 +3475,198 @@ async function uploadAnonymousSkill(client, params) {
3283
3475
  R2.info(`Install: localskills install ${res.data.publicId}`);
3284
3476
  }
3285
3477
 
3478
+ // src/commands/profile.ts
3479
+ import { Command as Command9 } from "commander";
3480
+ var PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
3481
+ var MAX_PROFILE_NAME_LENGTH = 32;
3482
+ var profileCommand = new Command9("profile").description("Manage CLI profiles for multiple accounts");
3483
+ profileCommand.command("list").description("List all profiles").action(() => {
3484
+ const config = loadFullConfig();
3485
+ const persisted = config.active_profile;
3486
+ let resolved;
3487
+ try {
3488
+ resolved = getActiveProfileName();
3489
+ } catch (err) {
3490
+ if (err instanceof ProfileNotFoundError) {
3491
+ console.error(`Warning: ${err.message}
3492
+ `);
3493
+ resolved = persisted;
3494
+ } else {
3495
+ throw err;
3496
+ }
3497
+ }
3498
+ const names = Object.keys(config.profiles);
3499
+ console.log("Profiles:\n");
3500
+ for (const name of names) {
3501
+ const profile = config.profiles[name];
3502
+ let marker = "";
3503
+ if (name === resolved && name === persisted) {
3504
+ marker = " (active)";
3505
+ } else if (name === resolved) {
3506
+ marker = " (active via override)";
3507
+ } else if (name === persisted) {
3508
+ marker = " (default)";
3509
+ }
3510
+ const auth = profile.token ? "authenticated" : "not authenticated";
3511
+ const skillCount = Object.keys(profile.installed_skills).length;
3512
+ console.log(` ${name}${marker} \u2014 ${auth}, ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3513
+ }
3514
+ });
3515
+ profileCommand.command("create <name>").description("Create a new profile").action((name) => {
3516
+ if (!PROFILE_NAME_RE.test(name)) {
3517
+ console.error(
3518
+ `Error: Invalid profile name "${name}". Profile names must use lowercase letters, numbers, and hyphens, and must start with a letter or number.`
3519
+ );
3520
+ process.exit(1);
3521
+ }
3522
+ if (name.length > MAX_PROFILE_NAME_LENGTH) {
3523
+ console.error(`Error: Profile name must be ${MAX_PROFILE_NAME_LENGTH} characters or fewer.`);
3524
+ process.exit(1);
3525
+ }
3526
+ const config = loadFullConfig();
3527
+ if (config.profiles[name]) {
3528
+ console.error(`Error: Profile "${name}" already exists.`);
3529
+ process.exit(1);
3530
+ }
3531
+ config.profiles[name] = {
3532
+ token: null,
3533
+ installed_skills: {},
3534
+ defaults: {
3535
+ scope: "project",
3536
+ method: "symlink"
3537
+ },
3538
+ anonymous_key: null
3539
+ };
3540
+ saveFullConfig(config);
3541
+ console.log(`Profile "${name}" created.`);
3542
+ });
3543
+ profileCommand.command("switch <name>").description("Switch the active profile").action((name) => {
3544
+ const config = loadFullConfig();
3545
+ if (!config.profiles[name]) {
3546
+ console.error(`Error: Profile "${name}" does not exist.`);
3547
+ console.error(`Available profiles: ${Object.keys(config.profiles).join(", ")}`);
3548
+ process.exit(1);
3549
+ }
3550
+ config.active_profile = name;
3551
+ saveFullConfig(config);
3552
+ console.log(`Switched to profile "${name}".`);
3553
+ });
3554
+ profileCommand.command("delete <name>").description("Delete a profile").option("-f, --force", "Skip confirmation prompt").action(async (name, opts) => {
3555
+ if (name === DEFAULT_PROFILE_NAME) {
3556
+ console.error(`Error: Cannot delete the "${DEFAULT_PROFILE_NAME}" profile.`);
3557
+ process.exit(1);
3558
+ }
3559
+ const config = loadFullConfig();
3560
+ if (!config.profiles[name]) {
3561
+ console.error(`Error: Profile "${name}" does not exist.`);
3562
+ process.exit(1);
3563
+ }
3564
+ if (name === config.active_profile) {
3565
+ console.error(`Error: Cannot delete the active profile "${name}". Run \`localskills profile switch <other>\` first.`);
3566
+ process.exit(1);
3567
+ }
3568
+ if (!opts.force) {
3569
+ const profile = config.profiles[name];
3570
+ const skillCount = Object.keys(profile.installed_skills).length;
3571
+ const details = [
3572
+ profile.token ? "authenticated session" : null,
3573
+ skillCount > 0 ? `${skillCount} installed skill${skillCount !== 1 ? "s" : ""}` : null
3574
+ ].filter(Boolean);
3575
+ const detailStr = details.length > 0 ? ` (has ${details.join(", ")})` : "";
3576
+ const confirmed = await Re({
3577
+ message: `Delete profile "${name}"${detailStr}? This cannot be undone.`
3578
+ });
3579
+ if (Ct(confirmed) || !confirmed) {
3580
+ console.log("Cancelled.");
3581
+ return;
3582
+ }
3583
+ }
3584
+ delete config.profiles[name];
3585
+ saveFullConfig(config);
3586
+ console.log(`Profile "${name}" deleted.`);
3587
+ });
3588
+
3589
+ // src/lib/update-check.ts
3590
+ var REGISTRY_URL = "https://registry.npmjs.org/@localskills/cli/latest";
3591
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3592
+ var FETCH_TIMEOUT_MS = 500;
3593
+ var pendingCheck = null;
3594
+ var pendingAbort = null;
3595
+ var currentVer = null;
3596
+ function shouldCheck() {
3597
+ if (process.env.LOCALSKILLS_NO_UPDATE_CHECK === "1") return false;
3598
+ try {
3599
+ const config = loadFullConfig();
3600
+ if (config.update_check_opted_out) return false;
3601
+ const last = config.last_update_check;
3602
+ if (last) {
3603
+ const elapsed = Date.now() - new Date(last).getTime();
3604
+ if (elapsed < CHECK_INTERVAL_MS) return false;
3605
+ }
3606
+ } catch {
3607
+ return false;
3608
+ }
3609
+ return true;
3610
+ }
3611
+ function startUpdateCheck(currentVersion) {
3612
+ try {
3613
+ if (!shouldCheck()) return;
3614
+ currentVer = currentVersion;
3615
+ try {
3616
+ setLastUpdateCheck((/* @__PURE__ */ new Date()).toISOString());
3617
+ } catch {
3618
+ }
3619
+ pendingAbort = new AbortController();
3620
+ pendingCheck = fetch(REGISTRY_URL, { signal: pendingAbort.signal }).then((res) => {
3621
+ if (!res.ok) return null;
3622
+ return res.json();
3623
+ }).then((data) => {
3624
+ if (data && typeof data.version === "string" && isGreaterThan(data.version, currentVersion)) {
3625
+ return data.version;
3626
+ }
3627
+ return null;
3628
+ }).catch(() => null);
3629
+ } catch {
3630
+ }
3631
+ }
3632
+ async function printUpdateNotice() {
3633
+ try {
3634
+ if (!pendingCheck) return;
3635
+ const timeout = new Promise((resolve7) => {
3636
+ setTimeout(() => resolve7(null), FETCH_TIMEOUT_MS).unref();
3637
+ });
3638
+ const remoteVersion = await Promise.race([pendingCheck, timeout]);
3639
+ pendingAbort?.abort();
3640
+ pendingAbort = null;
3641
+ pendingCheck = null;
3642
+ if (remoteVersion) {
3643
+ const from = currentVer ?? "unknown";
3644
+ process.stderr.write(
3645
+ `
3646
+ Update available: ${from} \u2192 ${remoteVersion}
3647
+ Run: npm update -g @localskills/cli
3648
+ `
3649
+ );
3650
+ }
3651
+ } catch {
3652
+ }
3653
+ }
3654
+
3286
3655
  // src/index.ts
3287
- var program = new Command9();
3288
- program.name("localskills").description("Install and manage agent skills from localskills.sh").version("0.1.0");
3656
+ var require2 = createRequire(import.meta.url);
3657
+ var { version } = require2("../package.json");
3658
+ var program = new Command10();
3659
+ program.name("localskills").description("Install and manage agent skills from localskills.sh").version(version).option("--profile <name>", "Use a specific profile").option("--no-update-check", "Skip update check");
3660
+ program.hook("preAction", (thisCommand) => {
3661
+ const opts = thisCommand.opts();
3662
+ setProfileOverride(opts.profile);
3663
+ if (opts.updateCheck !== false) {
3664
+ startUpdateCheck(version);
3665
+ }
3666
+ });
3667
+ program.hook("postAction", async () => {
3668
+ await printUpdateNotice();
3669
+ });
3289
3670
  program.addCommand(loginCommand);
3290
3671
  program.addCommand(logoutCommand);
3291
3672
  program.addCommand(whoamiCommand);
@@ -3296,4 +3677,12 @@ program.addCommand(pullCommand);
3296
3677
  program.addCommand(publishCommand);
3297
3678
  program.addCommand(pushCommand);
3298
3679
  program.addCommand(shareCommand);
3299
- program.parse();
3680
+ program.addCommand(profileCommand);
3681
+ program.parseAsync().catch((err) => {
3682
+ if (err instanceof ProfileNotFoundError) {
3683
+ console.error(`Error: ${err.message}`);
3684
+ } else {
3685
+ console.error(err instanceof Error ? err.message : String(err));
3686
+ }
3687
+ process.exit(1);
3688
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localskills/cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "CLI for localskills.sh — install agent skills locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,8 +19,8 @@
19
19
  "tsup": "^8",
20
20
  "typescript": "^5.7",
21
21
  "vitest": "^3",
22
- "@localskills/tsconfig": "0.0.0",
23
- "@localskills/shared": "0.0.0"
22
+ "@localskills/shared": "0.0.0",
23
+ "@localskills/tsconfig": "0.0.0"
24
24
  },
25
25
  "scripts": {
26
26
  "build": "tsup",