@pushrec/skills 0.1.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 ADDED
@@ -0,0 +1,1534 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander9 = require("commander");
28
+
29
+ // src/commands/auth.ts
30
+ var import_commander = require("commander");
31
+ var import_readline = require("readline");
32
+ var import_promises = require("readline/promises");
33
+ var import_chalk = __toESM(require("chalk"));
34
+
35
+ // src/lib/keychain.ts
36
+ var import_fs = require("fs");
37
+ var import_path2 = require("path");
38
+ var import_child_process = require("child_process");
39
+ var import_os2 = require("os");
40
+
41
+ // src/constants.ts
42
+ var import_os = require("os");
43
+ var import_path = require("path");
44
+ var REGISTRY_URL = process.env.PUSHREC_REGISTRY_URL ?? "https://skills.pushrec.com";
45
+ var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".pushrec");
46
+ var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
47
+ var SKILLS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "skills");
48
+ var KEYCHAIN_SERVICE = "pushrec-skills";
49
+ var KEYCHAIN_ACCOUNT = "license-key";
50
+ var LICENSE_CACHE_FILE = (0, import_path.join)(CONFIG_DIR, "license-cache.json");
51
+ var ED25519_PUBLIC_KEYS = {
52
+ v1: "4nm6WVfyEb9AUhbARD1vTh5MR4JmCn8+lmUVKRCu8MY="
53
+ };
54
+ var OFFLINE_GRACE = {
55
+ /** Re-validate with server every 7 days */
56
+ revalidateIntervalMs: 7 * 24 * 60 * 60 * 1e3,
57
+ /** Allow offline use for 30 days without server contact */
58
+ gracePeriodMs: 30 * 24 * 60 * 60 * 1e3,
59
+ /** Hard disable after 90 days offline */
60
+ hardDisableMs: 90 * 24 * 60 * 60 * 1e3
61
+ };
62
+
63
+ // src/lib/keychain.ts
64
+ var CREDENTIALS_FILE = (0, import_path2.join)(CONFIG_DIR, ".credentials");
65
+ async function storeLicenseKey(key) {
66
+ const stored = await storeInKeychain(key);
67
+ if (stored) return;
68
+ storeInFile(key);
69
+ }
70
+ async function retrieveLicenseKey() {
71
+ const fromKeychain = await retrieveFromKeychain();
72
+ if (fromKeychain) return fromKeychain;
73
+ return retrieveFromFile();
74
+ }
75
+ async function deleteLicenseKey() {
76
+ await deleteFromKeychain();
77
+ deleteFromFile();
78
+ }
79
+ async function storeInKeychain(key) {
80
+ const os = (0, import_os2.platform)();
81
+ try {
82
+ if (os === "darwin") {
83
+ (0, import_child_process.execFileSync)(
84
+ "security",
85
+ [
86
+ "add-generic-password",
87
+ "-U",
88
+ "-s",
89
+ KEYCHAIN_SERVICE,
90
+ "-a",
91
+ KEYCHAIN_ACCOUNT,
92
+ "-w",
93
+ key
94
+ ],
95
+ { stdio: "pipe", timeout: 5e3 }
96
+ );
97
+ return true;
98
+ }
99
+ if (os === "linux") {
100
+ const result = (0, import_child_process.spawnSync)(
101
+ "secret-tool",
102
+ [
103
+ "store",
104
+ "--label",
105
+ KEYCHAIN_SERVICE,
106
+ "service",
107
+ KEYCHAIN_SERVICE,
108
+ "account",
109
+ KEYCHAIN_ACCOUNT
110
+ ],
111
+ { input: key, stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
112
+ );
113
+ if (result.status === 0) return true;
114
+ }
115
+ } catch {
116
+ }
117
+ return false;
118
+ }
119
+ async function retrieveFromKeychain() {
120
+ const os = (0, import_os2.platform)();
121
+ try {
122
+ if (os === "darwin") {
123
+ const result = (0, import_child_process.execFileSync)(
124
+ "security",
125
+ [
126
+ "find-generic-password",
127
+ "-s",
128
+ KEYCHAIN_SERVICE,
129
+ "-a",
130
+ KEYCHAIN_ACCOUNT,
131
+ "-w"
132
+ ],
133
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
134
+ );
135
+ return result.trim() || null;
136
+ }
137
+ if (os === "linux") {
138
+ const result = (0, import_child_process.execFileSync)(
139
+ "secret-tool",
140
+ [
141
+ "lookup",
142
+ "service",
143
+ KEYCHAIN_SERVICE,
144
+ "account",
145
+ KEYCHAIN_ACCOUNT
146
+ ],
147
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
148
+ );
149
+ return result.trim() || null;
150
+ }
151
+ } catch {
152
+ }
153
+ return null;
154
+ }
155
+ async function deleteFromKeychain() {
156
+ const os = (0, import_os2.platform)();
157
+ try {
158
+ if (os === "darwin") {
159
+ (0, import_child_process.execFileSync)(
160
+ "security",
161
+ [
162
+ "delete-generic-password",
163
+ "-s",
164
+ KEYCHAIN_SERVICE,
165
+ "-a",
166
+ KEYCHAIN_ACCOUNT
167
+ ],
168
+ { stdio: "pipe", timeout: 5e3 }
169
+ );
170
+ }
171
+ if (os === "linux") {
172
+ (0, import_child_process.execFileSync)(
173
+ "secret-tool",
174
+ [
175
+ "clear",
176
+ "service",
177
+ KEYCHAIN_SERVICE,
178
+ "account",
179
+ KEYCHAIN_ACCOUNT
180
+ ],
181
+ { stdio: "pipe", timeout: 5e3 }
182
+ );
183
+ }
184
+ } catch {
185
+ }
186
+ }
187
+ function storeInFile(key) {
188
+ const dir = (0, import_path2.dirname)(CREDENTIALS_FILE);
189
+ if (!(0, import_fs.existsSync)(dir)) {
190
+ (0, import_fs.mkdirSync)(dir, { recursive: true });
191
+ }
192
+ (0, import_fs.writeFileSync)(CREDENTIALS_FILE, key, "utf-8");
193
+ (0, import_fs.chmodSync)(CREDENTIALS_FILE, 384);
194
+ }
195
+ function retrieveFromFile() {
196
+ if (!(0, import_fs.existsSync)(CREDENTIALS_FILE)) return null;
197
+ try {
198
+ const key = (0, import_fs.readFileSync)(CREDENTIALS_FILE, "utf-8").trim();
199
+ return key || null;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+ function deleteFromFile() {
205
+ try {
206
+ if ((0, import_fs.existsSync)(CREDENTIALS_FILE)) {
207
+ (0, import_fs.unlinkSync)(CREDENTIALS_FILE);
208
+ }
209
+ } catch {
210
+ }
211
+ }
212
+
213
+ // src/lib/fingerprint.ts
214
+ var import_child_process2 = require("child_process");
215
+ var import_fs2 = require("fs");
216
+ var import_crypto = require("crypto");
217
+ var import_os3 = require("os");
218
+ function getMachineFingerprint() {
219
+ const rawId = getRawMachineId();
220
+ return (0, import_crypto.createHash)("sha256").update(rawId).digest("hex");
221
+ }
222
+ function getRawMachineId() {
223
+ const os = (0, import_os3.platform)();
224
+ switch (os) {
225
+ case "darwin":
226
+ return getMacOsId();
227
+ case "linux":
228
+ return getLinuxId();
229
+ case "win32":
230
+ return getWindowsId();
231
+ default:
232
+ return getFallbackId();
233
+ }
234
+ }
235
+ function getMacOsId() {
236
+ try {
237
+ const output = (0, import_child_process2.execFileSync)("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
238
+ encoding: "utf8",
239
+ timeout: 5e3
240
+ });
241
+ const match = output.match(/IOPlatformUUID[^=]+=\s*"([^"]+)"/);
242
+ if (match?.[1]) return `darwin:${match[1]}`;
243
+ } catch {
244
+ }
245
+ return getFallbackId();
246
+ }
247
+ function getLinuxId() {
248
+ try {
249
+ if ((0, import_fs2.existsSync)("/etc/machine-id")) {
250
+ const id = (0, import_fs2.readFileSync)("/etc/machine-id", "utf-8").trim();
251
+ if (id.length >= 32) return `linux:${id}`;
252
+ }
253
+ if ((0, import_fs2.existsSync)("/var/lib/dbus/machine-id")) {
254
+ const id = (0, import_fs2.readFileSync)("/var/lib/dbus/machine-id", "utf-8").trim();
255
+ if (id.length >= 32) return `linux:${id}`;
256
+ }
257
+ } catch {
258
+ }
259
+ return getFallbackId();
260
+ }
261
+ function getWindowsId() {
262
+ try {
263
+ const output = (0, import_child_process2.execFileSync)(
264
+ "reg",
265
+ ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
266
+ { encoding: "utf8", timeout: 5e3 }
267
+ );
268
+ const match = output.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/);
269
+ if (match?.[1]) return `win32:${match[1].trim()}`;
270
+ } catch {
271
+ }
272
+ return getFallbackId();
273
+ }
274
+ function getFallbackId() {
275
+ return `fallback:${(0, import_os3.platform)()}:${(0, import_os3.hostname)()}`;
276
+ }
277
+ function getPlatformName() {
278
+ const os = (0, import_os3.platform)();
279
+ switch (os) {
280
+ case "darwin":
281
+ return "macOS";
282
+ case "linux":
283
+ return "Linux";
284
+ case "win32":
285
+ return "Windows";
286
+ default:
287
+ return os;
288
+ }
289
+ }
290
+ function getHostname() {
291
+ return (0, import_os3.hostname)();
292
+ }
293
+
294
+ // src/lib/license.ts
295
+ var ed = __toESM(require("@noble/ed25519"));
296
+ var import_fs3 = require("fs");
297
+ var import_path3 = require("path");
298
+ function fromBase64Url(str) {
299
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
300
+ return Uint8Array.from(Buffer.from(padded, "base64"));
301
+ }
302
+ function parseLicenseKey(key) {
303
+ const match = key.match(/^lic_(v\d+)_(.+)\.([A-Za-z0-9_-]+)$/);
304
+ if (!match) return null;
305
+ const [, version, encodedPayload, encodedSignature] = match;
306
+ try {
307
+ const payloadBytes = fromBase64Url(encodedPayload);
308
+ const signature = fromBase64Url(encodedSignature);
309
+ const payload = JSON.parse(
310
+ new TextDecoder().decode(payloadBytes)
311
+ );
312
+ if (typeof payload.buyerId !== "number" || typeof payload.email !== "string" || typeof payload.issuedAt !== "string" || typeof payload.maxDevices !== "number" || typeof payload.keyVersion !== "string") {
313
+ return null;
314
+ }
315
+ return { version, payload, payloadBytes, signature };
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
320
+ async function verifyOffline(key) {
321
+ const parsed = parseLicenseKey(key);
322
+ if (!parsed) {
323
+ return { valid: false, error: "Invalid license key format" };
324
+ }
325
+ const publicKeyBase64 = ED25519_PUBLIC_KEYS[parsed.version];
326
+ if (!publicKeyBase64 || publicKeyBase64.startsWith("PLACEHOLDER")) {
327
+ return {
328
+ valid: false,
329
+ error: `Unsupported key version: ${parsed.version}`
330
+ };
331
+ }
332
+ const publicKey = fromBase64Url(publicKeyBase64);
333
+ try {
334
+ const valid = await ed.verifyAsync(
335
+ parsed.signature,
336
+ parsed.payloadBytes,
337
+ publicKey
338
+ );
339
+ if (!valid) {
340
+ return { valid: false, error: "Invalid license key signature" };
341
+ }
342
+ return { valid: true, payload: parsed.payload };
343
+ } catch {
344
+ return { valid: false, error: "Signature verification failed" };
345
+ }
346
+ }
347
+ function loadLicenseCache() {
348
+ if (!(0, import_fs3.existsSync)(LICENSE_CACHE_FILE)) return null;
349
+ try {
350
+ const raw = (0, import_fs3.readFileSync)(LICENSE_CACHE_FILE, "utf-8");
351
+ return JSON.parse(raw);
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+ function saveLicenseCache(cache) {
357
+ const dir = (0, import_path3.dirname)(LICENSE_CACHE_FILE);
358
+ if (!(0, import_fs3.existsSync)(dir)) {
359
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
360
+ }
361
+ (0, import_fs3.writeFileSync)(LICENSE_CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
362
+ (0, import_fs3.chmodSync)(LICENSE_CACHE_FILE, 384);
363
+ }
364
+ function deleteLicenseCache() {
365
+ try {
366
+ if ((0, import_fs3.existsSync)(LICENSE_CACHE_FILE)) {
367
+ (0, import_fs3.unlinkSync)(LICENSE_CACHE_FILE);
368
+ }
369
+ } catch {
370
+ }
371
+ }
372
+ function checkOfflineStatus(cache) {
373
+ const now = Date.now();
374
+ const verifiedAt = new Date(cache.verifiedAt).getTime();
375
+ const elapsed = now - verifiedAt;
376
+ if (elapsed < OFFLINE_GRACE.revalidateIntervalMs) {
377
+ return { status: "valid" };
378
+ }
379
+ if (elapsed < OFFLINE_GRACE.gracePeriodMs) {
380
+ return {
381
+ status: "stale",
382
+ message: "License not verified in 7+ days. Will re-verify on next network access."
383
+ };
384
+ }
385
+ if (elapsed < OFFLINE_GRACE.hardDisableMs) {
386
+ const daysLeft = Math.ceil(
387
+ (OFFLINE_GRACE.hardDisableMs - elapsed) / (24 * 60 * 60 * 1e3)
388
+ );
389
+ return {
390
+ status: "warning",
391
+ message: `License not verified in 30+ days. Features disable in ${daysLeft} days. Connect to internet to re-verify.`
392
+ };
393
+ }
394
+ return {
395
+ status: "disabled",
396
+ message: "License not verified in 90+ days. Connect to internet to re-verify your license."
397
+ };
398
+ }
399
+
400
+ // src/lib/config.ts
401
+ var import_fs4 = require("fs");
402
+ var import_path4 = require("path");
403
+ function defaultConfig() {
404
+ return {
405
+ registryUrl: REGISTRY_URL,
406
+ lastVerifiedAt: null,
407
+ installedSkills: {}
408
+ };
409
+ }
410
+ function loadConfig() {
411
+ if (!(0, import_fs4.existsSync)(CONFIG_FILE)) {
412
+ return defaultConfig();
413
+ }
414
+ try {
415
+ const raw = (0, import_fs4.readFileSync)(CONFIG_FILE, "utf-8");
416
+ return { ...defaultConfig(), ...JSON.parse(raw) };
417
+ } catch {
418
+ return defaultConfig();
419
+ }
420
+ }
421
+ function saveConfig(config) {
422
+ const dir = (0, import_path4.dirname)(CONFIG_FILE);
423
+ if (!(0, import_fs4.existsSync)(dir)) {
424
+ (0, import_fs4.mkdirSync)(dir, { recursive: true });
425
+ }
426
+ (0, import_fs4.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
427
+ }
428
+ function updateConfig(partial) {
429
+ const config = loadConfig();
430
+ const updated = { ...config, ...partial };
431
+ saveConfig(updated);
432
+ return updated;
433
+ }
434
+
435
+ // src/lib/registry.ts
436
+ function getBaseUrl() {
437
+ return loadConfig().registryUrl;
438
+ }
439
+ async function getAuthHeaders() {
440
+ const key = await retrieveLicenseKey();
441
+ if (!key) return {};
442
+ const fingerprint = getMachineFingerprint();
443
+ return {
444
+ Authorization: `Bearer ${key}`,
445
+ "X-Device-Fingerprint": fingerprint,
446
+ "X-Device-Platform": getPlatformName(),
447
+ "X-Device-Hostname": getHostname()
448
+ };
449
+ }
450
+ async function registryFetch(path, options = {}) {
451
+ const url = `${getBaseUrl()}${path}`;
452
+ const controller = new AbortController();
453
+ const timeout = setTimeout(() => controller.abort(), 15e3);
454
+ try {
455
+ const response = await fetch(url, {
456
+ ...options,
457
+ signal: controller.signal
458
+ });
459
+ return response;
460
+ } finally {
461
+ clearTimeout(timeout);
462
+ }
463
+ }
464
+ async function fetchSkillCatalog() {
465
+ const response = await registryFetch("/api/skills");
466
+ if (!response.ok) {
467
+ throw new Error(`Failed to fetch skill catalog: ${response.status}`);
468
+ }
469
+ return response.json();
470
+ }
471
+ async function fetchSkillManifest(name) {
472
+ const response = await registryFetch(`/api/skills/${encodeURIComponent(name)}`);
473
+ if (!response.ok) {
474
+ if (response.status === 404) {
475
+ throw new Error(`Skill "${name}" not found`);
476
+ }
477
+ throw new Error(`Failed to fetch skill "${name}": ${response.status}`);
478
+ }
479
+ return response.json();
480
+ }
481
+ async function downloadSkill(name) {
482
+ const authHeaders = await getAuthHeaders();
483
+ const response = await registryFetch(
484
+ `/api/skills/${encodeURIComponent(name)}/download`,
485
+ { headers: authHeaders }
486
+ );
487
+ if (!response.ok) {
488
+ const body = await response.json().catch(() => ({}));
489
+ throw new Error(
490
+ body.error ?? `Download failed: ${response.status}`
491
+ );
492
+ }
493
+ return response.arrayBuffer();
494
+ }
495
+ async function verifyLicenseOnline(key, fingerprint) {
496
+ const response = await registryFetch("/api/auth/verify", {
497
+ method: "POST",
498
+ headers: { "Content-Type": "application/json" },
499
+ body: JSON.stringify({ key, fingerprint })
500
+ });
501
+ if (response.status >= 500) {
502
+ throw new Error(`Registry server error (${response.status}). Try again later.`);
503
+ }
504
+ if (!response.ok) {
505
+ if (response.status === 401 || response.status === 403) {
506
+ const body = await response.json().catch(() => ({}));
507
+ const status = body.status;
508
+ const errorMsg = body.error;
509
+ const updatesExpireAt = body.updatesExpireAt;
510
+ if (status === "revoked") {
511
+ return { valid: false, error: "License has been revoked" };
512
+ }
513
+ if (status === "expired") {
514
+ return { valid: false, error: "License has expired" };
515
+ }
516
+ if (updatesExpireAt && new Date(updatesExpireAt) < /* @__PURE__ */ new Date()) {
517
+ return {
518
+ valid: false,
519
+ error: "Update subscription expired \u2014 you can still install previously-purchased versions"
520
+ };
521
+ }
522
+ if (errorMsg && /device/i.test(errorMsg)) {
523
+ const match = errorMsg.match(/(\d+)\s*(?:of|\/)\s*(\d+)/);
524
+ if (match) {
525
+ return {
526
+ valid: false,
527
+ error: `Device limit reached (${match[1]}/${match[2]} devices active)`
528
+ };
529
+ }
530
+ return { valid: false, error: `Device limit reached: ${errorMsg}` };
531
+ }
532
+ return { valid: false, error: errorMsg ?? "License revoked or invalid" };
533
+ }
534
+ throw new Error(`Verification failed (${response.status})`);
535
+ }
536
+ return response.json();
537
+ }
538
+ async function activateDevice(key, fingerprint, platformName, hostname) {
539
+ const response = await registryFetch("/api/auth/activate", {
540
+ method: "POST",
541
+ headers: { "Content-Type": "application/json" },
542
+ body: JSON.stringify({
543
+ key,
544
+ fingerprint,
545
+ platform: platformName,
546
+ hostname
547
+ })
548
+ });
549
+ if (response.status >= 500) {
550
+ throw new Error(`Registry server error (${response.status}). Try again later.`);
551
+ }
552
+ if (!response.ok) {
553
+ const body = await response.text().catch(() => "");
554
+ let errorMessage = `Activation failed: ${response.status}`;
555
+ try {
556
+ const parsed = JSON.parse(body);
557
+ if (parsed.error) errorMessage = parsed.error;
558
+ } catch {
559
+ if (body) errorMessage = `Activation failed (${response.status}): ${body}`;
560
+ }
561
+ throw new Error(errorMessage);
562
+ }
563
+ return response.json();
564
+ }
565
+ async function deactivateDevice(key, fingerprint) {
566
+ const response = await registryFetch("/api/auth/deactivate", {
567
+ method: "POST",
568
+ headers: { "Content-Type": "application/json" },
569
+ body: JSON.stringify({ key, fingerprint })
570
+ });
571
+ if (response.status >= 500) {
572
+ throw new Error(`Registry server error (${response.status}). Try again later.`);
573
+ }
574
+ if (!response.ok) {
575
+ const body = await response.text().catch(() => "");
576
+ let errorMessage = `Deactivation failed: ${response.status}`;
577
+ try {
578
+ const parsed = JSON.parse(body);
579
+ if (parsed.error) errorMessage = parsed.error;
580
+ } catch {
581
+ if (body) errorMessage = `Deactivation failed (${response.status}): ${body}`;
582
+ }
583
+ throw new Error(errorMessage);
584
+ }
585
+ return response.json();
586
+ }
587
+
588
+ // src/commands/auth.ts
589
+ function readInput(question) {
590
+ const rl = (0, import_readline.createInterface)({
591
+ input: process.stdin,
592
+ output: process.stdout
593
+ });
594
+ return new Promise((resolve2, reject) => {
595
+ rl.on("error", (err) => {
596
+ rl.close();
597
+ reject(err);
598
+ });
599
+ rl.on("close", () => {
600
+ resolve2("");
601
+ });
602
+ rl.question(question, (answer) => {
603
+ rl.close();
604
+ resolve2(answer.trim());
605
+ });
606
+ });
607
+ }
608
+ function maskEmail(email) {
609
+ const [local, domain] = email.split("@");
610
+ if (!local || !domain) return "***@***";
611
+ const visible = local.slice(0, 2);
612
+ return `${visible}***@${domain}`;
613
+ }
614
+ function authCommand() {
615
+ const auth = new import_commander.Command("auth").description(
616
+ "Manage license key and device registration"
617
+ );
618
+ auth.command("login").description("Activate your license key on this device").option("--json", "Output JSON").action(async (opts) => {
619
+ const existing = await retrieveLicenseKey();
620
+ if (existing) {
621
+ if (opts.json) {
622
+ console.log(JSON.stringify({ ok: false, error: "Already logged in" }));
623
+ } else {
624
+ console.log("Already logged in. Run `pushrec-skills auth logout` first to switch keys.");
625
+ }
626
+ return;
627
+ }
628
+ const key = await readInput("Enter your license key: ");
629
+ if (!key) {
630
+ if (opts.json) {
631
+ console.log(JSON.stringify({ ok: false, error: "No license key provided" }));
632
+ } else {
633
+ console.error("No license key provided.");
634
+ }
635
+ process.exit(1);
636
+ }
637
+ if (!opts.json) console.log("Verifying license...");
638
+ const offlineResult = await verifyOffline(key);
639
+ if (!offlineResult.valid) {
640
+ if (opts.json) {
641
+ console.log(JSON.stringify({ ok: false, error: `Invalid license key: ${offlineResult.error}` }));
642
+ } else {
643
+ console.error(`Invalid license key: ${offlineResult.error}`);
644
+ }
645
+ process.exit(1);
646
+ }
647
+ const fingerprint = getMachineFingerprint();
648
+ let onlineResult;
649
+ try {
650
+ onlineResult = await verifyLicenseOnline(key, fingerprint);
651
+ } catch (err) {
652
+ const message = err instanceof Error ? err.message : String(err);
653
+ if (opts.json) {
654
+ console.log(JSON.stringify({ ok: false, error: `Could not reach registry server: ${message}` }));
655
+ } else {
656
+ console.error(`Could not reach registry server: ${message}`);
657
+ console.error("Check your internet connection and try again.");
658
+ }
659
+ process.exit(1);
660
+ }
661
+ if (!onlineResult.valid) {
662
+ if (opts.json) {
663
+ console.log(JSON.stringify({ ok: false, error: `License rejected: ${onlineResult.error}` }));
664
+ } else {
665
+ console.error(`License rejected by server: ${onlineResult.error}`);
666
+ }
667
+ process.exit(1);
668
+ }
669
+ await storeLicenseKey(key);
670
+ if (!opts.json) console.log("Activating device...");
671
+ let activation;
672
+ try {
673
+ activation = await activateDevice(
674
+ key,
675
+ fingerprint,
676
+ getPlatformName(),
677
+ getHostname()
678
+ );
679
+ if (!opts.json) {
680
+ console.log(
681
+ `Device activated! ${activation.deviceCount}/${activation.maxDevices} devices registered.`
682
+ );
683
+ }
684
+ } catch (err) {
685
+ await deleteLicenseKey();
686
+ const message = err instanceof Error ? err.message : String(err);
687
+ if (opts.json) {
688
+ console.log(JSON.stringify({ ok: false, error: `Device activation failed: ${message}` }));
689
+ } else {
690
+ console.error(`Device activation failed: ${message}`);
691
+ }
692
+ process.exit(1);
693
+ }
694
+ const cache = {
695
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
696
+ expiresAt: onlineResult.updatesExpireAt ?? null,
697
+ fingerprint,
698
+ payload: {
699
+ buyerId: onlineResult.buyerId ?? 0,
700
+ email: onlineResult.email ?? "",
701
+ maxDevices: onlineResult.maxDevices ?? 3,
702
+ status: onlineResult.status ?? "active",
703
+ hasUpdates: onlineResult.hasUpdates ?? true,
704
+ deviceCount: onlineResult.deviceCount ?? 1
705
+ }
706
+ };
707
+ saveLicenseCache(cache);
708
+ if (opts.json) {
709
+ console.log(JSON.stringify({ ok: true, activated: true }));
710
+ } else {
711
+ console.log(import_chalk.default.green("\u2713 License activated!"));
712
+ console.log("");
713
+ console.log("Next steps:");
714
+ console.log(` Install all skills: ${import_chalk.default.cyan("npx @pushrec/skills install --all")}`);
715
+ console.log(` Browse catalog: ${import_chalk.default.cyan("npx @pushrec/skills list")}`);
716
+ console.log(` Install one skill: ${import_chalk.default.cyan("npx @pushrec/skills install <skill-name>")}`);
717
+ }
718
+ });
719
+ auth.command("logout").description("Remove license key and deactivate this device").option("--json", "Output JSON").action(async (opts) => {
720
+ const key = await retrieveLicenseKey();
721
+ if (!key) {
722
+ if (opts.json) {
723
+ console.log(JSON.stringify({ ok: true, message: "Not logged in" }));
724
+ } else {
725
+ console.log("Not logged in.");
726
+ }
727
+ return;
728
+ }
729
+ const fingerprint = getMachineFingerprint();
730
+ try {
731
+ await deactivateDevice(key, fingerprint);
732
+ if (!opts.json) console.log("Device deactivated.");
733
+ } catch {
734
+ if (!opts.json) console.log("Could not reach server \u2014 device will auto-expire after 90 days.");
735
+ }
736
+ await deleteLicenseKey();
737
+ deleteLicenseCache();
738
+ if (opts.json) {
739
+ console.log(JSON.stringify({ ok: true }));
740
+ } else {
741
+ console.log("License key removed.");
742
+ }
743
+ });
744
+ auth.command("status").description("Show current license and device status").option("--json", "Output JSON").action(async (opts) => {
745
+ const key = await retrieveLicenseKey();
746
+ if (!key) {
747
+ if (opts.json) {
748
+ console.log(JSON.stringify({ loggedIn: false, error: "Not logged in" }));
749
+ } else {
750
+ console.log("Not logged in. Run `pushrec-skills auth login` to activate.");
751
+ }
752
+ return;
753
+ }
754
+ const cache = loadLicenseCache();
755
+ if (!cache) {
756
+ if (opts.json) {
757
+ console.log(JSON.stringify({ loggedIn: true, error: "No cached verification" }));
758
+ } else {
759
+ console.log("License key found but no cached verification.");
760
+ console.log("Run `pushrec-skills auth login` to re-verify.");
761
+ }
762
+ return;
763
+ }
764
+ const offlineStatus = checkOfflineStatus(cache);
765
+ const { payload } = cache;
766
+ if (offlineStatus.status === "stale" || offlineStatus.status === "warning") {
767
+ try {
768
+ const fingerprint = getMachineFingerprint();
769
+ const fresh = await verifyLicenseOnline(key, fingerprint);
770
+ if (fresh.valid) {
771
+ const updated = {
772
+ ...cache,
773
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
774
+ expiresAt: fresh.updatesExpireAt ?? null,
775
+ payload: {
776
+ buyerId: fresh.buyerId ?? cache.payload.buyerId,
777
+ email: fresh.email ?? cache.payload.email,
778
+ maxDevices: fresh.maxDevices ?? cache.payload.maxDevices,
779
+ status: fresh.status ?? cache.payload.status,
780
+ hasUpdates: fresh.hasUpdates ?? cache.payload.hasUpdates,
781
+ deviceCount: fresh.deviceCount ?? cache.payload.deviceCount
782
+ }
783
+ };
784
+ saveLicenseCache(updated);
785
+ if (!opts.json) console.log("\nLicense re-verified successfully.");
786
+ }
787
+ } catch {
788
+ }
789
+ }
790
+ if (opts.json) {
791
+ console.log(JSON.stringify({
792
+ loggedIn: true,
793
+ license: payload.status,
794
+ email: maskEmail(payload.email),
795
+ devices: { count: payload.deviceCount, max: payload.maxDevices },
796
+ updates: {
797
+ active: payload.hasUpdates,
798
+ expiresAt: cache.expiresAt ?? null
799
+ },
800
+ verifiedAt: cache.verifiedAt,
801
+ offlineStatus: offlineStatus.status
802
+ }));
803
+ return;
804
+ }
805
+ console.log(`License: ${payload.status}`);
806
+ console.log(`Email: ${maskEmail(payload.email)}`);
807
+ console.log(`Devices: ${payload.deviceCount}/${payload.maxDevices}`);
808
+ console.log(`Updates: ${payload.hasUpdates ? "active" : "inactive"}${cache.expiresAt ? ` (expires ${cache.expiresAt.split("T")[0]})` : ""}`);
809
+ console.log(`Verified: ${cache.verifiedAt.split("T")[0]}`);
810
+ if (offlineStatus.message) {
811
+ console.log(`
812
+ Warning: ${offlineStatus.message}`);
813
+ }
814
+ });
815
+ auth.command("deactivate-device").description("Remove this device from your license (frees a device slot)").option("-y, --yes", "Skip confirmation prompt").option("--json", "Output JSON").action(async (opts) => {
816
+ const key = await retrieveLicenseKey();
817
+ if (!key) {
818
+ if (opts.json) {
819
+ console.log(JSON.stringify({ ok: false, error: "Not logged in" }));
820
+ } else {
821
+ console.log("Not logged in.");
822
+ }
823
+ return;
824
+ }
825
+ if (!opts.yes && !opts.json) {
826
+ const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
827
+ const answer = await rl.question("Deactivate this device? This uses a device slot (auto-expires in 90 days). [y/N] ");
828
+ rl.close();
829
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
830
+ console.log("Cancelled.");
831
+ return;
832
+ }
833
+ }
834
+ const fingerprint = getMachineFingerprint();
835
+ try {
836
+ const result = await deactivateDevice(key, fingerprint);
837
+ if (opts.json) {
838
+ console.log(JSON.stringify({ ok: true, deviceCount: result.deviceCount, maxDevices: result.maxDevices }));
839
+ } else {
840
+ console.log(
841
+ `Device deactivated. ${result.deviceCount}/${result.maxDevices} devices registered.`
842
+ );
843
+ }
844
+ } catch (err) {
845
+ const message = err instanceof Error ? err.message : String(err);
846
+ if (opts.json) {
847
+ console.log(JSON.stringify({ ok: false, error: message }));
848
+ } else {
849
+ console.error(`Deactivation failed: ${message}`);
850
+ }
851
+ process.exit(1);
852
+ }
853
+ });
854
+ return auth;
855
+ }
856
+
857
+ // src/commands/install.ts
858
+ var import_commander2 = require("commander");
859
+ var import_ora = __toESM(require("ora"));
860
+
861
+ // src/lib/installer.ts
862
+ var import_fs5 = require("fs");
863
+ var import_path5 = require("path");
864
+ var import_child_process3 = require("child_process");
865
+ var import_os4 = require("os");
866
+ var import_crypto2 = require("crypto");
867
+ async function installSkill(name, archive) {
868
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name) || name.includes("..")) {
869
+ throw new Error(`Invalid skill name: "${name}"`);
870
+ }
871
+ const targetDir = (0, import_path5.join)(SKILLS_DIR, name);
872
+ const tmpId = (0, import_crypto2.randomBytes)(4).toString("hex");
873
+ const tmpDir = (0, import_path5.join)((0, import_os4.tmpdir)(), `pushrec-install-${tmpId}`);
874
+ try {
875
+ (0, import_fs5.mkdirSync)(tmpDir, { recursive: true });
876
+ const zipPath = (0, import_path5.join)(tmpDir, `${name}.zip`);
877
+ (0, import_fs5.writeFileSync)(zipPath, Buffer.from(archive));
878
+ const extractDir = (0, import_path5.join)(tmpDir, "extracted");
879
+ (0, import_fs5.mkdirSync)(extractDir, { recursive: true });
880
+ extractZip(zipPath, extractDir);
881
+ const skillRoot = findSkillRoot(extractDir, name);
882
+ const resolvedRoot = (0, import_path5.resolve)(skillRoot);
883
+ const resolvedExtract = (0, import_path5.resolve)(extractDir);
884
+ const rel = (0, import_path5.relative)(resolvedExtract, resolvedRoot);
885
+ if (rel.startsWith("..") || (0, import_path5.resolve)(rel) === resolvedRoot) {
886
+ throw new Error("Archive contains path traversal \u2014 aborting install.");
887
+ }
888
+ if (!(0, import_fs5.existsSync)(SKILLS_DIR)) {
889
+ (0, import_fs5.mkdirSync)(SKILLS_DIR, { recursive: true });
890
+ }
891
+ if ((0, import_fs5.existsSync)(targetDir)) {
892
+ (0, import_fs5.rmSync)(targetDir, { recursive: true, force: true });
893
+ }
894
+ (0, import_fs5.cpSync)(skillRoot, targetDir, { recursive: true });
895
+ return { path: targetDir };
896
+ } finally {
897
+ try {
898
+ (0, import_fs5.rmSync)(tmpDir, { recursive: true, force: true });
899
+ } catch {
900
+ }
901
+ }
902
+ }
903
+ function extractZip(zipPath, destDir) {
904
+ const os = (0, import_os4.platform)();
905
+ if (os === "win32") {
906
+ (0, import_child_process3.execFileSync)(
907
+ "powershell",
908
+ ["-NoProfile", "-Command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
909
+ { stdio: "pipe", timeout: 6e4 }
910
+ );
911
+ } else {
912
+ (0, import_child_process3.execFileSync)("unzip", ["-o", "-q", zipPath, "-d", destDir], {
913
+ stdio: "pipe",
914
+ timeout: 6e4
915
+ });
916
+ }
917
+ }
918
+ function findSkillRoot(extractDir, name) {
919
+ const entries = (0, import_fs5.readdirSync)(extractDir);
920
+ const skillDir = entries.find((entry) => {
921
+ const fullPath = (0, import_path5.join)(extractDir, entry);
922
+ return (0, import_fs5.statSync)(fullPath).isDirectory() && (entry === name || entry.startsWith(`${name}-`));
923
+ });
924
+ if (skillDir) {
925
+ return (0, import_path5.join)(extractDir, skillDir);
926
+ }
927
+ if (entries.includes("SKILL.md")) {
928
+ return extractDir;
929
+ }
930
+ const firstDir = entries.find(
931
+ (entry) => (0, import_fs5.statSync)((0, import_path5.join)(extractDir, entry)).isDirectory()
932
+ );
933
+ if (firstDir) {
934
+ return (0, import_path5.join)(extractDir, firstDir);
935
+ }
936
+ throw new Error(
937
+ `Could not find skill root in archive for "${name}". Expected a directory containing SKILL.md.`
938
+ );
939
+ }
940
+ function uninstallSkill(name) {
941
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name) || name.includes("..") || name.includes("/") || name.includes("\\")) {
942
+ throw new Error(`Invalid skill name: "${name}". Only lowercase letters, numbers, and hyphens allowed.`);
943
+ }
944
+ const targetDir = (0, import_path5.join)(SKILLS_DIR, name);
945
+ if (!(0, import_fs5.existsSync)(targetDir)) return false;
946
+ (0, import_fs5.rmSync)(targetDir, { recursive: true, force: true });
947
+ return true;
948
+ }
949
+ function isSkillInstalled(name) {
950
+ return (0, import_fs5.existsSync)((0, import_path5.join)(SKILLS_DIR, name, "SKILL.md"));
951
+ }
952
+
953
+ // src/commands/install.ts
954
+ async function ensureLicense(forFreeSkill) {
955
+ if (forFreeSkill) return true;
956
+ const key = await retrieveLicenseKey();
957
+ if (!key) {
958
+ console.error("No license key found. Run `pushrec-skills auth login` first.");
959
+ return false;
960
+ }
961
+ const offlineResult = await verifyOffline(key);
962
+ if (!offlineResult.valid) {
963
+ console.error(`License invalid: ${offlineResult.error}`);
964
+ return false;
965
+ }
966
+ const cache = loadLicenseCache();
967
+ if (cache) {
968
+ const status = checkOfflineStatus(cache);
969
+ if (status.status === "disabled") {
970
+ console.error(status.message);
971
+ return false;
972
+ }
973
+ if (status.status === "warning") {
974
+ console.log(`Warning: ${status.message}`);
975
+ }
976
+ if (status.status === "stale" || status.status === "warning") {
977
+ try {
978
+ const fingerprint = getMachineFingerprint();
979
+ const fresh = await verifyLicenseOnline(key, fingerprint);
980
+ if (fresh.valid) {
981
+ const updated = {
982
+ ...cache,
983
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
984
+ expiresAt: fresh.updatesExpireAt ?? null,
985
+ payload: {
986
+ buyerId: fresh.buyerId ?? cache.payload.buyerId,
987
+ email: fresh.email ?? cache.payload.email,
988
+ maxDevices: fresh.maxDevices ?? cache.payload.maxDevices,
989
+ status: fresh.status ?? cache.payload.status,
990
+ hasUpdates: fresh.hasUpdates ?? cache.payload.hasUpdates,
991
+ deviceCount: fresh.deviceCount ?? cache.payload.deviceCount
992
+ }
993
+ };
994
+ saveLicenseCache(updated);
995
+ } else {
996
+ console.error(`License rejected: ${fresh.error}`);
997
+ return false;
998
+ }
999
+ } catch {
1000
+ console.log("Could not reach server. Using cached verification.");
1001
+ }
1002
+ }
1003
+ return true;
1004
+ }
1005
+ try {
1006
+ const fingerprint = getMachineFingerprint();
1007
+ const result = await verifyLicenseOnline(key, fingerprint);
1008
+ if (!result.valid) {
1009
+ console.error(`License rejected: ${result.error}`);
1010
+ return false;
1011
+ }
1012
+ const newCache = {
1013
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
1014
+ expiresAt: result.updatesExpireAt ?? null,
1015
+ fingerprint,
1016
+ payload: {
1017
+ buyerId: result.buyerId ?? 0,
1018
+ email: result.email ?? "",
1019
+ maxDevices: result.maxDevices ?? 3,
1020
+ status: result.status ?? "active",
1021
+ hasUpdates: result.hasUpdates ?? true,
1022
+ deviceCount: result.deviceCount ?? 1
1023
+ }
1024
+ };
1025
+ saveLicenseCache(newCache);
1026
+ return true;
1027
+ } catch (err) {
1028
+ console.error(
1029
+ `Could not verify license: ${err instanceof Error ? err.message : String(err)}`
1030
+ );
1031
+ return false;
1032
+ }
1033
+ }
1034
+ async function installOne(name, prefix) {
1035
+ const spinner = (0, import_ora.default)(`${prefix}Fetching ${name}...`).start();
1036
+ try {
1037
+ const manifest = await fetchSkillManifest(name);
1038
+ spinner.text = `${prefix}Verifying license...`;
1039
+ const licensed = await ensureLicense(manifest.isFree);
1040
+ if (!licensed) {
1041
+ spinner.fail(`${prefix}License check failed for ${name}`);
1042
+ return { ok: false, error: "License check failed" };
1043
+ }
1044
+ spinner.text = `${prefix}Downloading ${name} v${manifest.currentVersion ?? "latest"}...`;
1045
+ const archive = await downloadSkill(name);
1046
+ spinner.text = `${prefix}Installing ${name}...`;
1047
+ const { path } = await installSkill(name, archive);
1048
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1049
+ const config = loadConfig();
1050
+ config.installedSkills[name] = {
1051
+ name,
1052
+ version: manifest.currentVersion ?? "0.0.0",
1053
+ installedAt: config.installedSkills[name]?.installedAt ?? now,
1054
+ updatedAt: now
1055
+ };
1056
+ updateConfig({ installedSkills: config.installedSkills });
1057
+ spinner.succeed(`${prefix}${name} v${manifest.currentVersion ?? "latest"} installed to ${path}`);
1058
+ return { ok: true, path, version: manifest.currentVersion ?? "latest" };
1059
+ } catch (err) {
1060
+ const message = err instanceof Error ? err.message : String(err);
1061
+ spinner.fail(`${prefix}Failed to install ${name}: ${message}`);
1062
+ return { ok: false, error: message };
1063
+ }
1064
+ }
1065
+ function installCommand() {
1066
+ const cmd = new import_commander2.Command("install").description("Install skill(s) from the Pushrec registry").argument("[name]", "Skill name to install").option("--all", "Install all available skills").option("--force", "Reinstall even if already installed").option("--json", "Output JSON").action(async (name, opts) => {
1067
+ if (!name && !opts.all) {
1068
+ if (opts.json) {
1069
+ console.log(JSON.stringify({ ok: false, error: "Specify a skill name or use --all" }));
1070
+ process.exit(1);
1071
+ }
1072
+ console.error("Specify a skill name or use --all to install everything.");
1073
+ console.error("Usage: pushrec-skills install <name> | pushrec-skills install --all");
1074
+ process.exit(1);
1075
+ }
1076
+ if (name && opts.all) {
1077
+ if (opts.json) {
1078
+ console.log(JSON.stringify({ ok: false, error: "Cannot specify both a skill name and --all" }));
1079
+ process.exit(1);
1080
+ }
1081
+ console.error("Cannot specify both a skill name and --all.");
1082
+ process.exit(1);
1083
+ }
1084
+ if (name) {
1085
+ if (!opts.force && isSkillInstalled(name)) {
1086
+ if (opts.json) {
1087
+ console.log(JSON.stringify({ ok: true, skill: name, skipped: true, reason: "already installed" }));
1088
+ } else {
1089
+ console.log(`${name} is already installed. Use --force to reinstall.`);
1090
+ }
1091
+ return;
1092
+ }
1093
+ const result = await installOne(name, "");
1094
+ if (!result.ok) {
1095
+ if (opts.json) {
1096
+ console.log(JSON.stringify({ ok: false, skill: name, error: result.error }));
1097
+ }
1098
+ process.exit(1);
1099
+ }
1100
+ if (opts.json) {
1101
+ console.log(JSON.stringify({ ok: true, skill: name, version: result.version, path: result.path }));
1102
+ } else {
1103
+ console.log(`
1104
+ Done! Use /${name} in your next Claude Code session.`);
1105
+ }
1106
+ return;
1107
+ }
1108
+ if (!opts.json) console.log("Fetching skill catalog...");
1109
+ let catalog;
1110
+ try {
1111
+ catalog = await fetchSkillCatalog();
1112
+ } catch (err) {
1113
+ const message = err instanceof Error ? err.message : String(err);
1114
+ if (opts.json) {
1115
+ console.log(JSON.stringify({ ok: false, error: `Could not fetch catalog: ${message}` }));
1116
+ } else {
1117
+ console.error(`Could not fetch catalog: ${message}`);
1118
+ }
1119
+ process.exit(1);
1120
+ }
1121
+ const skills = catalog.items;
1122
+ const toInstall = opts.force ? skills : skills.filter((s) => !isSkillInstalled(s.name));
1123
+ if (toInstall.length === 0) {
1124
+ if (opts.json) {
1125
+ console.log(JSON.stringify({ ok: true, installed: 0, failed: 0, skipped: skills.length }));
1126
+ } else {
1127
+ console.log("All skills are already installed.");
1128
+ }
1129
+ return;
1130
+ }
1131
+ if (!opts.json) console.log(`Installing ${toInstall.length} skills...
1132
+ `);
1133
+ let installed = 0;
1134
+ let failed = 0;
1135
+ const results = [];
1136
+ for (let i = 0; i < toInstall.length; i++) {
1137
+ const skill = toInstall[i];
1138
+ const prefix = opts.json ? "" : `[${i + 1}/${toInstall.length}] `;
1139
+ const result = await installOne(skill.name, prefix);
1140
+ results.push({ skill: skill.name, ok: result.ok, version: result.version, error: result.error });
1141
+ if (result.ok) {
1142
+ installed++;
1143
+ } else {
1144
+ failed++;
1145
+ }
1146
+ }
1147
+ if (opts.json) {
1148
+ console.log(JSON.stringify({ ok: failed === 0, installed, failed, results }));
1149
+ } else {
1150
+ console.log(`
1151
+ Done! ${installed} installed, ${failed} failed.`);
1152
+ }
1153
+ if (failed > 0) process.exit(1);
1154
+ });
1155
+ return cmd;
1156
+ }
1157
+
1158
+ // src/commands/update.ts
1159
+ var import_commander3 = require("commander");
1160
+ async function updateOne(name, currentVersion, prefix, json) {
1161
+ try {
1162
+ const manifest = await fetchSkillManifest(name);
1163
+ const latest = manifest.currentVersion;
1164
+ if (!latest || latest === currentVersion) {
1165
+ if (!json) console.log(`${prefix}${name} is up to date (v${currentVersion}).`);
1166
+ return true;
1167
+ }
1168
+ if (!json) console.log(`${prefix}Updating ${name} v${currentVersion} -> v${latest}...`);
1169
+ const archive = await downloadSkill(name);
1170
+ await installSkill(name, archive);
1171
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1172
+ const config = loadConfig();
1173
+ if (config.installedSkills[name]) {
1174
+ config.installedSkills[name].version = latest;
1175
+ config.installedSkills[name].updatedAt = now;
1176
+ updateConfig({ installedSkills: config.installedSkills });
1177
+ }
1178
+ if (!json) console.log(`${prefix}Updated ${name} to v${latest}.`);
1179
+ return true;
1180
+ } catch (err) {
1181
+ if (!json) {
1182
+ console.error(
1183
+ `${prefix}Failed to update ${name}: ${err instanceof Error ? err.message : String(err)}`
1184
+ );
1185
+ }
1186
+ return false;
1187
+ }
1188
+ }
1189
+ function updateCommand() {
1190
+ const cmd = new import_commander3.Command("update").description("Update installed skills to latest versions").argument("[name]", "Specific skill to update (updates all if omitted)").option("--json", "Output JSON").action(async (name, opts) => {
1191
+ const key = await retrieveLicenseKey();
1192
+ if (key) {
1193
+ const offlineResult = await verifyOffline(key);
1194
+ if (!offlineResult.valid) {
1195
+ if (opts.json) {
1196
+ console.log(JSON.stringify({ ok: false, error: `License invalid: ${offlineResult.error}` }));
1197
+ } else {
1198
+ console.error(`License invalid: ${offlineResult.error}`);
1199
+ }
1200
+ process.exit(1);
1201
+ }
1202
+ }
1203
+ const cache = loadLicenseCache();
1204
+ if (cache && !cache.payload.hasUpdates) {
1205
+ if (opts.json) {
1206
+ console.log(JSON.stringify({ ok: false, error: "Updates not included in your license. Visit pushrec.com to upgrade." }));
1207
+ } else {
1208
+ console.error("Updates not included in your license. Visit pushrec.com to upgrade.");
1209
+ }
1210
+ process.exit(1);
1211
+ }
1212
+ const config = loadConfig();
1213
+ const installed = config.installedSkills;
1214
+ if (Object.keys(installed).length === 0) {
1215
+ if (opts.json) {
1216
+ console.log(JSON.stringify({ ok: true, updated: 0, upToDate: 0, failed: 0 }));
1217
+ } else {
1218
+ console.log("No skills installed. Run `pushrec-skills install <name>` first.");
1219
+ }
1220
+ return;
1221
+ }
1222
+ if (name) {
1223
+ const skill = installed[name];
1224
+ if (!skill) {
1225
+ if (opts.json) {
1226
+ console.log(JSON.stringify({ ok: false, error: `${name} is not installed` }));
1227
+ } else {
1228
+ console.error(`${name} is not installed.`);
1229
+ }
1230
+ process.exit(1);
1231
+ }
1232
+ const ok = await updateOne(name, skill.version, "", !!opts.json);
1233
+ if (opts.json) {
1234
+ console.log(JSON.stringify({ ok, skill: name }));
1235
+ }
1236
+ if (!ok) process.exit(1);
1237
+ return;
1238
+ }
1239
+ if (!opts.json) console.log("Checking for updates...\n");
1240
+ const entries = Object.entries(installed);
1241
+ let updated = 0;
1242
+ let failed = 0;
1243
+ let upToDate = 0;
1244
+ const results = [];
1245
+ for (let i = 0; i < entries.length; i++) {
1246
+ const [skillName, skill] = entries[i];
1247
+ const prefix = opts.json ? "" : `[${i + 1}/${entries.length}] `;
1248
+ try {
1249
+ const manifest = await fetchSkillManifest(skillName);
1250
+ const latest = manifest.currentVersion;
1251
+ if (!latest || latest === skill.version) {
1252
+ upToDate++;
1253
+ results.push({ skill: skillName, ok: true });
1254
+ continue;
1255
+ }
1256
+ const ok = await updateOne(skillName, skill.version, prefix, !!opts.json);
1257
+ results.push({ skill: skillName, ok });
1258
+ if (ok) {
1259
+ updated++;
1260
+ } else {
1261
+ failed++;
1262
+ }
1263
+ } catch {
1264
+ if (!opts.json) console.log(`${prefix}${skillName}: could not check for updates.`);
1265
+ results.push({ skill: skillName, ok: false, error: "Could not check for updates" });
1266
+ failed++;
1267
+ }
1268
+ }
1269
+ if (opts.json) {
1270
+ console.log(JSON.stringify({ ok: failed === 0, updated, upToDate, failed, results }));
1271
+ } else {
1272
+ console.log(
1273
+ `
1274
+ Done! ${updated} updated, ${upToDate} up to date, ${failed} failed.`
1275
+ );
1276
+ }
1277
+ if (failed > 0) process.exit(1);
1278
+ });
1279
+ return cmd;
1280
+ }
1281
+
1282
+ // src/commands/list.ts
1283
+ var import_commander4 = require("commander");
1284
+ function pad(str, width) {
1285
+ return str.length >= width ? str.slice(0, width) : str + " ".repeat(width - str.length);
1286
+ }
1287
+ function listCommand() {
1288
+ const cmd = new import_commander4.Command("list").description("List available or installed skills").option("--installed", "Show only installed skills").option("--json", "Output as JSON").action(async (opts) => {
1289
+ if (opts.installed) {
1290
+ const config = loadConfig();
1291
+ const entries = Object.values(config.installedSkills);
1292
+ if (entries.length === 0) {
1293
+ console.log("No skills installed. Run `pushrec-skills install <name>` to get started.");
1294
+ return;
1295
+ }
1296
+ if (opts.json) {
1297
+ console.log(JSON.stringify(entries, null, 2));
1298
+ return;
1299
+ }
1300
+ console.log(
1301
+ `${pad("NAME", 30)} ${pad("VERSION", 12)} ${pad("INSTALLED", 12)} UPDATED`
1302
+ );
1303
+ console.log("-".repeat(78));
1304
+ for (const skill of entries) {
1305
+ console.log(
1306
+ `${pad(skill.name, 30)} ${pad(skill.version, 12)} ${pad(skill.installedAt.split("T")[0], 12)} ${skill.updatedAt.split("T")[0]}`
1307
+ );
1308
+ }
1309
+ console.log(`
1310
+ ${entries.length} skills installed.`);
1311
+ return;
1312
+ }
1313
+ console.log("Fetching skill catalog...\n");
1314
+ let catalog;
1315
+ try {
1316
+ catalog = await fetchSkillCatalog();
1317
+ } catch (err) {
1318
+ console.error(
1319
+ `Could not fetch catalog: ${err instanceof Error ? err.message : String(err)}`
1320
+ );
1321
+ process.exit(1);
1322
+ }
1323
+ if (opts.json) {
1324
+ console.log(JSON.stringify(catalog.items, null, 2));
1325
+ return;
1326
+ }
1327
+ console.log(
1328
+ `${pad("NAME", 30)} ${pad("VERSION", 12)} ${pad("STATUS", 14)} CATEGORY`
1329
+ );
1330
+ console.log("-".repeat(78));
1331
+ for (const skill of catalog.items) {
1332
+ const installed = isSkillInstalled(skill.name);
1333
+ const status = skill.isFree ? installed ? "free/installed" : "free" : installed ? "installed" : "available";
1334
+ console.log(
1335
+ `${pad(skill.name, 30)} ${pad(skill.currentVersion ?? "-", 12)} ${pad(status, 14)} ${skill.category ?? "-"}`
1336
+ );
1337
+ }
1338
+ console.log(`
1339
+ ${catalog.total} skills available.`);
1340
+ });
1341
+ return cmd;
1342
+ }
1343
+
1344
+ // src/commands/search.ts
1345
+ var import_commander5 = require("commander");
1346
+ function pad2(str, width) {
1347
+ return str.length >= width ? str.slice(0, width) : str + " ".repeat(width - str.length);
1348
+ }
1349
+ function searchCommand() {
1350
+ const cmd = new import_commander5.Command("search").description("Search skills by keyword").argument("<query>", "Search term (matches name, description, category)").option("--json", "Output as JSON").action(async (query, opts) => {
1351
+ let catalog;
1352
+ try {
1353
+ catalog = await fetchSkillCatalog();
1354
+ } catch (err) {
1355
+ console.error(
1356
+ `Could not fetch catalog: ${err instanceof Error ? err.message : String(err)}`
1357
+ );
1358
+ process.exit(1);
1359
+ }
1360
+ const lower = query.toLowerCase();
1361
+ const matches = catalog.items.filter((skill) => {
1362
+ const name = skill.name.toLowerCase();
1363
+ const desc = (skill.description ?? "").toLowerCase();
1364
+ const cat = (skill.category ?? "").toLowerCase();
1365
+ const display = (skill.displayName ?? "").toLowerCase();
1366
+ return name.includes(lower) || desc.includes(lower) || cat.includes(lower) || display.includes(lower);
1367
+ });
1368
+ if (matches.length === 0) {
1369
+ console.log(`No skills matching "${query}".`);
1370
+ return;
1371
+ }
1372
+ if (opts.json) {
1373
+ console.log(JSON.stringify(matches, null, 2));
1374
+ return;
1375
+ }
1376
+ console.log(
1377
+ `${pad2("NAME", 30)} ${pad2("VERSION", 12)} ${pad2("STATUS", 14)} DESCRIPTION`
1378
+ );
1379
+ console.log("-".repeat(90));
1380
+ for (const skill of matches) {
1381
+ const installed = isSkillInstalled(skill.name);
1382
+ const status = skill.isFree ? installed ? "free/installed" : "free" : installed ? "installed" : "available";
1383
+ const desc = skill.description ? skill.description.slice(0, 40) + (skill.description.length > 40 ? "..." : "") : "-";
1384
+ console.log(
1385
+ `${pad2(skill.name, 30)} ${pad2(skill.currentVersion ?? "-", 12)} ${pad2(status, 14)} ${desc}`
1386
+ );
1387
+ }
1388
+ console.log(`
1389
+ ${matches.length} skills found.`);
1390
+ });
1391
+ return cmd;
1392
+ }
1393
+
1394
+ // src/commands/info.ts
1395
+ var import_commander6 = require("commander");
1396
+ function infoCommand() {
1397
+ const cmd = new import_commander6.Command("info").description("Show detailed information about a skill").argument("<name>", "Skill name").option("--json", "Output as JSON").action(async (name, opts) => {
1398
+ let manifest;
1399
+ try {
1400
+ manifest = await fetchSkillManifest(name);
1401
+ } catch (err) {
1402
+ console.error(
1403
+ err instanceof Error ? err.message : `Could not fetch info for "${name}"`
1404
+ );
1405
+ process.exit(1);
1406
+ }
1407
+ if (opts.json) {
1408
+ console.log(JSON.stringify(manifest, null, 2));
1409
+ return;
1410
+ }
1411
+ const installed = isSkillInstalled(name);
1412
+ const config = loadConfig();
1413
+ const installedInfo = config.installedSkills[name];
1414
+ console.log(`${manifest.displayName ?? manifest.name} v${manifest.currentVersion ?? "?"}`);
1415
+ console.log("");
1416
+ if (manifest.description) {
1417
+ console.log(` ${manifest.description}`);
1418
+ console.log("");
1419
+ }
1420
+ console.log(` Category: ${manifest.category ?? "-"}`);
1421
+ console.log(` Free: ${manifest.isFree ? "yes" : "no"}`);
1422
+ console.log(` Published: ${manifest.publishedAt ? manifest.publishedAt.split("T")[0] : "-"}`);
1423
+ console.log(` Installed: ${installed ? `yes (v${installedInfo?.version ?? "?"})` : "no"}`);
1424
+ if (manifest.versions && manifest.versions.length > 0) {
1425
+ console.log("");
1426
+ console.log(" Versions:");
1427
+ for (const ver of manifest.versions.slice(0, 10)) {
1428
+ const date = ver.publishedAt.split("T")[0];
1429
+ const changelog = ver.changelog ? ` \u2014 ${ver.changelog}` : "";
1430
+ console.log(` ${ver.version} (${date})${changelog}`);
1431
+ }
1432
+ if (manifest.versions.length > 10) {
1433
+ console.log(` ... and ${manifest.versions.length - 10} more`);
1434
+ }
1435
+ }
1436
+ });
1437
+ return cmd;
1438
+ }
1439
+
1440
+ // src/commands/uninstall.ts
1441
+ var import_commander7 = require("commander");
1442
+ var import_chalk2 = __toESM(require("chalk"));
1443
+ function createUninstallCommand() {
1444
+ return new import_commander7.Command("uninstall").description("Uninstall a skill from ~/.claude/skills/").argument("<skill-name>", "Name of skill to uninstall").option("--json", "Output JSON").action(async (skillName, opts) => {
1445
+ try {
1446
+ const removed = uninstallSkill(skillName);
1447
+ if (!removed) {
1448
+ if (opts.json) {
1449
+ console.log(JSON.stringify({ ok: false, error: `Skill "${skillName}" is not installed` }));
1450
+ process.exit(1);
1451
+ }
1452
+ console.error(import_chalk2.default.red(`\u2717 Skill "${skillName}" is not installed`));
1453
+ process.exit(1);
1454
+ }
1455
+ if (opts.json) {
1456
+ console.log(JSON.stringify({ ok: true, skill: skillName }));
1457
+ } else {
1458
+ console.log(import_chalk2.default.green(`\u2713 Uninstalled ${skillName}`));
1459
+ }
1460
+ } catch (err) {
1461
+ if (opts.json) {
1462
+ console.log(JSON.stringify({ ok: false, error: err.message }));
1463
+ process.exit(1);
1464
+ }
1465
+ console.error(import_chalk2.default.red(`\u2717 ${err.message}`));
1466
+ process.exit(1);
1467
+ }
1468
+ });
1469
+ }
1470
+
1471
+ // src/commands/health.ts
1472
+ var import_commander8 = require("commander");
1473
+ function healthCommand() {
1474
+ const cmd = new import_commander8.Command("health").description("Check registry server health").option("--json", "Output as JSON").action(async (opts) => {
1475
+ let response;
1476
+ try {
1477
+ response = await registryFetch("/api/health");
1478
+ } catch (err) {
1479
+ console.error(
1480
+ `Health check failed: ${err instanceof Error ? err.message : String(err)}`
1481
+ );
1482
+ process.exit(1);
1483
+ }
1484
+ const body = await response.json().catch(() => null);
1485
+ if (opts.json) {
1486
+ console.log(
1487
+ JSON.stringify(
1488
+ { status: response.status, ok: response.ok, body },
1489
+ null,
1490
+ 2
1491
+ )
1492
+ );
1493
+ return;
1494
+ }
1495
+ if (response.ok) {
1496
+ console.log(`Registry: OK (${response.status})`);
1497
+ if (body && typeof body === "object") {
1498
+ for (const [key, value] of Object.entries(body)) {
1499
+ if (key !== "status") {
1500
+ console.log(` ${key}: ${value}`);
1501
+ }
1502
+ }
1503
+ }
1504
+ } else {
1505
+ console.error(`Registry: UNHEALTHY (${response.status})`);
1506
+ if (body && typeof body === "object") {
1507
+ const errBody = body;
1508
+ if (errBody.error) {
1509
+ console.error(` error: ${errBody.error}`);
1510
+ }
1511
+ }
1512
+ process.exit(1);
1513
+ }
1514
+ });
1515
+ return cmd;
1516
+ }
1517
+
1518
+ // src/index.ts
1519
+ var program = new import_commander9.Command();
1520
+ program.name("pushrec-skills").description(
1521
+ "Install, update, and manage premium Claude Code skills from Pushrec"
1522
+ ).version("0.1.0");
1523
+ program.addCommand(authCommand());
1524
+ program.addCommand(installCommand());
1525
+ program.addCommand(updateCommand());
1526
+ program.addCommand(listCommand());
1527
+ program.addCommand(searchCommand());
1528
+ program.addCommand(infoCommand());
1529
+ program.addCommand(createUninstallCommand());
1530
+ program.addCommand(healthCommand());
1531
+ program.parseAsync().catch((err) => {
1532
+ console.error(err instanceof Error ? err.message : String(err));
1533
+ process.exit(1);
1534
+ });