@pushrec/skills 0.2.0 → 0.3.4

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 CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/index.ts
27
- var import_commander10 = require("commander");
27
+ var import_commander13 = require("commander");
28
28
 
29
29
  // src/commands/auth.ts
30
30
  var import_commander = require("commander");
@@ -114,6 +114,17 @@ async function storeInKeychain(key) {
114
114
  }
115
115
  } catch {
116
116
  }
117
+ if (os === "win32") {
118
+ try {
119
+ (0, import_child_process.execFileSync)(
120
+ "cmdkey",
121
+ [`/generic:${KEYCHAIN_SERVICE}`, `/user:${KEYCHAIN_ACCOUNT}`, `/pass:${key}`],
122
+ { stdio: "pipe", timeout: 5e3 }
123
+ );
124
+ } catch {
125
+ }
126
+ return false;
127
+ }
117
128
  return false;
118
129
  }
119
130
  async function retrieveFromKeychain() {
@@ -181,6 +192,16 @@ async function deleteFromKeychain() {
181
192
  { stdio: "pipe", timeout: 5e3 }
182
193
  );
183
194
  }
195
+ if (os === "win32") {
196
+ try {
197
+ (0, import_child_process.execFileSync)(
198
+ "cmdkey",
199
+ [`/delete:${KEYCHAIN_SERVICE}`],
200
+ { stdio: "pipe", timeout: 5e3 }
201
+ );
202
+ } catch {
203
+ }
204
+ }
184
205
  } catch {
185
206
  }
186
207
  }
@@ -190,7 +211,18 @@ function storeInFile(key) {
190
211
  (0, import_fs.mkdirSync)(dir, { recursive: true });
191
212
  }
192
213
  (0, import_fs.writeFileSync)(CREDENTIALS_FILE, key, "utf-8");
193
- (0, import_fs.chmodSync)(CREDENTIALS_FILE, 384);
214
+ if ((0, import_os2.platform)() !== "win32") {
215
+ (0, import_fs.chmodSync)(CREDENTIALS_FILE, 384);
216
+ } else {
217
+ try {
218
+ (0, import_child_process.execFileSync)(
219
+ "icacls",
220
+ [CREDENTIALS_FILE, "/inheritance:r", "/grant:r", `${process.env.USERNAME}:F`],
221
+ { stdio: "pipe", timeout: 5e3 }
222
+ );
223
+ } catch {
224
+ }
225
+ }
194
226
  }
195
227
  function retrieveFromFile() {
196
228
  if (!(0, import_fs.existsSync)(CREDENTIALS_FILE)) return null;
@@ -294,6 +326,8 @@ function getHostname() {
294
326
  // src/lib/license.ts
295
327
  var ed = __toESM(require("@noble/ed25519"));
296
328
  var import_fs3 = require("fs");
329
+ var import_child_process3 = require("child_process");
330
+ var import_os4 = require("os");
297
331
  var import_path3 = require("path");
298
332
  function fromBase64Url(str) {
299
333
  const padded = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -359,7 +393,18 @@ function saveLicenseCache(cache) {
359
393
  (0, import_fs3.mkdirSync)(dir, { recursive: true });
360
394
  }
361
395
  (0, import_fs3.writeFileSync)(LICENSE_CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
362
- (0, import_fs3.chmodSync)(LICENSE_CACHE_FILE, 384);
396
+ if ((0, import_os4.platform)() !== "win32") {
397
+ (0, import_fs3.chmodSync)(LICENSE_CACHE_FILE, 384);
398
+ } else {
399
+ try {
400
+ (0, import_child_process3.execFileSync)(
401
+ "icacls",
402
+ [LICENSE_CACHE_FILE, "/inheritance:r", "/grant:r", `${process.env.USERNAME}:F`],
403
+ { stdio: "pipe", timeout: 5e3 }
404
+ );
405
+ } catch {
406
+ }
407
+ }
363
408
  }
364
409
  function deleteLicenseCache() {
365
410
  try {
@@ -399,35 +444,341 @@ function checkOfflineStatus(cache) {
399
444
 
400
445
  // src/lib/config.ts
401
446
  var import_fs4 = require("fs");
447
+ var import_path5 = require("path");
448
+
449
+ // src/lib/client-roots.ts
450
+ var import_os5 = require("os");
402
451
  var import_path4 = require("path");
403
- function defaultConfig() {
452
+ var SUPPORTED_CLIENTS = [
453
+ "claude",
454
+ "codex",
455
+ "gemini",
456
+ "antigravity"
457
+ ];
458
+ function ctxPlatform(ctx) {
459
+ return ctx?.platform ?? (0, import_os5.platform)();
460
+ }
461
+ function ctxHome(ctx) {
462
+ return ctx?.homeDir ?? (0, import_os5.homedir)();
463
+ }
464
+ function ctxEnv(ctx) {
465
+ return ctx?.env ?? process.env;
466
+ }
467
+ function resolveClaudeHome(ctx) {
468
+ const env = ctxEnv(ctx);
469
+ return env.PUSHREC_CLAUDE_HOME || (0, import_path4.join)(ctxHome(ctx), ".claude");
470
+ }
471
+ function resolveCodexHome(ctx) {
472
+ const env = ctxEnv(ctx);
473
+ return env.CODEX_HOME || (0, import_path4.join)(ctxHome(ctx), ".codex");
474
+ }
475
+ function resolveGeminiHome(ctx) {
476
+ const env = ctxEnv(ctx);
477
+ return env.GEMINI_HOME || (0, import_path4.join)(ctxHome(ctx), ".gemini");
478
+ }
479
+ function resolveAntigravityUserDir(ctx) {
480
+ const env = ctxEnv(ctx);
481
+ if (env.PUSHREC_ANTIGRAVITY_USER_DIR) {
482
+ return env.PUSHREC_ANTIGRAVITY_USER_DIR;
483
+ }
484
+ const platform5 = ctxPlatform(ctx);
485
+ const home = ctxHome(ctx);
486
+ if (platform5 === "darwin") {
487
+ return (0, import_path4.join)(home, "Library", "Application Support", "Antigravity", "User");
488
+ }
489
+ if (platform5 === "win32") {
490
+ const appData = env.APPDATA || (0, import_path4.join)(home, "AppData", "Roaming");
491
+ return (0, import_path4.join)(appData, "Antigravity", "User");
492
+ }
493
+ return (0, import_path4.join)(home, ".config", "Antigravity", "User");
494
+ }
495
+ function resolveClientInstallRoots(client, ctx) {
496
+ const platform5 = ctxPlatform(ctx);
497
+ switch (client) {
498
+ case "claude": {
499
+ const profileHome = resolveClaudeHome(ctx);
500
+ return {
501
+ client,
502
+ platform: platform5,
503
+ profileHome,
504
+ skillsRoot: (0, import_path4.join)(profileHome, "skills"),
505
+ nativeSkillRootVerified: true,
506
+ previewProfile: false,
507
+ notes: ["Claude Code native skills root is verified: <CLAUDE_HOME>/skills."]
508
+ };
509
+ }
510
+ case "codex": {
511
+ const profileHome = resolveCodexHome(ctx);
512
+ return {
513
+ client,
514
+ platform: platform5,
515
+ profileHome,
516
+ skillsRoot: (0, import_path4.join)(profileHome, "skills"),
517
+ nativeSkillRootVerified: true,
518
+ previewProfile: true,
519
+ notes: ["Codex native skills root is verified: <CODEX_HOME>/skills."]
520
+ };
521
+ }
522
+ case "gemini": {
523
+ const profileHome = resolveGeminiHome(ctx);
524
+ return {
525
+ client,
526
+ platform: platform5,
527
+ profileHome,
528
+ skillsRoot: null,
529
+ nativeSkillRootVerified: false,
530
+ previewProfile: true,
531
+ notes: [
532
+ "Gemini preview profile detected, but native skills root is not yet standardized in CLI foundation.",
533
+ "Use compatibility profile artifacts (e.g., GEMINI.md / commands/extensions) until native root is locked."
534
+ ]
535
+ };
536
+ }
537
+ case "antigravity": {
538
+ const profileHome = resolveAntigravityUserDir(ctx);
539
+ return {
540
+ client,
541
+ platform: platform5,
542
+ profileHome,
543
+ skillsRoot: null,
544
+ nativeSkillRootVerified: false,
545
+ previewProfile: true,
546
+ notes: [
547
+ "Antigravity preview profile detected, but native skills root is not yet verified in CLI foundation.",
548
+ "Use compatibility profile artifacts (AGENTS.md / compatible assets) until native root is locked."
549
+ ]
550
+ };
551
+ }
552
+ default: {
553
+ const exhaustive = client;
554
+ throw new Error(`Unsupported client: ${String(exhaustive)}`);
555
+ }
556
+ }
557
+ }
558
+ function getLegacyClaudeSkillsRoot(ctx) {
559
+ return resolveClientInstallRoots("claude", ctx).skillsRoot;
560
+ }
561
+
562
+ // src/lib/config.ts
563
+ var CLI_CONFIG_SCHEMA_VERSION = 2;
564
+ function cloneInstalledSkills(skills) {
565
+ if (!skills) return {};
566
+ return Object.fromEntries(
567
+ Object.entries(skills).map(([name, meta]) => [name, { ...meta }])
568
+ );
569
+ }
570
+ function defaultVaultState() {
571
+ return {
572
+ status: "not_started",
573
+ vaultPath: null,
574
+ claudeMdPath: null,
575
+ agentsMdPath: null,
576
+ lastUpdatedAt: null,
577
+ backups: []
578
+ };
579
+ }
580
+ function defaultSetupState() {
581
+ return {
582
+ schemaVersion: 1,
583
+ currentRunId: null,
584
+ completedPhases: [],
585
+ lastRunAt: null,
586
+ lastError: null
587
+ };
588
+ }
589
+ function emptyClientProfile(client) {
590
+ const roots = resolveClientInstallRoots(client);
591
+ return {
592
+ client,
593
+ detected: false,
594
+ selected: false,
595
+ profileHome: roots.profileHome,
596
+ skillsRoot: roots.skillsRoot,
597
+ nativeSkillRootVerified: roots.nativeSkillRootVerified,
598
+ previewProfile: roots.previewProfile,
599
+ lastDetectedAt: null,
600
+ installedSkills: {},
601
+ notes: [...roots.notes]
602
+ };
603
+ }
604
+ function defaultClients() {
605
+ const out = {};
606
+ for (const client of SUPPORTED_CLIENTS) {
607
+ out[client] = emptyClientProfile(client);
608
+ }
609
+ return out;
610
+ }
611
+ function createDefaultConfig() {
404
612
  return {
613
+ schemaVersion: CLI_CONFIG_SCHEMA_VERSION,
405
614
  registryUrl: REGISTRY_URL,
406
615
  lastVerifiedAt: null,
407
- installedSkills: {}
616
+ installedSkills: {},
617
+ clients: defaultClients(),
618
+ skillState: {},
619
+ vault: defaultVaultState(),
620
+ setup: defaultSetupState()
621
+ };
622
+ }
623
+ function asRecord(value) {
624
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
625
+ return null;
626
+ }
627
+ return value;
628
+ }
629
+ function migrateClientProfile(client, rawValue, legacyInstalledSkills) {
630
+ const base = emptyClientProfile(client);
631
+ const raw = asRecord(rawValue);
632
+ if (!raw) {
633
+ if (client === "claude") {
634
+ base.installedSkills = cloneInstalledSkills(legacyInstalledSkills);
635
+ }
636
+ return base;
637
+ }
638
+ return {
639
+ ...base,
640
+ detected: typeof raw.detected === "boolean" ? raw.detected : base.detected,
641
+ selected: typeof raw.selected === "boolean" ? raw.selected : base.selected,
642
+ profileHome: typeof raw.profileHome === "string" || raw.profileHome === null ? raw.profileHome : base.profileHome,
643
+ skillsRoot: typeof raw.skillsRoot === "string" || raw.skillsRoot === null ? raw.skillsRoot : base.skillsRoot,
644
+ nativeSkillRootVerified: typeof raw.nativeSkillRootVerified === "boolean" ? raw.nativeSkillRootVerified : base.nativeSkillRootVerified,
645
+ previewProfile: typeof raw.previewProfile === "boolean" ? raw.previewProfile : base.previewProfile,
646
+ lastDetectedAt: typeof raw.lastDetectedAt === "string" || raw.lastDetectedAt === null ? raw.lastDetectedAt : base.lastDetectedAt,
647
+ installedSkills: cloneInstalledSkills(
648
+ asRecord(raw.installedSkills)
649
+ ),
650
+ notes: Array.isArray(raw.notes) ? raw.notes.filter((x) => typeof x === "string") : base.notes
651
+ };
652
+ }
653
+ function migrateSkillState(rawValue) {
654
+ const raw = asRecord(rawValue);
655
+ if (!raw) return {};
656
+ const out = {};
657
+ for (const [name, value] of Object.entries(raw)) {
658
+ const item = asRecord(value);
659
+ if (!item) continue;
660
+ out[name] = {
661
+ name,
662
+ mode: item.mode === "managed" || item.mode === "forked" || item.mode === "custom-unmanaged" ? item.mode : "managed",
663
+ lastKnownVersion: typeof item.lastKnownVersion === "string" || item.lastKnownVersion === null ? item.lastKnownVersion : null,
664
+ installedPath: typeof item.installedPath === "string" || item.installedPath === null ? item.installedPath : null,
665
+ targetClient: item.targetClient === "claude" || item.targetClient === "codex" || item.targetClient === "gemini" || item.targetClient === "antigravity" || item.targetClient === null ? item.targetClient : null,
666
+ contentHash: typeof item.contentHash === "string" || item.contentHash === null ? item.contentHash : null,
667
+ backups: Array.isArray(item.backups) ? item.backups.filter((x) => typeof x === "string") : [],
668
+ localModified: typeof item.localModified === "boolean" ? item.localModified : false,
669
+ lastCheckedAt: typeof item.lastCheckedAt === "string" || item.lastCheckedAt === null ? item.lastCheckedAt : null
670
+ };
671
+ }
672
+ return out;
673
+ }
674
+ function migrateVaultState(rawValue) {
675
+ const base = defaultVaultState();
676
+ const raw = asRecord(rawValue);
677
+ if (!raw) return base;
678
+ return {
679
+ status: raw.status === "not_started" || raw.status === "initialized" || raw.status === "adopted" || raw.status === "error" ? raw.status : base.status,
680
+ vaultPath: typeof raw.vaultPath === "string" || raw.vaultPath === null ? raw.vaultPath : base.vaultPath,
681
+ claudeMdPath: typeof raw.claudeMdPath === "string" || raw.claudeMdPath === null ? raw.claudeMdPath : base.claudeMdPath,
682
+ agentsMdPath: typeof raw.agentsMdPath === "string" || raw.agentsMdPath === null ? raw.agentsMdPath : base.agentsMdPath,
683
+ lastUpdatedAt: typeof raw.lastUpdatedAt === "string" || raw.lastUpdatedAt === null ? raw.lastUpdatedAt : base.lastUpdatedAt,
684
+ backups: Array.isArray(raw.backups) ? raw.backups.filter((x) => typeof x === "string") : base.backups
685
+ };
686
+ }
687
+ function migrateSetupState(rawValue) {
688
+ const base = defaultSetupState();
689
+ const raw = asRecord(rawValue);
690
+ if (!raw) return base;
691
+ return {
692
+ schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : base.schemaVersion,
693
+ currentRunId: typeof raw.currentRunId === "string" || raw.currentRunId === null ? raw.currentRunId : base.currentRunId,
694
+ completedPhases: Array.isArray(raw.completedPhases) ? raw.completedPhases.filter((x) => typeof x === "string") : base.completedPhases,
695
+ lastRunAt: typeof raw.lastRunAt === "string" || raw.lastRunAt === null ? raw.lastRunAt : base.lastRunAt,
696
+ lastError: typeof raw.lastError === "string" || raw.lastError === null ? raw.lastError : base.lastError
697
+ };
698
+ }
699
+ function migrateCliConfig(rawValue) {
700
+ const defaults = createDefaultConfig();
701
+ const raw = asRecord(rawValue);
702
+ if (!raw) return defaults;
703
+ const legacyInstalled = cloneInstalledSkills(
704
+ asRecord(raw.installedSkills)
705
+ );
706
+ const rawClients = asRecord(raw.clients);
707
+ const clients = {};
708
+ for (const client of SUPPORTED_CLIENTS) {
709
+ clients[client] = migrateClientProfile(
710
+ client,
711
+ rawClients?.[client],
712
+ client === "claude" ? legacyInstalled : {}
713
+ );
714
+ }
715
+ return {
716
+ schemaVersion: CLI_CONFIG_SCHEMA_VERSION,
717
+ registryUrl: typeof raw.registryUrl === "string" ? raw.registryUrl : REGISTRY_URL,
718
+ lastVerifiedAt: typeof raw.lastVerifiedAt === "string" || raw.lastVerifiedAt === null ? raw.lastVerifiedAt : null,
719
+ installedSkills: legacyInstalled,
720
+ clients,
721
+ skillState: migrateSkillState(raw.skillState),
722
+ vault: migrateVaultState(raw.vault),
723
+ setup: migrateSetupState(raw.setup)
724
+ };
725
+ }
726
+ function mergeClientProfiles(base, patch) {
727
+ if (!patch) return base;
728
+ const out = { ...base };
729
+ for (const [client, next] of Object.entries(patch)) {
730
+ if (!next) continue;
731
+ const key = client;
732
+ const prev = out[key];
733
+ out[key] = prev ? {
734
+ ...prev,
735
+ ...next,
736
+ installedSkills: {
737
+ ...prev.installedSkills,
738
+ ...next.installedSkills ?? {}
739
+ },
740
+ notes: next.notes ?? prev.notes
741
+ } : next;
742
+ }
743
+ return out;
744
+ }
745
+ function mergeCliConfig(current, patch) {
746
+ return {
747
+ ...current,
748
+ ...patch,
749
+ schemaVersion: CLI_CONFIG_SCHEMA_VERSION,
750
+ installedSkills: patch.installedSkills ?? current.installedSkills,
751
+ clients: mergeClientProfiles(current.clients, patch.clients),
752
+ skillState: { ...current.skillState, ...patch.skillState ?? {} },
753
+ vault: { ...current.vault, ...patch.vault ?? {} },
754
+ setup: { ...current.setup, ...patch.setup ?? {} }
408
755
  };
409
756
  }
410
757
  function loadConfig() {
411
758
  if (!(0, import_fs4.existsSync)(CONFIG_FILE)) {
412
- return defaultConfig();
759
+ return createDefaultConfig();
413
760
  }
414
761
  try {
415
762
  const raw = (0, import_fs4.readFileSync)(CONFIG_FILE, "utf-8");
416
- return { ...defaultConfig(), ...JSON.parse(raw) };
763
+ return migrateCliConfig(JSON.parse(raw));
417
764
  } catch {
418
- return defaultConfig();
765
+ return createDefaultConfig();
419
766
  }
420
767
  }
421
768
  function saveConfig(config) {
422
- const dir = (0, import_path4.dirname)(CONFIG_FILE);
769
+ const dir = (0, import_path5.dirname)(CONFIG_FILE);
423
770
  if (!(0, import_fs4.existsSync)(dir)) {
424
771
  (0, import_fs4.mkdirSync)(dir, { recursive: true });
425
772
  }
426
- (0, import_fs4.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
773
+ (0, import_fs4.writeFileSync)(
774
+ CONFIG_FILE,
775
+ JSON.stringify(migrateCliConfig(config), null, 2),
776
+ "utf-8"
777
+ );
427
778
  }
428
779
  function updateConfig(partial) {
429
780
  const config = loadConfig();
430
- const updated = { ...config, ...partial };
781
+ const updated = mergeCliConfig(config, partial);
431
782
  saveConfig(updated);
432
783
  return updated;
433
784
  }
@@ -478,10 +829,11 @@ async function fetchSkillManifest(name) {
478
829
  }
479
830
  return response.json();
480
831
  }
481
- async function downloadSkill(name) {
832
+ async function downloadSkill(name, version) {
482
833
  const authHeaders = await getAuthHeaders();
834
+ const versionQuery = version ? `?version=${encodeURIComponent(version)}` : "";
483
835
  const response = await registryFetch(
484
- `/api/skills/${encodeURIComponent(name)}/download`,
836
+ `/api/skills/${encodeURIComponent(name)}/download${versionQuery}`,
485
837
  { headers: authHeaders }
486
838
  );
487
839
  if (!response.ok) {
@@ -591,17 +943,17 @@ function readInput(question) {
591
943
  input: process.stdin,
592
944
  output: process.stdout
593
945
  });
594
- return new Promise((resolve2, reject) => {
946
+ return new Promise((resolve7, reject) => {
595
947
  rl.on("error", (err) => {
596
948
  rl.close();
597
949
  reject(err);
598
950
  });
599
951
  rl.on("close", () => {
600
- resolve2("");
952
+ resolve7("");
601
953
  });
602
954
  rl.question(question, (answer) => {
603
955
  rl.close();
604
- resolve2(answer.trim());
956
+ resolve7(answer.trim());
605
957
  });
606
958
  });
607
959
  }
@@ -860,33 +1212,37 @@ var import_ora = __toESM(require("ora"));
860
1212
 
861
1213
  // src/lib/installer.ts
862
1214
  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");
1215
+ var import_path6 = require("path");
1216
+ var import_child_process4 = require("child_process");
1217
+ var import_os6 = require("os");
866
1218
  var import_crypto2 = require("crypto");
867
- async function installSkill(name, archive) {
1219
+ function resolveSkillsRoot(options) {
1220
+ return options?.skillsRoot ?? getLegacyClaudeSkillsRoot();
1221
+ }
1222
+ async function installSkill(name, archive, options) {
868
1223
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name) || name.includes("..")) {
869
1224
  throw new Error(`Invalid skill name: "${name}"`);
870
1225
  }
871
- const targetDir = (0, import_path5.join)(SKILLS_DIR, name);
1226
+ const skillsRoot = resolveSkillsRoot(options);
1227
+ const targetDir = (0, import_path6.join)(skillsRoot, name);
872
1228
  const tmpId = (0, import_crypto2.randomBytes)(4).toString("hex");
873
- const tmpDir = (0, import_path5.join)((0, import_os4.tmpdir)(), `pushrec-install-${tmpId}`);
1229
+ const tmpDir = (0, import_path6.join)((0, import_os6.tmpdir)(), `pushrec-install-${tmpId}`);
874
1230
  try {
875
1231
  (0, import_fs5.mkdirSync)(tmpDir, { recursive: true });
876
- const zipPath = (0, import_path5.join)(tmpDir, `${name}.zip`);
1232
+ const zipPath = (0, import_path6.join)(tmpDir, `${name}.zip`);
877
1233
  (0, import_fs5.writeFileSync)(zipPath, Buffer.from(archive));
878
- const extractDir = (0, import_path5.join)(tmpDir, "extracted");
1234
+ const extractDir = (0, import_path6.join)(tmpDir, "extracted");
879
1235
  (0, import_fs5.mkdirSync)(extractDir, { recursive: true });
880
1236
  extractZip(zipPath, extractDir);
881
1237
  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) {
1238
+ const resolvedRoot = (0, import_path6.resolve)(skillRoot);
1239
+ const resolvedExtract = (0, import_path6.resolve)(extractDir);
1240
+ const rel = (0, import_path6.relative)(resolvedExtract, resolvedRoot);
1241
+ if (rel.startsWith("..") || (0, import_path6.resolve)(rel) === resolvedRoot) {
886
1242
  throw new Error("Archive contains path traversal \u2014 aborting install.");
887
1243
  }
888
- if (!(0, import_fs5.existsSync)(SKILLS_DIR)) {
889
- (0, import_fs5.mkdirSync)(SKILLS_DIR, { recursive: true });
1244
+ if (!(0, import_fs5.existsSync)(skillsRoot)) {
1245
+ (0, import_fs5.mkdirSync)(skillsRoot, { recursive: true });
890
1246
  }
891
1247
  if ((0, import_fs5.existsSync)(targetDir)) {
892
1248
  (0, import_fs5.rmSync)(targetDir, { recursive: true, force: true });
@@ -901,15 +1257,22 @@ async function installSkill(name, archive) {
901
1257
  }
902
1258
  }
903
1259
  function extractZip(zipPath, destDir) {
904
- const os = (0, import_os4.platform)();
1260
+ const os = (0, import_os6.platform)();
905
1261
  if (os === "win32") {
906
- (0, import_child_process3.execFileSync)(
1262
+ const safeZip = zipPath.replace(/'/g, "''");
1263
+ const safeDest = destDir.replace(/'/g, "''");
1264
+ (0, import_child_process4.execFileSync)(
907
1265
  "powershell",
908
- ["-NoProfile", "-Command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
1266
+ [
1267
+ "-NoProfile",
1268
+ "-NonInteractive",
1269
+ "-Command",
1270
+ `Expand-Archive -LiteralPath '${safeZip}' -DestinationPath '${safeDest}' -Force`
1271
+ ],
909
1272
  { stdio: "pipe", timeout: 6e4 }
910
1273
  );
911
1274
  } else {
912
- (0, import_child_process3.execFileSync)("unzip", ["-o", "-q", zipPath, "-d", destDir], {
1275
+ (0, import_child_process4.execFileSync)("unzip", ["-o", "-q", zipPath, "-d", destDir], {
913
1276
  stdio: "pipe",
914
1277
  timeout: 6e4
915
1278
  });
@@ -918,36 +1281,311 @@ function extractZip(zipPath, destDir) {
918
1281
  function findSkillRoot(extractDir, name) {
919
1282
  const entries = (0, import_fs5.readdirSync)(extractDir);
920
1283
  const skillDir = entries.find((entry) => {
921
- const fullPath = (0, import_path5.join)(extractDir, entry);
1284
+ const fullPath = (0, import_path6.join)(extractDir, entry);
922
1285
  return (0, import_fs5.statSync)(fullPath).isDirectory() && (entry === name || entry.startsWith(`${name}-`));
923
1286
  });
924
1287
  if (skillDir) {
925
- return (0, import_path5.join)(extractDir, skillDir);
1288
+ return (0, import_path6.join)(extractDir, skillDir);
926
1289
  }
927
1290
  if (entries.includes("SKILL.md")) {
928
1291
  return extractDir;
929
1292
  }
930
1293
  const firstDir = entries.find(
931
- (entry) => (0, import_fs5.statSync)((0, import_path5.join)(extractDir, entry)).isDirectory()
1294
+ (entry) => (0, import_fs5.statSync)((0, import_path6.join)(extractDir, entry)).isDirectory()
932
1295
  );
933
1296
  if (firstDir) {
934
- return (0, import_path5.join)(extractDir, firstDir);
1297
+ return (0, import_path6.join)(extractDir, firstDir);
935
1298
  }
936
1299
  throw new Error(
937
1300
  `Could not find skill root in archive for "${name}". Expected a directory containing SKILL.md.`
938
1301
  );
939
1302
  }
940
- function uninstallSkill(name) {
1303
+ function uninstallSkill(name, options) {
941
1304
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name) || name.includes("..") || name.includes("/") || name.includes("\\")) {
942
1305
  throw new Error(`Invalid skill name: "${name}". Only lowercase letters, numbers, and hyphens allowed.`);
943
1306
  }
944
- const targetDir = (0, import_path5.join)(SKILLS_DIR, name);
1307
+ const targetDir = (0, import_path6.join)(resolveSkillsRoot(options), name);
945
1308
  if (!(0, import_fs5.existsSync)(targetDir)) return false;
946
1309
  (0, import_fs5.rmSync)(targetDir, { recursive: true, force: true });
947
1310
  return true;
948
1311
  }
949
1312
  function isSkillInstalled(name) {
950
- return (0, import_fs5.existsSync)((0, import_path5.join)(SKILLS_DIR, name, "SKILL.md"));
1313
+ return isSkillInstalledAtRoot(name, resolveSkillsRoot());
1314
+ }
1315
+ function getBundledSkillPath(name) {
1316
+ return (0, import_path6.join)(__dirname, "..", "bundled-skills", name);
1317
+ }
1318
+ function isSkillInstalledAtRoot(name, skillsRoot) {
1319
+ return (0, import_fs5.existsSync)((0, import_path6.join)(skillsRoot, name, "SKILL.md"));
1320
+ }
1321
+ function copyBundledSkill(name, options) {
1322
+ const sourcePath = options?.bundledSkillPath ?? getBundledSkillPath(name);
1323
+ if (!(0, import_fs5.existsSync)(sourcePath)) return;
1324
+ const skillsRoot = resolveSkillsRoot(options);
1325
+ const targetPath = (0, import_path6.join)(skillsRoot, name);
1326
+ if (!(0, import_fs5.existsSync)(skillsRoot)) {
1327
+ (0, import_fs5.mkdirSync)(skillsRoot, { recursive: true });
1328
+ }
1329
+ if ((0, import_fs5.existsSync)((0, import_path6.join)(targetPath, "SKILL.md"))) return;
1330
+ (0, import_fs5.cpSync)(sourcePath, targetPath, { recursive: true });
1331
+ }
1332
+
1333
+ // src/lib/managed-skill.ts
1334
+ var import_fs8 = require("fs");
1335
+ var import_path9 = require("path");
1336
+
1337
+ // src/lib/backups.ts
1338
+ var import_fs6 = require("fs");
1339
+ var import_path7 = require("path");
1340
+ var import_crypto3 = require("crypto");
1341
+ var DEFAULT_BACKUPS_DIR = (0, import_path7.join)(CONFIG_DIR, "backups");
1342
+ function resolveBackupsRoot(options = {}) {
1343
+ const env = options.env ?? process.env;
1344
+ const configured = env.PUSHREC_BACKUPS_DIR?.trim();
1345
+ return (0, import_path7.resolve)(configured && configured.length > 0 ? configured : DEFAULT_BACKUPS_DIR);
1346
+ }
1347
+ function ensureBackupsRoot(options = {}) {
1348
+ const root = resolveBackupsRoot(options);
1349
+ if (!(0, import_fs6.existsSync)(root)) {
1350
+ (0, import_fs6.mkdirSync)(root, { recursive: true });
1351
+ }
1352
+ return root;
1353
+ }
1354
+ function sanitizeBackupLabel(label) {
1355
+ const cleaned = label.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
1356
+ return cleaned.length > 0 ? cleaned : "item";
1357
+ }
1358
+ function createBackupId(prefix, now = /* @__PURE__ */ new Date()) {
1359
+ const ts = now.toISOString().replace(/[:.]/g, "-");
1360
+ const suffix = (0, import_crypto3.randomBytes)(3).toString("hex");
1361
+ return `${sanitizeBackupLabel(prefix)}-${ts}-${suffix}`;
1362
+ }
1363
+ function buildBackupDirectoryPath(label, options = {}) {
1364
+ const root = resolveBackupsRoot(options);
1365
+ const id = createBackupId(label, options.now);
1366
+ return (0, import_path7.join)(root, id);
1367
+ }
1368
+ function buildSkillBackupDirectoryPath(skillName, options = {}) {
1369
+ return buildBackupDirectoryPath(`skill-${skillName}`, options);
1370
+ }
1371
+
1372
+ // src/lib/tree-hash.ts
1373
+ var import_crypto4 = require("crypto");
1374
+ var import_fs7 = require("fs");
1375
+ var import_path8 = require("path");
1376
+ function isExcludedName(name, exclude) {
1377
+ return exclude.has(name);
1378
+ }
1379
+ function walkFiles(root, current, exclude, out) {
1380
+ const entries = (0, import_fs7.readdirSync)(current).sort((a, b) => a.localeCompare(b));
1381
+ for (const entry of entries) {
1382
+ if (isExcludedName(entry, exclude)) continue;
1383
+ const fullPath = (0, import_path8.join)(current, entry);
1384
+ const stat = (0, import_fs7.lstatSync)(fullPath);
1385
+ if (stat.isDirectory()) {
1386
+ walkFiles(root, fullPath, exclude, out);
1387
+ continue;
1388
+ }
1389
+ if (!stat.isFile()) continue;
1390
+ out.push(fullPath);
1391
+ }
1392
+ }
1393
+ function hashDirectoryTree(rootPath, options = {}) {
1394
+ const root = (0, import_path8.resolve)(rootPath);
1395
+ const stat = (0, import_fs7.lstatSync)(root);
1396
+ if (!stat.isDirectory()) {
1397
+ throw new Error(`Not a directory: ${root}`);
1398
+ }
1399
+ const exclude = new Set(options.excludeNames ?? []);
1400
+ const files = [];
1401
+ walkFiles(root, root, exclude, files);
1402
+ const digest = (0, import_crypto4.createHash)("sha256");
1403
+ let totalBytes = 0;
1404
+ for (const file of files) {
1405
+ const rel = (0, import_path8.relative)(root, file).replace(/\\/g, "/");
1406
+ const bytes = (0, import_fs7.readFileSync)(file);
1407
+ totalBytes += bytes.byteLength;
1408
+ digest.update("PATH\0");
1409
+ digest.update(rel);
1410
+ digest.update("\0SIZE\0");
1411
+ digest.update(String(bytes.byteLength));
1412
+ digest.update("\0DATA\0");
1413
+ digest.update(bytes);
1414
+ digest.update("\0END\0");
1415
+ }
1416
+ return {
1417
+ hash: digest.digest("hex"),
1418
+ fileCount: files.length,
1419
+ totalBytes,
1420
+ root
1421
+ };
1422
+ }
1423
+
1424
+ // src/lib/managed-skill.ts
1425
+ var HASH_EXCLUDES = [".DS_Store"];
1426
+ var SKILL_BACKUP_METADATA_FILE = ".pushrec-backup.json";
1427
+ function resolveInstalledSkillPath(skillName, state) {
1428
+ return state?.installedPath ?? (0, import_path9.join)(getLegacyClaudeSkillsRoot(), skillName);
1429
+ }
1430
+ function hashInstalledSkillPath(path) {
1431
+ if (!(0, import_fs8.existsSync)(path)) return null;
1432
+ const stat = (0, import_fs8.lstatSync)(path);
1433
+ if (!stat.isDirectory()) return null;
1434
+ return hashDirectoryTree(path, { excludeNames: HASH_EXCLUDES }).hash;
1435
+ }
1436
+ function isLocalSkillModified(expectedHash, currentHash) {
1437
+ if (!expectedHash || !currentHash) return false;
1438
+ return expectedHash !== currentHash;
1439
+ }
1440
+ function createSkillBackupSnapshot(skillName, installedPath, metadata) {
1441
+ if (!(0, import_fs8.existsSync)(installedPath)) return null;
1442
+ const stat = (0, import_fs8.lstatSync)(installedPath);
1443
+ if (!stat.isDirectory()) return null;
1444
+ ensureBackupsRoot();
1445
+ const backupPath = buildSkillBackupDirectoryPath(skillName);
1446
+ (0, import_fs8.cpSync)(installedPath, backupPath, { recursive: true });
1447
+ const payload = {
1448
+ skillName,
1449
+ sourceVersion: metadata?.sourceVersion ?? null,
1450
+ sourceHash: metadata?.sourceHash ?? null,
1451
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1452
+ };
1453
+ (0, import_fs8.writeFileSync)(
1454
+ (0, import_path9.join)(backupPath, SKILL_BACKUP_METADATA_FILE),
1455
+ JSON.stringify(payload, null, 2) + "\n",
1456
+ "utf-8"
1457
+ );
1458
+ return backupPath;
1459
+ }
1460
+ function readSkillBackupMetadata(backupPath) {
1461
+ const metadataPath = (0, import_path9.join)(backupPath, SKILL_BACKUP_METADATA_FILE);
1462
+ if (!(0, import_fs8.existsSync)(metadataPath)) return null;
1463
+ try {
1464
+ const parsed = JSON.parse((0, import_fs8.readFileSync)(metadataPath, "utf-8"));
1465
+ if (!parsed || typeof parsed !== "object") return null;
1466
+ return {
1467
+ skillName: typeof parsed.skillName === "string" ? parsed.skillName : "unknown-skill",
1468
+ sourceVersion: typeof parsed.sourceVersion === "string" || parsed.sourceVersion === null ? parsed.sourceVersion : null,
1469
+ sourceHash: typeof parsed.sourceHash === "string" || parsed.sourceHash === null ? parsed.sourceHash : null,
1470
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date()).toISOString()
1471
+ };
1472
+ } catch {
1473
+ return null;
1474
+ }
1475
+ }
1476
+ function buildNextManagedSkillState(params) {
1477
+ const nowIso2 = params.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
1478
+ const previousBackups = params.previous?.backups ?? [];
1479
+ const backups = params.backupPath && !previousBackups.includes(params.backupPath) ? [...previousBackups, params.backupPath] : previousBackups;
1480
+ return {
1481
+ name: params.name,
1482
+ mode: params.previous?.mode ?? "managed",
1483
+ lastKnownVersion: params.version,
1484
+ installedPath: params.installedPath,
1485
+ targetClient: params.targetClient ?? params.previous?.targetClient ?? "claude",
1486
+ contentHash: params.hash ?? params.previous?.contentHash ?? null,
1487
+ backups,
1488
+ localModified: false,
1489
+ lastCheckedAt: nowIso2
1490
+ };
1491
+ }
1492
+
1493
+ // src/lib/compatibility.ts
1494
+ function runtimePlatformFromNodePlatform(platform5) {
1495
+ if (platform5 === "win32") return "windows";
1496
+ if (platform5 === "darwin") return "macos";
1497
+ return "linux";
1498
+ }
1499
+ function normalizeList(values) {
1500
+ return (values ?? []).map((x) => x.trim().toLowerCase()).filter((x) => x.length > 0);
1501
+ }
1502
+ function normalizeWindowsStatus(value) {
1503
+ return typeof value === "string" ? value.trim().toUpperCase() : "UNKNOWN";
1504
+ }
1505
+ function compatibilityOf(skill) {
1506
+ return skill.compatibility ?? null;
1507
+ }
1508
+ function evaluateSkillCompatibility(skill, options = {}) {
1509
+ const client = options.client ?? "claude";
1510
+ const platform5 = options.platform ?? runtimePlatformFromNodePlatform(process.platform);
1511
+ const compatibility = compatibilityOf(skill);
1512
+ if (!compatibility) {
1513
+ return {
1514
+ allowed: true,
1515
+ requiresOverride: false,
1516
+ status: "ALLOW_WITH_WARNING",
1517
+ reasonCode: "COMPATIBILITY_METADATA_MISSING",
1518
+ message: "Compatibility metadata missing; proceeding in permissive mode for backward compatibility."
1519
+ };
1520
+ }
1521
+ const clients = normalizeList(compatibility.clients);
1522
+ if (clients.length > 0 && !clients.includes(client)) {
1523
+ return {
1524
+ allowed: false,
1525
+ requiresOverride: false,
1526
+ status: "BLOCK_UNSUPPORTED_CLIENT",
1527
+ reasonCode: "UNSUPPORTED_CLIENT",
1528
+ message: `Skill is not marked compatible with client '${client}'.`
1529
+ };
1530
+ }
1531
+ const platforms = normalizeList(compatibility.platforms);
1532
+ if (platforms.length > 0 && !platforms.includes(platform5)) {
1533
+ return {
1534
+ allowed: false,
1535
+ requiresOverride: false,
1536
+ status: "BLOCK_UNSUPPORTED_PLATFORM",
1537
+ reasonCode: "UNSUPPORTED_PLATFORM",
1538
+ message: `Skill is not marked compatible with platform '${platform5}'.`
1539
+ };
1540
+ }
1541
+ if (platform5 === "windows") {
1542
+ const labels = normalizeList(compatibility.labels);
1543
+ const windowsStatus = normalizeWindowsStatus(compatibility.windowsStatus);
1544
+ if (windowsStatus === "INCOMPATIBLE" || windowsStatus === "FAIL") {
1545
+ return {
1546
+ allowed: false,
1547
+ requiresOverride: false,
1548
+ status: "BLOCK_INCOMPATIBLE",
1549
+ reasonCode: "WINDOWS_STATUS_INCOMPATIBLE",
1550
+ message: `Skill is marked Windows-incompatible (${windowsStatus}).`
1551
+ };
1552
+ }
1553
+ if (windowsStatus === "BLOCKED" || windowsStatus === "NOT_RUN") {
1554
+ return {
1555
+ allowed: false,
1556
+ requiresOverride: true,
1557
+ status: "BLOCK_WINDOWS_GATED",
1558
+ reasonCode: "WINDOWS_STATUS_BLOCKED",
1559
+ message: "Skill is Windows-gated (BLOCKED/NOT_RUN). Use explicit override to proceed."
1560
+ };
1561
+ }
1562
+ if (labels.includes("windows-gated")) {
1563
+ return {
1564
+ allowed: false,
1565
+ requiresOverride: true,
1566
+ status: "BLOCK_WINDOWS_GATED",
1567
+ reasonCode: "WINDOWS_GATED_LABEL",
1568
+ message: "Skill is labeled windows-gated. Use explicit override to proceed."
1569
+ };
1570
+ }
1571
+ }
1572
+ return {
1573
+ allowed: true,
1574
+ requiresOverride: false,
1575
+ status: "ALLOW",
1576
+ reasonCode: "ALLOWED",
1577
+ message: "Compatibility check passed."
1578
+ };
1579
+ }
1580
+ function compatibilitySummary(skill) {
1581
+ const compatibility = compatibilityOf(skill);
1582
+ if (!compatibility) return "unknown";
1583
+ const windowsStatus = compatibility.windowsStatus ? String(compatibility.windowsStatus).toLowerCase() : null;
1584
+ if (windowsStatus) return windowsStatus;
1585
+ const labels = normalizeList(compatibility.labels);
1586
+ if (labels.includes("windows-gated")) return "windows-gated";
1587
+ if (labels.includes("windows-incompatible")) return "windows-incompatible";
1588
+ return "listed";
951
1589
  }
952
1590
 
953
1591
  // src/commands/install.ts
@@ -1031,31 +1669,93 @@ async function ensureLicense(forFreeSkill) {
1031
1669
  return false;
1032
1670
  }
1033
1671
  }
1034
- async function installOne(name, prefix) {
1672
+ async function installOne(name, prefix, options) {
1035
1673
  const spinner = (0, import_ora.default)(`${prefix}Fetching ${name}...`).start();
1036
1674
  try {
1037
1675
  const manifest = await fetchSkillManifest(name);
1676
+ const requestedVersion = options.requestedVersion?.trim() || void 0;
1677
+ if (requestedVersion) {
1678
+ const knownVersions = /* @__PURE__ */ new Set([
1679
+ ...manifest.currentVersion ? [manifest.currentVersion] : [],
1680
+ ...manifest.versions.map((v) => v.version)
1681
+ ]);
1682
+ if (!knownVersions.has(requestedVersion)) {
1683
+ spinner.fail(`${prefix}Version ${requestedVersion} is not available for ${name}`);
1684
+ return {
1685
+ ok: false,
1686
+ error: `Version ${requestedVersion} is not available for ${name}`
1687
+ };
1688
+ }
1689
+ }
1690
+ const installVersion = requestedVersion ?? manifest.currentVersion ?? "0.0.0";
1691
+ const compatibility = evaluateSkillCompatibility(manifest, { client: "claude" });
1692
+ if (!compatibility.allowed) {
1693
+ if (compatibility.requiresOverride && options.allowBlocked) {
1694
+ spinner.warn(
1695
+ `${prefix}${name}: compatibility override enabled (${compatibility.reasonCode}); continuing.`
1696
+ );
1697
+ } else {
1698
+ spinner.fail(`${prefix}${name}: ${compatibility.message}`);
1699
+ return {
1700
+ ok: false,
1701
+ error: `${compatibility.reasonCode}: ${compatibility.message}`
1702
+ };
1703
+ }
1704
+ }
1038
1705
  spinner.text = `${prefix}Verifying license...`;
1039
1706
  const licensed = await ensureLicense(manifest.isFree);
1040
1707
  if (!licensed) {
1041
1708
  spinner.fail(`${prefix}License check failed for ${name}`);
1042
1709
  return { ok: false, error: "License check failed" };
1043
1710
  }
1044
- spinner.text = `${prefix}Downloading ${name} v${manifest.currentVersion ?? "latest"}...`;
1045
- const archive = await downloadSkill(name);
1711
+ const configBefore = loadConfig();
1712
+ const previousManaged = configBefore.skillState[name];
1713
+ const installedPathBefore = resolveInstalledSkillPath(name, previousManaged);
1714
+ const currentHash = hashInstalledSkillPath(installedPathBefore);
1715
+ const modified = isLocalSkillModified(previousManaged?.contentHash ?? null, currentHash);
1716
+ if (modified && !options.forceReplace) {
1717
+ spinner.fail(`${prefix}Skipped ${name}: local modifications detected (use --force to replace with backup).`);
1718
+ return {
1719
+ ok: false,
1720
+ error: "Local modifications detected. Re-run with --force to back up and replace."
1721
+ };
1722
+ }
1723
+ let backupPath = null;
1724
+ if (options.forceReplace && currentHash) {
1725
+ spinner.text = `${prefix}Creating backup snapshot for ${name}...`;
1726
+ const sourceVersion = configBefore.installedSkills[name]?.version ?? previousManaged?.lastKnownVersion ?? null;
1727
+ backupPath = createSkillBackupSnapshot(name, installedPathBefore, {
1728
+ sourceVersion,
1729
+ sourceHash: currentHash
1730
+ });
1731
+ }
1732
+ spinner.text = `${prefix}Downloading ${name} v${installVersion}...`;
1733
+ const archive = await downloadSkill(name, requestedVersion);
1046
1734
  spinner.text = `${prefix}Installing ${name}...`;
1047
1735
  const { path } = await installSkill(name, archive);
1048
1736
  const now = (/* @__PURE__ */ new Date()).toISOString();
1049
1737
  const config = loadConfig();
1050
1738
  config.installedSkills[name] = {
1051
1739
  name,
1052
- version: manifest.currentVersion ?? "0.0.0",
1740
+ version: installVersion,
1053
1741
  installedAt: config.installedSkills[name]?.installedAt ?? now,
1054
1742
  updatedAt: now
1055
1743
  };
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" };
1744
+ const contentHash = hashInstalledSkillPath(path);
1745
+ const nextManaged = buildNextManagedSkillState({
1746
+ name,
1747
+ version: installVersion,
1748
+ installedPath: path,
1749
+ previous: config.skillState[name],
1750
+ backupPath,
1751
+ hash: contentHash
1752
+ });
1753
+ updateConfig({
1754
+ installedSkills: config.installedSkills,
1755
+ skillState: { [name]: nextManaged }
1756
+ });
1757
+ spinner.succeed(`${prefix}${name} v${installVersion} installed to ${path}`);
1758
+ return { ok: true, path, version: installVersion, backupPath };
1059
1759
  } catch (err) {
1060
1760
  const message = err instanceof Error ? err.message : String(err);
1061
1761
  spinner.fail(`${prefix}Failed to install ${name}: ${message}`);
@@ -1063,7 +1763,10 @@ async function installOne(name, prefix) {
1063
1763
  }
1064
1764
  }
1065
1765
  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) => {
1766
+ 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 (backs up modified installs first)").option("--version <semver>", "Install a specific version (single skill only)").option(
1767
+ "--allow-blocked",
1768
+ "Allow installing compatibility-gated skills (for explicit preview/testing workflows)"
1769
+ ).option("--json", "Output JSON").action(async (name, opts) => {
1067
1770
  if (!name && !opts.all) {
1068
1771
  if (opts.json) {
1069
1772
  console.log(JSON.stringify({ ok: false, error: "Specify a skill name or use --all" }));
@@ -1081,6 +1784,14 @@ function installCommand() {
1081
1784
  console.error("Cannot specify both a skill name and --all.");
1082
1785
  process.exit(1);
1083
1786
  }
1787
+ if (opts.version && opts.all) {
1788
+ if (opts.json) {
1789
+ console.log(JSON.stringify({ ok: false, error: "--version can only be used with a single skill name" }));
1790
+ process.exit(1);
1791
+ }
1792
+ console.error("--version can only be used with a single skill name.");
1793
+ process.exit(1);
1794
+ }
1084
1795
  if (name) {
1085
1796
  if (!opts.force && isSkillInstalled(name)) {
1086
1797
  if (opts.json) {
@@ -1090,7 +1801,11 @@ function installCommand() {
1090
1801
  }
1091
1802
  return;
1092
1803
  }
1093
- const result = await installOne(name, "");
1804
+ const result = await installOne(name, "", {
1805
+ requestedVersion: opts.version,
1806
+ forceReplace: !!opts.force,
1807
+ allowBlocked: !!opts.allowBlocked
1808
+ });
1094
1809
  if (!result.ok) {
1095
1810
  if (opts.json) {
1096
1811
  console.log(JSON.stringify({ ok: false, skill: name, error: result.error }));
@@ -1098,7 +1813,7 @@ function installCommand() {
1098
1813
  process.exit(1);
1099
1814
  }
1100
1815
  if (opts.json) {
1101
- console.log(JSON.stringify({ ok: true, skill: name, version: result.version, path: result.path }));
1816
+ console.log(JSON.stringify({ ok: true, skill: name, version: result.version, path: result.path, backupPath: result.backupPath ?? null }));
1102
1817
  } else {
1103
1818
  console.log(`
1104
1819
  Done! Use /${name} in your next Claude Code session.`);
@@ -1136,7 +1851,10 @@ Done! Use /${name} in your next Claude Code session.`);
1136
1851
  for (let i = 0; i < toInstall.length; i++) {
1137
1852
  const skill = toInstall[i];
1138
1853
  const prefix = opts.json ? "" : `[${i + 1}/${toInstall.length}] `;
1139
- const result = await installOne(skill.name, prefix);
1854
+ const result = await installOne(skill.name, prefix, {
1855
+ forceReplace: !!opts.force,
1856
+ allowBlocked: !!opts.allowBlocked
1857
+ });
1140
1858
  results.push({ skill: skill.name, ok: result.ok, version: result.version, error: result.error });
1141
1859
  if (result.ok) {
1142
1860
  installed++;
@@ -1157,125 +1875,235 @@ Done! ${installed} installed, ${failed} failed.`);
1157
1875
 
1158
1876
  // src/commands/update.ts
1159
1877
  var import_commander3 = require("commander");
1160
- async function updateOne(name, currentVersion, prefix, json) {
1878
+ async function updateOne(name, currentVersion, prefix, json, forceModified, allowBlocked) {
1161
1879
  try {
1162
1880
  const manifest = await fetchSkillManifest(name);
1163
1881
  const latest = manifest.currentVersion;
1882
+ const compatibility = evaluateSkillCompatibility(manifest, { client: "claude" });
1883
+ if (!compatibility.allowed) {
1884
+ if (!(compatibility.requiresOverride && allowBlocked)) {
1885
+ if (!json) {
1886
+ console.log(`${prefix}Skipped ${name}: ${compatibility.message}`);
1887
+ }
1888
+ return {
1889
+ status: "failed",
1890
+ version: currentVersion,
1891
+ error: `${compatibility.reasonCode}: ${compatibility.message}`
1892
+ };
1893
+ }
1894
+ if (!json) {
1895
+ console.log(
1896
+ `${prefix}${name}: compatibility override enabled (${compatibility.reasonCode}); continuing.`
1897
+ );
1898
+ }
1899
+ }
1164
1900
  if (!latest || latest === currentVersion) {
1165
1901
  if (!json) console.log(`${prefix}${name} is up to date (v${currentVersion}).`);
1166
- return true;
1902
+ return { status: "up_to_date", version: latest ?? currentVersion };
1903
+ }
1904
+ const preConfig = loadConfig();
1905
+ const previousManaged = preConfig.skillState[name];
1906
+ const installedPathBefore = resolveInstalledSkillPath(name, previousManaged);
1907
+ const currentHash = hashInstalledSkillPath(installedPathBefore);
1908
+ const modified = isLocalSkillModified(previousManaged?.contentHash ?? null, currentHash);
1909
+ if (modified && !forceModified) {
1910
+ if (!json) {
1911
+ console.log(
1912
+ `${prefix}Skipped ${name}: local modifications detected. Re-run with --force-modified to back up and replace.`
1913
+ );
1914
+ }
1915
+ return {
1916
+ status: "skipped_modified",
1917
+ version: currentVersion,
1918
+ error: "Local modifications detected. Re-run with --force-modified to back up and replace."
1919
+ };
1920
+ }
1921
+ let backupPath = null;
1922
+ if (currentHash) {
1923
+ backupPath = createSkillBackupSnapshot(name, installedPathBefore, {
1924
+ sourceVersion: preConfig.installedSkills[name]?.version ?? previousManaged?.lastKnownVersion ?? null,
1925
+ sourceHash: currentHash
1926
+ });
1927
+ if (!json && backupPath) {
1928
+ console.log(`${prefix}Backup snapshot created: ${backupPath}`);
1929
+ }
1167
1930
  }
1168
1931
  if (!json) console.log(`${prefix}Updating ${name} v${currentVersion} -> v${latest}...`);
1169
1932
  const archive = await downloadSkill(name);
1170
- await installSkill(name, archive);
1933
+ const { path: installedPathAfter } = await installSkill(name, archive);
1171
1934
  const now = (/* @__PURE__ */ new Date()).toISOString();
1172
1935
  const config = loadConfig();
1173
1936
  if (config.installedSkills[name]) {
1174
1937
  config.installedSkills[name].version = latest;
1175
1938
  config.installedSkills[name].updatedAt = now;
1176
- updateConfig({ installedSkills: config.installedSkills });
1177
1939
  }
1940
+ const contentHash = hashInstalledSkillPath(installedPathAfter);
1941
+ const nextManaged = buildNextManagedSkillState({
1942
+ name,
1943
+ version: latest,
1944
+ installedPath: installedPathAfter,
1945
+ previous: config.skillState[name],
1946
+ backupPath,
1947
+ hash: contentHash
1948
+ });
1949
+ updateConfig({
1950
+ installedSkills: config.installedSkills,
1951
+ skillState: { [name]: nextManaged }
1952
+ });
1178
1953
  if (!json) console.log(`${prefix}Updated ${name} to v${latest}.`);
1179
- return true;
1954
+ return { status: "updated", version: latest, backupPath };
1180
1955
  } catch (err) {
1956
+ const message = err instanceof Error ? err.message : String(err);
1181
1957
  if (!json) {
1182
- console.error(
1183
- `${prefix}Failed to update ${name}: ${err instanceof Error ? err.message : String(err)}`
1184
- );
1958
+ console.error(`${prefix}Failed to update ${name}: ${message}`);
1185
1959
  }
1186
- return false;
1960
+ return { status: "failed", error: message };
1187
1961
  }
1188
1962
  }
1189
1963
  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) {
1964
+ 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(
1965
+ "--force-modified",
1966
+ "Allow replacing locally modified skills (creates backup before overwrite)"
1967
+ ).option(
1968
+ "--allow-blocked",
1969
+ "Allow updating compatibility-gated skills (for explicit preview/testing workflows)"
1970
+ ).option("--json", "Output JSON").action(
1971
+ async (name, opts) => {
1972
+ const key = await retrieveLicenseKey();
1973
+ if (key) {
1974
+ const offlineResult = await verifyOffline(key);
1975
+ if (!offlineResult.valid) {
1976
+ if (opts.json) {
1977
+ console.log(
1978
+ JSON.stringify({
1979
+ ok: false,
1980
+ error: `License invalid: ${offlineResult.error}`
1981
+ })
1982
+ );
1983
+ } else {
1984
+ console.error(`License invalid: ${offlineResult.error}`);
1985
+ }
1986
+ process.exit(1);
1987
+ }
1988
+ }
1989
+ const cache = loadLicenseCache();
1990
+ if (cache && !cache.payload.hasUpdates) {
1195
1991
  if (opts.json) {
1196
- console.log(JSON.stringify({ ok: false, error: `License invalid: ${offlineResult.error}` }));
1992
+ console.log(
1993
+ JSON.stringify({
1994
+ ok: false,
1995
+ error: "Updates not included in your license. Visit pushrec.com to upgrade."
1996
+ })
1997
+ );
1197
1998
  } else {
1198
- console.error(`License invalid: ${offlineResult.error}`);
1999
+ console.error(
2000
+ "Updates not included in your license. Visit pushrec.com to upgrade."
2001
+ );
1199
2002
  }
1200
2003
  process.exit(1);
1201
2004
  }
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) {
2005
+ const config = loadConfig();
2006
+ const installed = config.installedSkills;
2007
+ if (Object.keys(installed).length === 0) {
1225
2008
  if (opts.json) {
1226
- console.log(JSON.stringify({ ok: false, error: `${name} is not installed` }));
2009
+ console.log(
2010
+ JSON.stringify({ ok: true, updated: 0, upToDate: 0, skippedModified: 0, failed: 0 })
2011
+ );
1227
2012
  } else {
1228
- console.error(`${name} is not installed.`);
2013
+ console.log("No skills installed. Run `pushrec-skills install <name>` first.");
1229
2014
  }
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 }));
2015
+ return;
1235
2016
  }
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;
2017
+ if (name) {
2018
+ const skill = installed[name];
2019
+ if (!skill) {
2020
+ if (opts.json) {
2021
+ console.log(JSON.stringify({ ok: false, error: `${name} is not installed` }));
2022
+ } else {
2023
+ console.error(`${name} is not installed.`);
2024
+ }
2025
+ process.exit(1);
1255
2026
  }
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++;
2027
+ const result = await updateOne(
2028
+ name,
2029
+ skill.version,
2030
+ "",
2031
+ !!opts.json,
2032
+ !!opts.forceModified,
2033
+ !!opts.allowBlocked
2034
+ );
2035
+ if (opts.json) {
2036
+ console.log(
2037
+ JSON.stringify({
2038
+ ok: result.status === "updated" || result.status === "up_to_date",
2039
+ skill: name,
2040
+ status: result.status,
2041
+ version: result.version ?? null,
2042
+ backupPath: result.backupPath ?? null,
2043
+ error: result.error
2044
+ })
2045
+ );
1262
2046
  }
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++;
2047
+ if (result.status === "failed" || result.status === "skipped_modified") {
2048
+ process.exit(1);
2049
+ }
2050
+ return;
1267
2051
  }
2052
+ if (!opts.json) console.log("Checking for updates...\n");
2053
+ const entries = Object.entries(installed);
2054
+ let updated = 0;
2055
+ let failed = 0;
2056
+ let upToDate = 0;
2057
+ let skippedModified = 0;
2058
+ const results = [];
2059
+ for (let i = 0; i < entries.length; i++) {
2060
+ const [skillName, skill] = entries[i];
2061
+ const prefix = opts.json ? "" : `[${i + 1}/${entries.length}] `;
2062
+ const result = await updateOne(
2063
+ skillName,
2064
+ skill.version,
2065
+ prefix,
2066
+ !!opts.json,
2067
+ !!opts.forceModified,
2068
+ !!opts.allowBlocked
2069
+ );
2070
+ results.push({
2071
+ skill: skillName,
2072
+ status: result.status,
2073
+ version: result.version,
2074
+ backupPath: result.backupPath,
2075
+ error: result.error
2076
+ });
2077
+ if (result.status === "updated") updated++;
2078
+ if (result.status === "up_to_date") upToDate++;
2079
+ if (result.status === "skipped_modified") skippedModified++;
2080
+ if (result.status === "failed") failed++;
2081
+ }
2082
+ if (opts.json) {
2083
+ console.log(
2084
+ JSON.stringify({
2085
+ ok: failed === 0,
2086
+ updated,
2087
+ upToDate,
2088
+ skippedModified,
2089
+ failed,
2090
+ results
2091
+ })
2092
+ );
2093
+ } else {
2094
+ console.log(
2095
+ `
2096
+ Done! ${updated} updated, ${upToDate} up to date, ${skippedModified} skipped (local modifications), ${failed} failed.`
2097
+ );
2098
+ if (skippedModified > 0) {
2099
+ console.log(
2100
+ "Use --force-modified to back up and replace locally modified skills."
2101
+ );
2102
+ }
2103
+ }
2104
+ if (failed > 0) process.exit(1);
1268
2105
  }
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
- });
2106
+ );
1279
2107
  return cmd;
1280
2108
  }
1281
2109
 
@@ -1325,14 +2153,14 @@ ${entries.length} skills installed.`);
1325
2153
  return;
1326
2154
  }
1327
2155
  console.log(
1328
- `${pad("NAME", 30)} ${pad("VERSION", 12)} ${pad("STATUS", 14)} CATEGORY`
2156
+ `${pad("NAME", 30)} ${pad("VERSION", 12)} ${pad("STATUS", 14)} ${pad("COMPAT", 16)} CATEGORY`
1329
2157
  );
1330
- console.log("-".repeat(78));
2158
+ console.log("-".repeat(98));
1331
2159
  for (const skill of catalog.items) {
1332
2160
  const installed = isSkillInstalled(skill.name);
1333
2161
  const status = skill.isFree ? installed ? "free/installed" : "free" : installed ? "installed" : "available";
1334
2162
  console.log(
1335
- `${pad(skill.name, 30)} ${pad(skill.currentVersion ?? "-", 12)} ${pad(status, 14)} ${skill.category ?? "-"}`
2163
+ `${pad(skill.name, 30)} ${pad(skill.currentVersion ?? "-", 12)} ${pad(status, 14)} ${pad(compatibilitySummary(skill), 16)} ${skill.category ?? "-"}`
1336
2164
  );
1337
2165
  }
1338
2166
  console.log(`
@@ -1421,6 +2249,11 @@ function infoCommand() {
1421
2249
  console.log(` Free: ${manifest.isFree ? "yes" : "no"}`);
1422
2250
  console.log(` Published: ${manifest.publishedAt ? manifest.publishedAt.split("T")[0] : "-"}`);
1423
2251
  console.log(` Installed: ${installed ? `yes (v${installedInfo?.version ?? "?"})` : "no"}`);
2252
+ console.log(` Compat: ${compatibilitySummary(manifest)}`);
2253
+ const compatEval = evaluateSkillCompatibility(manifest, { client: "claude" });
2254
+ if (!compatEval.allowed || compatEval.status === "ALLOW_WITH_WARNING") {
2255
+ console.log(` CompatNote: ${compatEval.reasonCode} \u2014 ${compatEval.message}`);
2256
+ }
1424
2257
  if (manifest.versions && manifest.versions.length > 0) {
1425
2258
  console.log("");
1426
2259
  console.log(" Versions:");
@@ -1441,7 +2274,7 @@ function infoCommand() {
1441
2274
  var import_commander7 = require("commander");
1442
2275
  var import_chalk2 = __toESM(require("chalk"));
1443
2276
  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) => {
2277
+ return new import_commander7.Command("uninstall").description("Uninstall a skill from the active Claude skills root (default ~/.claude/skills/)").argument("<skill-name>", "Name of skill to uninstall").option("--json", "Output JSON").action(async (skillName, opts) => {
1445
2278
  try {
1446
2279
  const removed = uninstallSkill(skillName);
1447
2280
  if (!removed) {
@@ -1471,9 +2304,91 @@ function createUninstallCommand() {
1471
2304
  // src/commands/health.ts
1472
2305
  var import_commander8 = require("commander");
1473
2306
  var import_chalk3 = __toESM(require("chalk"));
1474
- function healthCommand() {
1475
- const cmd = new import_commander8.Command("health").description("Check registry server health").option("--full", "Run full diagnostic (registry + license + device + skills)").option("--json", "Output as JSON").action(async (opts) => {
1476
- if (opts.full) {
2307
+
2308
+ // src/lib/client-detect.ts
2309
+ var import_fs9 = require("fs");
2310
+ var import_path10 = require("path");
2311
+ function pathExists(path, ctx) {
2312
+ return (ctx?.existsSync ?? import_fs9.existsSync)(path);
2313
+ }
2314
+ function nowIso(ctx) {
2315
+ return ctx?.nowIso?.() ?? (/* @__PURE__ */ new Date()).toISOString();
2316
+ }
2317
+ function detectionCandidates(client, roots) {
2318
+ const profileHome = roots.profileHome;
2319
+ if (!profileHome) return [];
2320
+ switch (client) {
2321
+ case "claude":
2322
+ return [
2323
+ (0, import_path10.join)(profileHome, "settings.json"),
2324
+ (0, import_path10.join)(profileHome, "skills"),
2325
+ profileHome
2326
+ ];
2327
+ case "codex":
2328
+ return [
2329
+ (0, import_path10.join)(profileHome, "config.toml"),
2330
+ (0, import_path10.join)(profileHome, "auth.json"),
2331
+ (0, import_path10.join)(profileHome, "version.json"),
2332
+ (0, import_path10.join)(profileHome, "skills"),
2333
+ profileHome
2334
+ ];
2335
+ case "gemini":
2336
+ return [
2337
+ (0, import_path10.join)(profileHome, "installation_id"),
2338
+ (0, import_path10.join)(profileHome, "projects.json"),
2339
+ (0, import_path10.join)(profileHome, "GEMINI.md"),
2340
+ profileHome
2341
+ ];
2342
+ case "antigravity":
2343
+ return [
2344
+ (0, import_path10.join)(profileHome, "settings.json"),
2345
+ (0, import_path10.join)(profileHome, "globalStorage"),
2346
+ profileHome
2347
+ ];
2348
+ default: {
2349
+ const exhaustive = client;
2350
+ throw new Error(`Unsupported client: ${String(exhaustive)}`);
2351
+ }
2352
+ }
2353
+ }
2354
+ function detectClientProfile(client, ctx) {
2355
+ const roots = resolveClientInstallRoots(client, ctx);
2356
+ const candidates = detectionCandidates(client, roots);
2357
+ const hits = candidates.filter((candidate) => pathExists(candidate, ctx));
2358
+ const detected = hits.length > 0;
2359
+ return {
2360
+ client,
2361
+ detected,
2362
+ selected: detected,
2363
+ profileHome: roots.profileHome,
2364
+ skillsRoot: roots.skillsRoot,
2365
+ nativeSkillRootVerified: roots.nativeSkillRootVerified,
2366
+ previewProfile: roots.previewProfile,
2367
+ lastDetectedAt: detected ? nowIso(ctx) : null,
2368
+ installedSkills: {},
2369
+ notes: [
2370
+ ...roots.notes,
2371
+ ...detected ? [`Detected profile footprint via ${hits[0]}.`] : ["No profile footprint detected on this machine."]
2372
+ ]
2373
+ };
2374
+ }
2375
+ function detectClientProfiles(ctx) {
2376
+ const profiles = {};
2377
+ const detectedClients = [];
2378
+ for (const client of SUPPORTED_CLIENTS) {
2379
+ const profile = detectClientProfile(client, ctx);
2380
+ profiles[client] = profile;
2381
+ if (profile.detected) {
2382
+ detectedClients.push(client);
2383
+ }
2384
+ }
2385
+ return { profiles, detectedClients };
2386
+ }
2387
+
2388
+ // src/commands/health.ts
2389
+ function healthCommand() {
2390
+ const cmd = new import_commander8.Command("health").description("Check registry server health").option("--full", "Run full diagnostic (registry + license + device + skills)").option("--json", "Output as JSON").action(async (opts) => {
2391
+ if (opts.full) {
1477
2392
  return runFullDiagnostic(opts.json ?? false);
1478
2393
  }
1479
2394
  let response;
@@ -1518,6 +2433,35 @@ function healthCommand() {
1518
2433
  });
1519
2434
  return cmd;
1520
2435
  }
2436
+ function pushClientDetectionCheck(checks) {
2437
+ try {
2438
+ const detected = detectClientProfiles();
2439
+ const detectedNames = detected.detectedClients;
2440
+ const detectedCount = detectedNames.length;
2441
+ const previewDetected = detectedNames.filter(
2442
+ (client) => detected.profiles[client]?.previewProfile
2443
+ );
2444
+ const claudeDetected = Boolean(detected.profiles.claude?.detected);
2445
+ let detail = detectedCount > 0 ? `${detectedCount} detected (${detectedNames.join(", ")})` : "No supported local client profiles detected";
2446
+ if (!claudeDetected) {
2447
+ detail += " | Claude Code profile not detected";
2448
+ }
2449
+ if (previewDetected.length > 0) {
2450
+ detail += ` | preview profiles: ${previewDetected.join(", ")}`;
2451
+ }
2452
+ checks.push({
2453
+ name: "Clients",
2454
+ status: detectedCount > 0 ? "pass" : "warn",
2455
+ detail
2456
+ });
2457
+ } catch (err) {
2458
+ checks.push({
2459
+ name: "Clients",
2460
+ status: "warn",
2461
+ detail: `Detection error: ${err instanceof Error ? err.message : String(err)}`
2462
+ });
2463
+ }
2464
+ }
1521
2465
  async function runFullDiagnostic(json) {
1522
2466
  const checks = [];
1523
2467
  try {
@@ -1535,6 +2479,7 @@ async function runFullDiagnostic(json) {
1535
2479
  checks.push({ name: "License", status: "fail", detail: "No license key found. Run: pushrec-skills setup" });
1536
2480
  checks.push({ name: "Device", status: "skip", detail: "Skipped (no license)" });
1537
2481
  checks.push({ name: "Skills", status: "skip", detail: "Skipped (no license)" });
2482
+ pushClientDetectionCheck(checks);
1538
2483
  return printDiagnostic(checks, json);
1539
2484
  }
1540
2485
  const offlineResult = await verifyOffline(key);
@@ -1542,6 +2487,7 @@ async function runFullDiagnostic(json) {
1542
2487
  checks.push({ name: "License", status: "fail", detail: `Invalid signature: ${offlineResult.error}` });
1543
2488
  checks.push({ name: "Device", status: "skip", detail: "Skipped (invalid license)" });
1544
2489
  checks.push({ name: "Skills", status: "skip", detail: "Skipped (invalid license)" });
2490
+ pushClientDetectionCheck(checks);
1545
2491
  return printDiagnostic(checks, json);
1546
2492
  }
1547
2493
  const fingerprint = getMachineFingerprint();
@@ -1580,6 +2526,7 @@ async function runFullDiagnostic(json) {
1580
2526
  } else {
1581
2527
  checks.push({ name: "Skills", status: "warn", detail: "No skills installed. Run: pushrec-skills install --all" });
1582
2528
  }
2529
+ pushClientDetectionCheck(checks);
1583
2530
  printDiagnostic(checks, json);
1584
2531
  }
1585
2532
  function printDiagnostic(checks, json) {
@@ -1609,88 +2556,459 @@ function printDiagnostic(checks, json) {
1609
2556
  }
1610
2557
 
1611
2558
  // src/commands/setup.ts
2559
+ var import_fs11 = require("fs");
2560
+ var import_path12 = require("path");
1612
2561
  var import_commander9 = require("commander");
1613
2562
  var import_readline2 = require("readline");
1614
2563
  var import_chalk4 = __toESM(require("chalk"));
1615
2564
  var import_ora2 = __toESM(require("ora"));
2565
+
2566
+ // src/lib/setup-state.ts
2567
+ var import_crypto5 = require("crypto");
2568
+ function createSetupRunId(now = /* @__PURE__ */ new Date()) {
2569
+ const ts = now.toISOString().replace(/[:.]/g, "-");
2570
+ const suffix = (0, import_crypto5.randomBytes)(3).toString("hex");
2571
+ return `setup-${ts}-${suffix}`;
2572
+ }
2573
+ function beginSetupRunState(current, params) {
2574
+ const nowIso2 = params?.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
2575
+ return {
2576
+ ...current,
2577
+ schemaVersion: current.schemaVersion || 1,
2578
+ currentRunId: params?.runId ?? createSetupRunId(new Date(nowIso2)),
2579
+ completedPhases: [],
2580
+ lastRunAt: nowIso2,
2581
+ lastError: null
2582
+ };
2583
+ }
2584
+ function markSetupPhaseCompletedState(current, phase, params) {
2585
+ const nowIso2 = params?.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
2586
+ const completed = current.completedPhases.includes(phase) ? current.completedPhases : [...current.completedPhases, phase];
2587
+ return {
2588
+ ...current,
2589
+ schemaVersion: current.schemaVersion || 1,
2590
+ completedPhases: completed,
2591
+ lastRunAt: nowIso2
2592
+ };
2593
+ }
2594
+ function failSetupRunState(current, error, params) {
2595
+ const nowIso2 = params?.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
2596
+ return {
2597
+ ...current,
2598
+ schemaVersion: current.schemaVersion || 1,
2599
+ lastRunAt: nowIso2,
2600
+ lastError: error
2601
+ };
2602
+ }
2603
+ function finishSetupRunState(current, params) {
2604
+ const nowIso2 = params?.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
2605
+ const withComplete = current.completedPhases.includes("complete") ? current.completedPhases : [...current.completedPhases, "complete"];
2606
+ return {
2607
+ ...current,
2608
+ schemaVersion: current.schemaVersion || 1,
2609
+ currentRunId: null,
2610
+ completedPhases: withComplete,
2611
+ lastRunAt: nowIso2,
2612
+ lastError: null
2613
+ };
2614
+ }
2615
+ function nextConfigWithSetup(config, setup) {
2616
+ return { ...config, setup };
2617
+ }
2618
+ function beginSetupRunCheckpoint(params) {
2619
+ const config = loadConfig();
2620
+ const setup = beginSetupRunState(config.setup, params);
2621
+ return updateConfig(nextConfigWithSetup(config, setup));
2622
+ }
2623
+ function completeSetupPhaseCheckpoint(phase, params) {
2624
+ const config = loadConfig();
2625
+ const setup = markSetupPhaseCompletedState(config.setup, phase, params);
2626
+ return updateConfig(nextConfigWithSetup(config, setup));
2627
+ }
2628
+ function failSetupRunCheckpoint(error, params) {
2629
+ const config = loadConfig();
2630
+ const setup = failSetupRunState(config.setup, error, params);
2631
+ return updateConfig(nextConfigWithSetup(config, setup));
2632
+ }
2633
+ function finishSetupRunCheckpoint(params) {
2634
+ const config = loadConfig();
2635
+ const setup = finishSetupRunState(config.setup, params);
2636
+ return updateConfig(nextConfigWithSetup(config, setup));
2637
+ }
2638
+
2639
+ // src/lib/vault-bootstrap.ts
2640
+ var import_fs10 = require("fs");
2641
+ var import_path11 = require("path");
2642
+ var VAULT_ROOT_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
2643
+ var DEFAULT_VAULT_DIRECTORIES = [
2644
+ "0-Inbox",
2645
+ "1-Projects",
2646
+ "2-Areas",
2647
+ "3-Resources",
2648
+ "4-Archive",
2649
+ "Daily",
2650
+ "Templates"
2651
+ ];
2652
+ function normalizeMarkdown(content) {
2653
+ return content.endsWith("\n") ? content : `${content}
2654
+ `;
2655
+ }
2656
+ function rootFilePath(vaultPath, name) {
2657
+ return (0, import_path11.join)(vaultPath, name);
2658
+ }
2659
+ function scanRootFile(vaultPath, name) {
2660
+ const path = rootFilePath(vaultPath, name);
2661
+ if (!(0, import_fs10.existsSync)(path)) {
2662
+ return { name, path, exists: false, sizeBytes: null };
2663
+ }
2664
+ const stat = (0, import_fs10.lstatSync)(path);
2665
+ return {
2666
+ name,
2667
+ path,
2668
+ exists: stat.isFile(),
2669
+ sizeBytes: stat.isFile() ? stat.size : null
2670
+ };
2671
+ }
2672
+ function scanVaultBootstrapState(vaultPathInput) {
2673
+ const vaultPath = (0, import_path11.resolve)(vaultPathInput);
2674
+ const exists = (0, import_fs10.existsSync)(vaultPath);
2675
+ let isDirectory = false;
2676
+ let isEmptyDirectory = null;
2677
+ let entryNames = [];
2678
+ if (exists) {
2679
+ const stat = (0, import_fs10.lstatSync)(vaultPath);
2680
+ if (!stat.isDirectory()) {
2681
+ throw new Error(`Vault path exists but is not a directory: ${vaultPath}`);
2682
+ }
2683
+ isDirectory = true;
2684
+ entryNames = (0, import_fs10.readdirSync)(vaultPath).sort((a, b) => a.localeCompare(b));
2685
+ isEmptyDirectory = entryNames.length === 0;
2686
+ }
2687
+ const rootFiles = {
2688
+ "CLAUDE.md": scanRootFile(vaultPath, "CLAUDE.md"),
2689
+ "AGENTS.md": scanRootFile(vaultPath, "AGENTS.md")
2690
+ };
2691
+ const requiredDirectories = DEFAULT_VAULT_DIRECTORIES.map((name) => {
2692
+ const path = (0, import_path11.join)(vaultPath, name);
2693
+ return {
2694
+ name,
2695
+ path,
2696
+ exists: (0, import_fs10.existsSync)(path) && (0, import_fs10.lstatSync)(path).isDirectory()
2697
+ };
2698
+ });
2699
+ const recommendedMode = exists && isDirectory && !isEmptyDirectory ? "adopt" : "initialize";
2700
+ return {
2701
+ vaultPath,
2702
+ exists,
2703
+ isDirectory,
2704
+ isEmptyDirectory,
2705
+ entryNames,
2706
+ recommendedMode,
2707
+ rootFiles,
2708
+ requiredDirectories
2709
+ };
2710
+ }
2711
+ function renderVaultRootFilesFromClaudeSource(claudeSourceContent) {
2712
+ const normalized = normalizeMarkdown(claudeSourceContent);
2713
+ return {
2714
+ claudeMd: normalized,
2715
+ // L3 scaffolding: AGENTS.md mirrors the same source content until client-aware generation lands.
2716
+ agentsMd: normalized
2717
+ };
2718
+ }
2719
+ function planVaultBootstrap(scan, rendered, options = {}) {
2720
+ const selectedMode = options.modeOverride ?? scan.recommendedMode;
2721
+ if (selectedMode === "adopt" && !scan.exists) {
2722
+ throw new Error(
2723
+ `Vault adopt mode requires an existing directory: ${scan.vaultPath}`
2724
+ );
2725
+ }
2726
+ const createMissingDirectories = options.createMissingDirectories ?? true;
2727
+ const createMissingRootFiles = options.createMissingRootFiles ?? true;
2728
+ const createDirectories = createMissingDirectories ? scan.requiredDirectories.filter((dir) => !dir.exists).map((dir) => dir.path) : [];
2729
+ const rootContentMap = {
2730
+ "CLAUDE.md": rendered.claudeMd,
2731
+ "AGENTS.md": rendered.agentsMd
2732
+ };
2733
+ const createRootFiles = [];
2734
+ const skippedExistingRootFiles = [];
2735
+ for (const name of VAULT_ROOT_FILE_NAMES) {
2736
+ const rootFile = scan.rootFiles[name];
2737
+ if (rootFile.exists) {
2738
+ skippedExistingRootFiles.push({ name, path: rootFile.path, reason: "exists" });
2739
+ continue;
2740
+ }
2741
+ if (!createMissingRootFiles) continue;
2742
+ createRootFiles.push({
2743
+ name,
2744
+ path: rootFile.path,
2745
+ bytes: Buffer.byteLength(rootContentMap[name], "utf-8")
2746
+ });
2747
+ }
2748
+ return {
2749
+ vaultPath: scan.vaultPath,
2750
+ mode: selectedMode,
2751
+ destructive: false,
2752
+ createDirectories,
2753
+ createRootFiles,
2754
+ skippedExistingRootFiles,
2755
+ requiredDirectories: scan.requiredDirectories,
2756
+ rootFiles: scan.rootFiles
2757
+ };
2758
+ }
2759
+ function applyVaultBootstrapPlan(plan, rendered) {
2760
+ (0, import_fs10.mkdirSync)(plan.vaultPath, { recursive: true });
2761
+ const createdDirectories = [];
2762
+ const createdRootFiles = [];
2763
+ const skippedExistingRootFiles = [...plan.skippedExistingRootFiles.map((x) => x.path)];
2764
+ for (const dir of plan.createDirectories) {
2765
+ if (!(0, import_fs10.existsSync)(dir)) {
2766
+ (0, import_fs10.mkdirSync)(dir, { recursive: true });
2767
+ createdDirectories.push(dir);
2768
+ }
2769
+ }
2770
+ const rootContentMap = {
2771
+ "CLAUDE.md": rendered.claudeMd,
2772
+ "AGENTS.md": rendered.agentsMd
2773
+ };
2774
+ for (const action of plan.createRootFiles) {
2775
+ if ((0, import_fs10.existsSync)(action.path)) {
2776
+ skippedExistingRootFiles.push(action.path);
2777
+ continue;
2778
+ }
2779
+ const content = rootContentMap[action.name];
2780
+ (0, import_fs10.writeFileSync)(action.path, content, "utf-8");
2781
+ createdRootFiles.push(action.path);
2782
+ }
2783
+ return {
2784
+ createdDirectories,
2785
+ createdRootFiles,
2786
+ skippedExistingRootFiles
2787
+ };
2788
+ }
2789
+
2790
+ // src/commands/setup.ts
2791
+ var DEFAULT_VAULT_MODE = "adopt";
2792
+ var VALID_VAULT_MODES = /* @__PURE__ */ new Set(["initialize", "adopt", "skip"]);
2793
+ var VAULT_MARKERS = [
2794
+ "0-Inbox",
2795
+ "1-Projects",
2796
+ "2-Areas",
2797
+ "3-Resources",
2798
+ "4-Archive",
2799
+ "Daily",
2800
+ "Templates"
2801
+ ];
2802
+ var DEFAULT_CLAUDE_ROOT_TEMPLATE = `# CLAUDE.md
2803
+
2804
+ This vault is managed by \`pushrec-skills setup\`.
2805
+
2806
+ ## Supported Path
2807
+
2808
+ - Claude Code is the native launch path for pushREC.
2809
+ - Codex, Gemini, and Antigravity are compatibility previews.
2810
+ - Windows support is compatibility-gated until native proof evidence is collected.
2811
+
2812
+ ## Safety Rules
2813
+
2814
+ - Do not assume private maintainer paths.
2815
+ - Do not overwrite existing root files silently.
2816
+ - Keep compatibility labels explicit for non-Claude workflows.
2817
+ `;
2818
+ function parseVaultMode(input) {
2819
+ const value = (input ?? DEFAULT_VAULT_MODE).toLowerCase();
2820
+ if (!VALID_VAULT_MODES.has(value)) {
2821
+ throw new Error(
2822
+ `Invalid --vault-mode: ${input}. Expected one of: initialize, adopt, skip.`
2823
+ );
2824
+ }
2825
+ return value;
2826
+ }
2827
+ function hasVaultMarkerLayout(path) {
2828
+ let present = 0;
2829
+ for (const marker of VAULT_MARKERS) {
2830
+ if ((0, import_fs11.existsSync)((0, import_path12.join)(path, marker))) {
2831
+ present++;
2832
+ }
2833
+ }
2834
+ return present >= 3 && (0, import_fs11.existsSync)((0, import_path12.join)(path, "1-Projects"));
2835
+ }
2836
+ function resolveVaultPath(mode, optionPath) {
2837
+ if (mode === "skip") {
2838
+ return { path: null, source: "skip" };
2839
+ }
2840
+ if (optionPath) {
2841
+ return { path: (0, import_path12.resolve)(optionPath), source: "--vault-path" };
2842
+ }
2843
+ const envPath = process.env.PUSHREC_VAULT_PATH;
2844
+ if (envPath) {
2845
+ return { path: (0, import_path12.resolve)(envPath), source: "PUSHREC_VAULT_PATH" };
2846
+ }
2847
+ const cwd = process.cwd();
2848
+ if (hasVaultMarkerLayout(cwd)) {
2849
+ return { path: (0, import_path12.resolve)(cwd), source: "cwd-detected" };
2850
+ }
2851
+ throw new Error(
2852
+ "Vault path is ambiguous. Pass --vault-path <absolute-or-relative-path> or set PUSHREC_VAULT_PATH."
2853
+ );
2854
+ }
2855
+ function validateVaultPath(mode, vaultPath) {
2856
+ if (mode !== "adopt") return;
2857
+ if (!(0, import_fs11.existsSync)(vaultPath)) {
2858
+ throw new Error(
2859
+ `Vault adopt mode requires an existing directory: ${vaultPath}`
2860
+ );
2861
+ }
2862
+ const stat = (0, import_fs11.lstatSync)(vaultPath);
2863
+ if (!stat.isDirectory()) {
2864
+ throw new Error(`Vault path is not a directory: ${vaultPath}`);
2865
+ }
2866
+ }
2867
+ function resolveClaudeTemplateSource() {
2868
+ const override = process.env.PUSHREC_CLAUDE_TEMPLATE;
2869
+ if (override?.trim()) {
2870
+ return override;
2871
+ }
2872
+ return DEFAULT_CLAUDE_ROOT_TEMPLATE;
2873
+ }
1616
2874
  function readInput2(question) {
1617
2875
  const rl = (0, import_readline2.createInterface)({
1618
2876
  input: process.stdin,
1619
2877
  output: process.stdout
1620
2878
  });
1621
- return new Promise((resolve2, reject) => {
2879
+ return new Promise((resolve7, reject) => {
1622
2880
  rl.on("error", (err) => {
1623
2881
  rl.close();
1624
2882
  reject(err);
1625
2883
  });
1626
2884
  rl.on("close", () => {
1627
- resolve2("");
2885
+ resolve7("");
1628
2886
  });
1629
2887
  rl.question(question, (answer) => {
1630
2888
  rl.close();
1631
- resolve2(answer.trim());
2889
+ resolve7(answer.trim());
1632
2890
  });
1633
2891
  });
1634
2892
  }
1635
2893
  function setupCommand() {
1636
- const cmd = new import_commander9.Command("setup").description("One-command onboarding: verify license, activate device, install all skills").option("--key <license-key>", "License key (or enter interactively)").option("--json", "Output JSON").action(async (opts) => {
1637
- const existing = await retrieveLicenseKey();
1638
- if (existing) {
2894
+ const cmd = new import_commander9.Command("setup").description("One-command onboarding: verify license, activate device, install all skills").option("--key <license-key>", "License key (or enter interactively)").option(
2895
+ "--vault-mode <mode>",
2896
+ "Vault bootstrap mode: initialize, adopt, or skip (default: adopt)",
2897
+ DEFAULT_VAULT_MODE
2898
+ ).option("--vault-path <path>", "Vault path for bootstrap/adopt").option(
2899
+ "--allow-blocked",
2900
+ "Allow installing compatibility-gated skills during setup (preview/testing only)"
2901
+ ).option("--json", "Output JSON").action(async (opts) => {
2902
+ let setupRunStarted = false;
2903
+ let existing = "";
2904
+ let detectedClients = [];
2905
+ let vaultSummary = {
2906
+ mode: DEFAULT_VAULT_MODE,
2907
+ status: "SKIPPED",
2908
+ vaultPath: null,
2909
+ pathSource: "skip",
2910
+ recommendedMode: null,
2911
+ createdDirectories: [],
2912
+ createdRootFiles: [],
2913
+ skippedExistingRootFiles: []
2914
+ };
2915
+ const recordSetupFailure = (error) => {
2916
+ if (!setupRunStarted) return;
2917
+ try {
2918
+ failSetupRunCheckpoint(error);
2919
+ } catch {
2920
+ }
2921
+ };
2922
+ const failAndExit = (error) => {
2923
+ recordSetupFailure(error);
1639
2924
  if (opts.json) {
1640
- console.log(JSON.stringify({ ok: true, message: "Already set up" }));
2925
+ console.log(JSON.stringify({ ok: false, error }));
1641
2926
  } else {
1642
- console.log(import_chalk4.default.green("Already set up!") + " Run " + import_chalk4.default.cyan("pushrec-skills auth status") + " to check your license.");
1643
- console.log("To re-setup, run " + import_chalk4.default.cyan("pushrec-skills auth logout") + " first.");
2927
+ console.error(error);
1644
2928
  }
1645
- return;
2929
+ process.exit(1);
2930
+ };
2931
+ const checkpointPhase = (phase) => {
2932
+ try {
2933
+ completeSetupPhaseCheckpoint(phase);
2934
+ } catch (err) {
2935
+ const message = err instanceof Error ? err.message : String(err);
2936
+ failAndExit(`Failed to persist setup checkpoint (${phase}): ${message}`);
2937
+ }
2938
+ };
2939
+ let selectedVaultMode = DEFAULT_VAULT_MODE;
2940
+ try {
2941
+ selectedVaultMode = parseVaultMode(opts.vaultMode);
2942
+ } catch (err) {
2943
+ const message = err instanceof Error ? err.message : String(err);
2944
+ failAndExit(message);
2945
+ }
2946
+ vaultSummary.mode = selectedVaultMode;
2947
+ try {
2948
+ beginSetupRunCheckpoint();
2949
+ setupRunStarted = true;
2950
+ } catch (err) {
2951
+ const message = err instanceof Error ? err.message : String(err);
2952
+ if (opts.json) {
2953
+ console.log(JSON.stringify({ ok: false, error: `Failed to initialize setup state: ${message}` }));
2954
+ } else {
2955
+ console.error(`Failed to initialize setup state: ${message}`);
2956
+ }
2957
+ process.exit(1);
2958
+ }
2959
+ try {
2960
+ existing = await retrieveLicenseKey() ?? "";
2961
+ } catch (err) {
2962
+ const message = err instanceof Error ? err.message : String(err);
2963
+ failAndExit(`Failed to read existing license key: ${message}`);
1646
2964
  }
1647
2965
  if (!opts.json) {
1648
2966
  console.log("");
1649
2967
  console.log(import_chalk4.default.bold("Pushrec Skills Setup"));
1650
2968
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1651
2969
  console.log("");
2970
+ if (existing) {
2971
+ console.log(import_chalk4.default.dim("Existing license found in keychain \u2014 re-running setup to verify, reactivate, and sync installs."));
2972
+ console.log("");
2973
+ }
1652
2974
  }
1653
- const key = opts.key ?? await readInput2("Paste your license key: ");
2975
+ const key = (opts.key ?? existing) || await readInput2("Paste your license key: ");
1654
2976
  if (!key) {
1655
- if (opts.json) {
1656
- console.log(JSON.stringify({ ok: false, error: "No license key provided" }));
1657
- } else {
1658
- console.error("No license key provided.");
1659
- }
1660
- process.exit(1);
2977
+ failAndExit("No license key provided");
1661
2978
  }
2979
+ checkpointPhase("license_input");
1662
2980
  const verifySpinner = opts.json ? null : (0, import_ora2.default)("Verifying license signature...").start();
1663
- const offlineResult = await verifyOffline(key);
2981
+ const offlineResult = await verifyOffline(key).catch((err) => {
2982
+ const message = err instanceof Error ? err.message : String(err);
2983
+ verifySpinner?.fail(`Could not verify license signature: ${message}`);
2984
+ return failAndExit(`Could not verify license signature: ${message}`);
2985
+ });
1664
2986
  if (!offlineResult.valid) {
1665
2987
  verifySpinner?.fail(`Invalid license key: ${offlineResult.error}`);
1666
- if (opts.json) {
1667
- console.log(JSON.stringify({ ok: false, error: `Invalid license key: ${offlineResult.error}` }));
1668
- }
1669
- process.exit(1);
2988
+ failAndExit(`Invalid license key: ${offlineResult.error}`);
1670
2989
  }
1671
2990
  verifySpinner?.succeed("License signature valid");
2991
+ checkpointPhase("offline_verify");
1672
2992
  const onlineSpinner = opts.json ? null : (0, import_ora2.default)("Checking with server...").start();
1673
2993
  const fingerprint = getMachineFingerprint();
1674
- let onlineResult;
1675
- try {
1676
- onlineResult = await verifyLicenseOnline(key, fingerprint);
1677
- } catch (err) {
2994
+ const onlineResult = await verifyLicenseOnline(key, fingerprint).catch((err) => {
1678
2995
  const message = err instanceof Error ? err.message : String(err);
1679
2996
  onlineSpinner?.fail(`Could not reach server: ${message}`);
1680
- if (opts.json) {
1681
- console.log(JSON.stringify({ ok: false, error: `Could not reach server: ${message}` }));
1682
- }
1683
- process.exit(1);
1684
- }
2997
+ return failAndExit(`Could not reach server: ${message}`);
2998
+ });
1685
2999
  if (!onlineResult.valid) {
1686
3000
  onlineSpinner?.fail(`License rejected: ${onlineResult.error}`);
1687
- if (opts.json) {
1688
- console.log(JSON.stringify({ ok: false, error: `License rejected: ${onlineResult.error}` }));
1689
- }
1690
- process.exit(1);
3001
+ failAndExit(`License rejected: ${onlineResult.error}`);
1691
3002
  }
1692
3003
  onlineSpinner?.succeed("Server confirmed license active");
1693
- await storeLicenseKey(key);
3004
+ checkpointPhase("online_verify");
3005
+ try {
3006
+ await storeLicenseKey(key);
3007
+ } catch (err) {
3008
+ const message = err instanceof Error ? err.message : String(err);
3009
+ failAndExit(`Failed to store license key: ${message}`);
3010
+ }
3011
+ checkpointPhase("store_license");
1694
3012
  const deviceSpinner = opts.json ? null : (0, import_ora2.default)("Activating this device...").start();
1695
3013
  let activation;
1696
3014
  try {
@@ -1700,15 +3018,15 @@ function setupCommand() {
1700
3018
  getPlatformName(),
1701
3019
  getHostname()
1702
3020
  );
1703
- deviceSpinner?.succeed(`Device activated (${activation.deviceCount}/${activation.maxDevices} slots used)`);
3021
+ const deviceCount = activation.deviceCount ?? onlineResult.deviceCount ?? 1;
3022
+ const maxDevices = activation.maxDevices ?? onlineResult.maxDevices ?? 3;
3023
+ deviceSpinner?.succeed(`Device activated (${deviceCount}/${maxDevices} slots used)`);
1704
3024
  } catch (err) {
1705
3025
  const message = err instanceof Error ? err.message : String(err);
1706
3026
  deviceSpinner?.fail(`Device activation failed: ${message}`);
1707
- if (opts.json) {
1708
- console.log(JSON.stringify({ ok: false, error: `Device activation failed: ${message}` }));
1709
- }
1710
- process.exit(1);
3027
+ failAndExit(`Device activation failed: ${message}`);
1711
3028
  }
3029
+ checkpointPhase("activate_device");
1712
3030
  const cache = {
1713
3031
  verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
1714
3032
  expiresAt: onlineResult.updatesExpireAt ?? null,
@@ -1722,7 +3040,75 @@ function setupCommand() {
1722
3040
  deviceCount: onlineResult.deviceCount ?? 1
1723
3041
  }
1724
3042
  };
1725
- saveLicenseCache(cache);
3043
+ try {
3044
+ saveLicenseCache(cache);
3045
+ } catch (err) {
3046
+ const message = err instanceof Error ? err.message : String(err);
3047
+ failAndExit(`Failed to save license cache: ${message}`);
3048
+ }
3049
+ checkpointPhase("cache_verification");
3050
+ try {
3051
+ const detected = detectClientProfiles();
3052
+ detectedClients = detected.detectedClients;
3053
+ updateConfig({ clients: detected.profiles });
3054
+ } catch {
3055
+ }
3056
+ checkpointPhase("detect_clients");
3057
+ if (selectedVaultMode !== "skip") {
3058
+ let resolvedVaultPath = null;
3059
+ try {
3060
+ const resolved = resolveVaultPath(selectedVaultMode, opts.vaultPath);
3061
+ resolvedVaultPath = resolved.path;
3062
+ vaultSummary.pathSource = resolved.source;
3063
+ vaultSummary.vaultPath = resolved.path;
3064
+ checkpointPhase("resolve_vault");
3065
+ if (!resolved.path) {
3066
+ throw new Error("Resolved vault path is empty for non-skip mode.");
3067
+ }
3068
+ validateVaultPath(selectedVaultMode, resolved.path);
3069
+ const scan = scanVaultBootstrapState(resolved.path);
3070
+ const rendered = renderVaultRootFilesFromClaudeSource(resolveClaudeTemplateSource());
3071
+ const vaultModeForBootstrap = selectedVaultMode === "adopt" ? "adopt" : "initialize";
3072
+ const plan = planVaultBootstrap(scan, rendered, {
3073
+ modeOverride: vaultModeForBootstrap
3074
+ });
3075
+ vaultSummary.recommendedMode = scan.recommendedMode;
3076
+ checkpointPhase("plan_vault_bootstrap");
3077
+ const applyResult = applyVaultBootstrapPlan(plan, rendered);
3078
+ checkpointPhase("apply_vault_bootstrap");
3079
+ const vaultStatus = plan.mode === "adopt" ? "adopted" : "initialized";
3080
+ const previousVaultState = loadConfig().vault;
3081
+ const updatedVaultState = updateConfig({
3082
+ vault: {
3083
+ ...previousVaultState,
3084
+ status: vaultStatus,
3085
+ vaultPath: plan.vaultPath,
3086
+ claudeMdPath: (0, import_path12.join)(plan.vaultPath, "CLAUDE.md"),
3087
+ agentsMdPath: (0, import_path12.join)(plan.vaultPath, "AGENTS.md"),
3088
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
3089
+ }
3090
+ }).vault;
3091
+ vaultSummary.status = plan.mode === "adopt" ? "ADOPTED" : "INITIALIZED";
3092
+ vaultSummary.createdDirectories = applyResult.createdDirectories;
3093
+ vaultSummary.createdRootFiles = applyResult.createdRootFiles;
3094
+ vaultSummary.skippedExistingRootFiles = applyResult.skippedExistingRootFiles;
3095
+ vaultSummary.vaultPath = updatedVaultState.vaultPath;
3096
+ } catch (err) {
3097
+ if (resolvedVaultPath) {
3098
+ const previousVaultState = loadConfig().vault;
3099
+ updateConfig({
3100
+ vault: {
3101
+ ...previousVaultState,
3102
+ status: "error",
3103
+ vaultPath: resolvedVaultPath,
3104
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
3105
+ }
3106
+ });
3107
+ }
3108
+ const message = err instanceof Error ? err.message : String(err);
3109
+ failAndExit(`Vault bootstrap failed: ${message}`);
3110
+ }
3111
+ }
1726
3112
  if (!opts.json) {
1727
3113
  console.log("");
1728
3114
  console.log(import_chalk4.default.bold("Installing skills..."));
@@ -1732,34 +3118,70 @@ function setupCommand() {
1732
3118
  catalog = await fetchSkillCatalog();
1733
3119
  } catch (err) {
1734
3120
  const message = err instanceof Error ? err.message : String(err);
3121
+ recordSetupFailure(`Could not fetch catalog: ${message}`);
1735
3122
  if (opts.json) {
1736
- console.log(JSON.stringify({ ok: true, activated: true, installed: 0, error: `Could not fetch catalog: ${message}` }));
3123
+ console.log(
3124
+ JSON.stringify({
3125
+ ok: true,
3126
+ activated: true,
3127
+ installed: 0,
3128
+ error: `Could not fetch catalog: ${message}`,
3129
+ detectedClients,
3130
+ vault: vaultSummary
3131
+ })
3132
+ );
1737
3133
  } else {
1738
3134
  console.error(`Could not fetch catalog: ${message}`);
1739
3135
  console.log(import_chalk4.default.yellow("License activated but skill install failed. Run ") + import_chalk4.default.cyan("pushrec-skills install --all") + import_chalk4.default.yellow(" to retry."));
1740
3136
  }
1741
3137
  return;
1742
3138
  }
3139
+ checkpointPhase("fetch_catalog");
1743
3140
  const toInstall = catalog.items.filter((s) => !isSkillInstalled(s.name));
1744
3141
  let installed = 0;
1745
3142
  let failed = 0;
3143
+ let blocked = 0;
1746
3144
  for (let i = 0; i < toInstall.length; i++) {
1747
3145
  const skill = toInstall[i];
1748
3146
  const prefix = opts.json ? "" : `[${i + 1}/${toInstall.length}] `;
1749
3147
  const spinner = opts.json ? null : (0, import_ora2.default)(`${prefix}Installing ${skill.name}...`).start();
1750
3148
  try {
1751
3149
  const manifest = await fetchSkillManifest(skill.name);
3150
+ const compatibility = evaluateSkillCompatibility(manifest, { client: "claude" });
3151
+ if (!compatibility.allowed) {
3152
+ if (compatibility.requiresOverride && opts.allowBlocked) {
3153
+ spinner?.warn(
3154
+ `${prefix}${skill.name}: compatibility override enabled (${compatibility.reasonCode}); continuing.`
3155
+ );
3156
+ } else {
3157
+ spinner?.fail(`${prefix}Skipped ${skill.name}: ${compatibility.message}`);
3158
+ blocked++;
3159
+ continue;
3160
+ }
3161
+ }
1752
3162
  const archive = await downloadSkill(skill.name);
1753
3163
  const { path } = await installSkill(skill.name, archive);
1754
3164
  const now = (/* @__PURE__ */ new Date()).toISOString();
1755
3165
  const config = loadConfig();
3166
+ const installedVersion = manifest.currentVersion ?? "0.0.0";
1756
3167
  config.installedSkills[skill.name] = {
1757
3168
  name: skill.name,
1758
- version: manifest.currentVersion ?? "0.0.0",
3169
+ version: installedVersion,
1759
3170
  installedAt: config.installedSkills[skill.name]?.installedAt ?? now,
1760
3171
  updatedAt: now
1761
3172
  };
1762
- updateConfig({ installedSkills: config.installedSkills });
3173
+ const contentHash = hashInstalledSkillPath(path);
3174
+ const nextManaged = buildNextManagedSkillState({
3175
+ name: skill.name,
3176
+ version: installedVersion,
3177
+ installedPath: path,
3178
+ previous: config.skillState[skill.name],
3179
+ hash: contentHash
3180
+ });
3181
+ updateConfig({
3182
+ installedSkills: config.installedSkills,
3183
+ skillState: { [skill.name]: nextManaged }
3184
+ });
1763
3185
  spinner?.succeed(`${prefix}${skill.name} v${manifest.currentVersion ?? "latest"} installed to ${path}`);
1764
3186
  installed++;
1765
3187
  } catch (err) {
@@ -1768,35 +3190,870 @@ function setupCommand() {
1768
3190
  failed++;
1769
3191
  }
1770
3192
  }
3193
+ checkpointPhase("install_skills");
3194
+ try {
3195
+ copyBundledSkill("pushrec-skills");
3196
+ } catch {
3197
+ }
3198
+ checkpointPhase("copy_bundled_skill");
1771
3199
  const skipped = catalog.items.length - toInstall.length;
3200
+ try {
3201
+ finishSetupRunCheckpoint();
3202
+ } catch (err) {
3203
+ const message = err instanceof Error ? err.message : String(err);
3204
+ if (!opts.json) {
3205
+ console.log(import_chalk4.default.yellow(`Warning: setup completed but failed to persist final setup checkpoint: ${message}`));
3206
+ }
3207
+ }
1772
3208
  if (opts.json) {
1773
- console.log(JSON.stringify({
1774
- ok: true,
1775
- activated: true,
1776
- installed,
1777
- failed,
1778
- skipped,
1779
- total: catalog.items.length
1780
- }));
3209
+ console.log(
3210
+ JSON.stringify({
3211
+ ok: true,
3212
+ activated: true,
3213
+ installed,
3214
+ blocked,
3215
+ failed,
3216
+ skipped,
3217
+ total: catalog.items.length,
3218
+ detectedClients,
3219
+ vault: vaultSummary
3220
+ })
3221
+ );
1781
3222
  } else {
1782
3223
  console.log("");
1783
- console.log(import_chalk4.default.green.bold("Setup complete!"));
1784
- console.log(` ${import_chalk4.default.green("+")} ${installed} skills installed`);
1785
- if (skipped > 0) console.log(` ${import_chalk4.default.dim("-")} ${skipped} already installed`);
1786
- if (failed > 0) console.log(` ${import_chalk4.default.red("x")} ${failed} failed`);
3224
+ console.log(import_chalk4.default.green(" Setup complete!"));
3225
+ console.log(` ${import_chalk4.default.green("+")} ${installed} skills installed`);
3226
+ if (blocked > 0) console.log(` ${import_chalk4.default.yellow("!")} ${blocked} compatibility-gated/skipped`);
3227
+ if (skipped > 0) console.log(` ${import_chalk4.default.dim("-")} ${skipped} already installed`);
3228
+ if (failed > 0) console.log(` ${import_chalk4.default.red("x")} ${failed} failed`);
3229
+ if (selectedVaultMode === "skip") {
3230
+ console.log(` ${import_chalk4.default.dim("-")} Vault bootstrap skipped (--vault-mode skip)`);
3231
+ } else {
3232
+ const vaultStatusLabel = vaultSummary.status === "INITIALIZED" ? "initialized" : "adopted";
3233
+ console.log(
3234
+ ` ${import_chalk4.default.green("+")} Vault ${vaultStatusLabel} at ${vaultSummary.vaultPath ?? "(unknown path)"}`
3235
+ );
3236
+ console.log(
3237
+ ` created dirs: ${vaultSummary.createdDirectories.length}, root files: ${vaultSummary.createdRootFiles.length}, skipped root files: ${vaultSummary.skippedExistingRootFiles.length}`
3238
+ );
3239
+ }
3240
+ console.log("");
3241
+ console.log(` ${import_chalk4.default.bold("NEXT STEP:")} Open Claude Code and type ${import_chalk4.default.cyan("/pushrec-skills")}`);
3242
+ console.log("");
3243
+ console.log(" Quick reference:");
3244
+ console.log(` ${import_chalk4.default.cyan("/pushrec-skills")} \u2014 Your guided setup (start here)`);
3245
+ console.log(` ${import_chalk4.default.cyan("/hormozi")} \u2014 Business strategy frameworks`);
3246
+ console.log(` ${import_chalk4.default.cyan("/linkedin-copywriting")} \u2014 LinkedIn content creation`);
3247
+ console.log(` ${import_chalk4.default.cyan("/copywriting")} \u2014 Master copywriting foundations`);
1787
3248
  console.log("");
1788
- console.log("Your skills are ready. Use " + import_chalk4.default.cyan("/<skill-name>") + " in your next Claude Code session.");
1789
- console.log("Run " + import_chalk4.default.cyan("pushrec-skills health --full") + " to verify everything works.");
3249
+ if (detectedClients.length > 0) {
3250
+ console.log(` Detected local clients: ${detectedClients.join(", ")} ${import_chalk4.default.dim("(Claude-first install is active; preview client setup comes next)")}`);
3251
+ console.log("");
3252
+ }
3253
+ console.log(` Community: ${import_chalk4.default.underline("skool.com/pushrec-2909")}`);
3254
+ console.log(` Full catalog: ${import_chalk4.default.dim("npx @pushrec/skills list")}`);
1790
3255
  }
1791
3256
  });
1792
3257
  return cmd;
1793
3258
  }
1794
3259
 
3260
+ // src/commands/dashboard.ts
3261
+ var import_commander10 = require("commander");
3262
+ var import_http = require("http");
3263
+ var import_fs12 = require("fs");
3264
+ var import_path13 = require("path");
3265
+ var import_os7 = require("os");
3266
+ var import_child_process5 = require("child_process");
3267
+ var DEFAULT_PORT = 5174;
3268
+ function getPushrecSkillDir() {
3269
+ return (0, import_path13.join)(getLegacyClaudeSkillsRoot(), "pushrec-skills");
3270
+ }
3271
+ function getDataSources() {
3272
+ const pushrecSkillDir = getPushrecSkillDir();
3273
+ return {
3274
+ "/api/config": CONFIG_FILE,
3275
+ "/api/profile": (0, import_path13.join)(pushrecSkillDir, "state", "profile.json"),
3276
+ "/api/progress": (0, import_path13.join)(pushrecSkillDir, "state", "progress.json"),
3277
+ "/api/catalog": (0, import_path13.join)(pushrecSkillDir, "state", "catalog.json"),
3278
+ "/api/diagnostic": (0, import_path13.join)(pushrecSkillDir, "state", "diagnostic.json"),
3279
+ "/api/teams": (0, import_path13.join)(pushrecSkillDir, "state", "teams.json"),
3280
+ "/api/tasks": (0, import_path13.join)(pushrecSkillDir, "state", "tasks.json")
3281
+ };
3282
+ }
3283
+ function getYamlSources() {
3284
+ const pushrecSkillDir = getPushrecSkillDir();
3285
+ return {
3286
+ "/api/prerequisites": (0, import_path13.join)(
3287
+ pushrecSkillDir,
3288
+ "references",
3289
+ "prerequisites.yaml"
3290
+ )
3291
+ };
3292
+ }
3293
+ function expandPath(filepath) {
3294
+ if (filepath.startsWith("~/")) {
3295
+ return (0, import_path13.join)((0, import_os7.homedir)(), filepath.slice(2));
3296
+ }
3297
+ return filepath;
3298
+ }
3299
+ function serveJson(res, data) {
3300
+ res.setHeader("Content-Type", "application/json");
3301
+ res.setHeader("Cache-Control", "no-cache");
3302
+ res.setHeader("Access-Control-Allow-Origin", "*");
3303
+ res.end(data);
3304
+ }
3305
+ function serveError(res, status, message) {
3306
+ res.statusCode = status;
3307
+ res.setHeader("Content-Type", "application/json");
3308
+ res.end(JSON.stringify({ error: message }));
3309
+ }
3310
+ function parseYaml(raw) {
3311
+ try {
3312
+ return JSON.parse(raw);
3313
+ } catch {
3314
+ return { raw };
3315
+ }
3316
+ }
3317
+ function handleApiRequest(req, res) {
3318
+ const url = req.url ?? "";
3319
+ const dataSources = getDataSources();
3320
+ const yamlSources = getYamlSources();
3321
+ if (url === "/api/mtime") {
3322
+ const mtimes = {};
3323
+ for (const [endpoint, filepath] of Object.entries(dataSources)) {
3324
+ try {
3325
+ mtimes[endpoint] = (0, import_fs12.statSync)(expandPath(filepath)).mtimeMs;
3326
+ } catch {
3327
+ mtimes[endpoint] = 0;
3328
+ }
3329
+ }
3330
+ for (const [endpoint, filepath] of Object.entries(yamlSources)) {
3331
+ try {
3332
+ mtimes[endpoint] = (0, import_fs12.statSync)(expandPath(filepath)).mtimeMs;
3333
+ } catch {
3334
+ mtimes[endpoint] = 0;
3335
+ }
3336
+ }
3337
+ serveJson(res, JSON.stringify(mtimes));
3338
+ return true;
3339
+ }
3340
+ if (url in dataSources) {
3341
+ const filepath = dataSources[url];
3342
+ const resolved = expandPath(filepath);
3343
+ try {
3344
+ const data = (0, import_fs12.readFileSync)(resolved, "utf-8");
3345
+ serveJson(res, data);
3346
+ } catch {
3347
+ serveError(res, 500, `Failed to read ${filepath}`);
3348
+ }
3349
+ return true;
3350
+ }
3351
+ if (url in yamlSources) {
3352
+ const filepath = yamlSources[url];
3353
+ const resolved = expandPath(filepath);
3354
+ try {
3355
+ const raw = (0, import_fs12.readFileSync)(resolved, "utf-8");
3356
+ const data = parseYaml(raw);
3357
+ serveJson(res, JSON.stringify(data));
3358
+ } catch {
3359
+ serveError(res, 500, `Failed to read ${filepath}`);
3360
+ }
3361
+ return true;
3362
+ }
3363
+ return false;
3364
+ }
3365
+ function findDistDirectory() {
3366
+ const candidates = [
3367
+ (0, import_path13.join)(getLegacyClaudeSkillsRoot(), "generative-ui", "dist"),
3368
+ (0, import_path13.join)((0, import_os7.homedir)(), ".cache", "genui-dashboard", "dist")
3369
+ ];
3370
+ for (const candidate of candidates) {
3371
+ if ((0, import_fs12.existsSync)((0, import_path13.join)(candidate, "index.html"))) {
3372
+ return candidate;
3373
+ }
3374
+ }
3375
+ return null;
3376
+ }
3377
+ function serveStatic(distDir, req, res) {
3378
+ let urlPath = (req.url ?? "/").split("?")[0];
3379
+ if (urlPath === "/") urlPath = "/index.html";
3380
+ const filePath = (0, import_path13.join)(distDir, urlPath);
3381
+ if (!filePath.startsWith(distDir)) {
3382
+ serveError(res, 403, "Forbidden");
3383
+ return;
3384
+ }
3385
+ try {
3386
+ const content = (0, import_fs12.readFileSync)(filePath);
3387
+ const ext = filePath.split(".").pop() ?? "";
3388
+ const mimeTypes = {
3389
+ html: "text/html",
3390
+ js: "application/javascript",
3391
+ css: "text/css",
3392
+ json: "application/json",
3393
+ png: "image/png",
3394
+ svg: "image/svg+xml",
3395
+ ico: "image/x-icon",
3396
+ woff2: "font/woff2",
3397
+ woff: "font/woff"
3398
+ };
3399
+ res.setHeader("Content-Type", mimeTypes[ext] ?? "application/octet-stream");
3400
+ res.end(content);
3401
+ } catch {
3402
+ try {
3403
+ const indexContent = (0, import_fs12.readFileSync)((0, import_path13.join)(distDir, "index.html"));
3404
+ res.setHeader("Content-Type", "text/html");
3405
+ res.end(indexContent);
3406
+ } catch {
3407
+ serveError(res, 404, "Not found");
3408
+ }
3409
+ }
3410
+ }
3411
+ function openBrowser(url) {
3412
+ const platform5 = process.platform;
3413
+ const command = platform5 === "darwin" ? `open "${url}"` : platform5 === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
3414
+ (0, import_child_process5.exec)(command, () => {
3415
+ });
3416
+ }
3417
+ function dashboardCommand() {
3418
+ const cmd = new import_commander10.Command("dashboard").description("Launch the Generative UI dashboard").option("--port <port>", "Server port", String(DEFAULT_PORT)).option("--dev", "Start in dev mode with Vite HMR").option("--no-open", "Skip opening browser").action(
3419
+ async (opts) => {
3420
+ const port = parseInt(opts.port, 10);
3421
+ if (isNaN(port) || port < 1024 || port > 65535) {
3422
+ console.error(
3423
+ `ERROR: Port must be between 1024 and 65535, got "${opts.port}".`
3424
+ );
3425
+ process.exit(1);
3426
+ }
3427
+ if (opts.dev) {
3428
+ console.log("Dev mode is not yet supported via CLI.");
3429
+ console.log(
3430
+ "Use the Vite dev server directly: cd dashboard && npm run dev"
3431
+ );
3432
+ process.exit(0);
3433
+ }
3434
+ const distDir = findDistDirectory();
3435
+ if (!distDir) {
3436
+ console.error("ERROR: No built dashboard found.");
3437
+ console.error(
3438
+ " Build the dashboard first, or use --dev mode."
3439
+ );
3440
+ console.error(
3441
+ ` Expected: ${(0, import_path13.join)(getLegacyClaudeSkillsRoot(), "generative-ui", "dist", "index.html")}`
3442
+ );
3443
+ process.exit(1);
3444
+ }
3445
+ const server = (0, import_http.createServer)((req, res) => {
3446
+ if (req.url?.startsWith("/api/")) {
3447
+ const handled = handleApiRequest(req, res);
3448
+ if (handled) return;
3449
+ }
3450
+ serveStatic(distDir, req, res);
3451
+ });
3452
+ server.listen(port, () => {
3453
+ const url = `http://localhost:${port}`;
3454
+ console.log(`Dashboard running at ${url}`);
3455
+ console.log(` Serving: ${distDir}`);
3456
+ console.log(` Press Ctrl+C to stop`);
3457
+ console.log();
3458
+ if (opts.open !== false) {
3459
+ openBrowser(url);
3460
+ }
3461
+ });
3462
+ server.on("error", (err) => {
3463
+ if (err.code === "EADDRINUSE") {
3464
+ console.error(`ERROR: Port ${port} is already in use.`);
3465
+ console.error(
3466
+ " Stop the process currently using this port with your platform's process tools, then retry."
3467
+ );
3468
+ } else {
3469
+ console.error(`ERROR: ${err.message}`);
3470
+ }
3471
+ process.exit(1);
3472
+ });
3473
+ const shutdown = () => {
3474
+ console.log("\nShutting down dashboard...");
3475
+ server.close(() => {
3476
+ process.exit(0);
3477
+ });
3478
+ setTimeout(() => process.exit(0), 3e3);
3479
+ };
3480
+ process.on("SIGINT", shutdown);
3481
+ process.on("SIGTERM", shutdown);
3482
+ if (process.platform === "win32") {
3483
+ process.on("SIGBREAK", shutdown);
3484
+ }
3485
+ }
3486
+ );
3487
+ return cmd;
3488
+ }
3489
+
3490
+ // src/commands/rollback.ts
3491
+ var import_fs13 = require("fs");
3492
+ var import_path14 = require("path");
3493
+ var import_commander11 = require("commander");
3494
+ function resolveBackupSelection(backups, selector) {
3495
+ if (backups.length === 0) {
3496
+ throw new Error("No backups recorded for this skill.");
3497
+ }
3498
+ if (!selector) {
3499
+ return backups[backups.length - 1];
3500
+ }
3501
+ if (backups.includes(selector)) {
3502
+ return selector;
3503
+ }
3504
+ const matches = backups.filter((path) => {
3505
+ const base = (0, import_path14.basename)(path);
3506
+ return base.includes(selector) || path.endsWith(selector);
3507
+ });
3508
+ if (matches.length === 1) {
3509
+ return matches[0];
3510
+ }
3511
+ if (matches.length > 1) {
3512
+ throw new Error(
3513
+ `Backup selector '${selector}' is ambiguous. Matches: ${matches.join(", ")}`
3514
+ );
3515
+ }
3516
+ throw new Error(`Backup '${selector}' not found in recorded backups.`);
3517
+ }
3518
+ function restoreBackupDirectory(backupPath, installedPath) {
3519
+ if (!(0, import_fs13.existsSync)(backupPath) || !(0, import_fs13.lstatSync)(backupPath).isDirectory()) {
3520
+ throw new Error(`Backup directory does not exist: ${backupPath}`);
3521
+ }
3522
+ (0, import_fs13.mkdirSync)((0, import_path14.dirname)(installedPath), { recursive: true });
3523
+ (0, import_fs13.rmSync)(installedPath, { recursive: true, force: true });
3524
+ (0, import_fs13.cpSync)(backupPath, installedPath, { recursive: true });
3525
+ const metadataInTarget = (0, import_path14.join)(installedPath, SKILL_BACKUP_METADATA_FILE);
3526
+ if ((0, import_fs13.existsSync)(metadataInTarget)) {
3527
+ (0, import_fs13.rmSync)(metadataInTarget, { force: true });
3528
+ }
3529
+ }
3530
+ function rollbackCommand() {
3531
+ const cmd = new import_commander11.Command("rollback").description("Rollback an installed skill to a backup snapshot").argument("<name>", "Skill name to rollback").option("--backup-id <id>", "Backup id/path selector (defaults to latest)").option("--json", "Output JSON").action(async (name, opts) => {
3532
+ const config = loadConfig();
3533
+ const managed = config.skillState[name];
3534
+ if (!managed) {
3535
+ const error = `${name} has no managed state; rollback unavailable.`;
3536
+ if (opts.json) {
3537
+ console.log(JSON.stringify({ ok: false, error }));
3538
+ } else {
3539
+ console.error(error);
3540
+ }
3541
+ process.exit(1);
3542
+ }
3543
+ let selectedBackup;
3544
+ try {
3545
+ selectedBackup = resolveBackupSelection(managed.backups, opts.backupId);
3546
+ } catch (err) {
3547
+ const error = err instanceof Error ? err.message : String(err);
3548
+ if (opts.json) {
3549
+ console.log(JSON.stringify({ ok: false, error }));
3550
+ } else {
3551
+ console.error(error);
3552
+ }
3553
+ process.exit(1);
3554
+ }
3555
+ const installedPath = resolveInstalledSkillPath(name, managed);
3556
+ const existingHash = hashInstalledSkillPath(installedPath);
3557
+ let preRollbackBackup = null;
3558
+ if (existingHash) {
3559
+ preRollbackBackup = createSkillBackupSnapshot(name, installedPath, {
3560
+ sourceVersion: config.installedSkills[name]?.version ?? managed.lastKnownVersion ?? null,
3561
+ sourceHash: existingHash
3562
+ });
3563
+ }
3564
+ try {
3565
+ restoreBackupDirectory(selectedBackup, installedPath);
3566
+ } catch (err) {
3567
+ const error = err instanceof Error ? err.message : String(err);
3568
+ if (opts.json) {
3569
+ console.log(JSON.stringify({ ok: false, error }));
3570
+ } else {
3571
+ console.error(error);
3572
+ }
3573
+ process.exit(1);
3574
+ }
3575
+ const restoredHash = hashInstalledSkillPath(installedPath);
3576
+ const backupMetadata = readSkillBackupMetadata(selectedBackup);
3577
+ const restoredVersion = backupMetadata?.sourceVersion ?? managed.lastKnownVersion ?? config.installedSkills[name]?.version ?? "unknown";
3578
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3579
+ const nextInstalled = {
3580
+ ...config.installedSkills[name] ?? {
3581
+ name,
3582
+ version: restoredVersion,
3583
+ installedAt: now,
3584
+ updatedAt: now
3585
+ },
3586
+ name,
3587
+ version: restoredVersion,
3588
+ updatedAt: now
3589
+ };
3590
+ const nextManaged = buildNextManagedSkillState({
3591
+ name,
3592
+ version: restoredVersion,
3593
+ installedPath,
3594
+ previous: managed,
3595
+ backupPath: preRollbackBackup,
3596
+ hash: restoredHash
3597
+ });
3598
+ updateConfig({
3599
+ installedSkills: {
3600
+ [name]: nextInstalled
3601
+ },
3602
+ skillState: {
3603
+ [name]: nextManaged
3604
+ }
3605
+ });
3606
+ if (opts.json) {
3607
+ console.log(
3608
+ JSON.stringify({
3609
+ ok: true,
3610
+ skill: name,
3611
+ restoredFrom: selectedBackup,
3612
+ restoredVersion,
3613
+ installedPath,
3614
+ preRollbackBackup
3615
+ })
3616
+ );
3617
+ return;
3618
+ }
3619
+ console.log(`Rolled back ${name}.`);
3620
+ console.log(` restoredFrom: ${selectedBackup}`);
3621
+ console.log(` restoredVersion: ${restoredVersion}`);
3622
+ console.log(` installedPath: ${installedPath}`);
3623
+ if (preRollbackBackup) {
3624
+ console.log(` preRollbackBackup: ${preRollbackBackup}`);
3625
+ }
3626
+ });
3627
+ return cmd;
3628
+ }
3629
+
3630
+ // src/commands/sync.ts
3631
+ var import_commander12 = require("commander");
3632
+ var import_ora3 = __toESM(require("ora"));
3633
+ function resolveSyncTargets(name, catalogNames, installedNames, updatesOnly) {
3634
+ if (name) {
3635
+ return { mode: "single", targets: [name] };
3636
+ }
3637
+ if (updatesOnly) {
3638
+ return { mode: "bulk", targets: installedNames.sort((a, b) => a.localeCompare(b)) };
3639
+ }
3640
+ return { mode: "bulk", targets: [...catalogNames].sort((a, b) => a.localeCompare(b)) };
3641
+ }
3642
+ var SKIP_EXIT_BLOCKING_STATUSES = /* @__PURE__ */ new Set([
3643
+ "failed",
3644
+ "skipped_compatibility",
3645
+ "skipped_modified"
3646
+ ]);
3647
+ var LicenseGate = class {
3648
+ paidAllowed = null;
3649
+ async ensure(forFreeSkill) {
3650
+ if (forFreeSkill) return true;
3651
+ if (this.paidAllowed !== null) return this.paidAllowed;
3652
+ const key = await retrieveLicenseKey();
3653
+ if (!key) {
3654
+ this.paidAllowed = false;
3655
+ return false;
3656
+ }
3657
+ const offlineResult = await verifyOffline(key);
3658
+ if (!offlineResult.valid) {
3659
+ this.paidAllowed = false;
3660
+ return false;
3661
+ }
3662
+ const cache = loadLicenseCache();
3663
+ if (cache) {
3664
+ const status = checkOfflineStatus(cache);
3665
+ if (status.status === "disabled") {
3666
+ this.paidAllowed = false;
3667
+ return false;
3668
+ }
3669
+ if (status.status === "stale" || status.status === "warning") {
3670
+ try {
3671
+ const fresh = await verifyLicenseOnline(key, getMachineFingerprint());
3672
+ if (!fresh.valid) {
3673
+ this.paidAllowed = false;
3674
+ return false;
3675
+ }
3676
+ const refreshed = {
3677
+ ...cache,
3678
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
3679
+ expiresAt: fresh.updatesExpireAt ?? null,
3680
+ payload: {
3681
+ buyerId: fresh.buyerId ?? cache.payload.buyerId,
3682
+ email: fresh.email ?? cache.payload.email,
3683
+ maxDevices: fresh.maxDevices ?? cache.payload.maxDevices,
3684
+ status: fresh.status ?? cache.payload.status,
3685
+ hasUpdates: fresh.hasUpdates ?? cache.payload.hasUpdates,
3686
+ deviceCount: fresh.deviceCount ?? cache.payload.deviceCount
3687
+ }
3688
+ };
3689
+ saveLicenseCache(refreshed);
3690
+ } catch {
3691
+ }
3692
+ }
3693
+ this.paidAllowed = true;
3694
+ return true;
3695
+ }
3696
+ try {
3697
+ const fresh = await verifyLicenseOnline(key, getMachineFingerprint());
3698
+ this.paidAllowed = !!fresh.valid;
3699
+ return this.paidAllowed;
3700
+ } catch {
3701
+ this.paidAllowed = false;
3702
+ return false;
3703
+ }
3704
+ }
3705
+ };
3706
+ async function syncOneSkill(name, opts) {
3707
+ const spinner = opts.json ? null : (0, import_ora3.default)(`Syncing ${name}...`).start();
3708
+ const fail = (status, reasonCode, message, fromVersion) => {
3709
+ spinner?.fail(`${name}: ${message}`);
3710
+ return { skill: name, status, reasonCode, message, fromVersion: fromVersion ?? null };
3711
+ };
3712
+ try {
3713
+ const config = loadConfig();
3714
+ const installed = config.installedSkills[name];
3715
+ const installedVersion = installed?.version ?? null;
3716
+ if (!installed && opts.updatesOnly) {
3717
+ spinner?.info(`${name}: not installed locally (updates-only mode).`);
3718
+ return {
3719
+ skill: name,
3720
+ status: "skipped_not_installed",
3721
+ reasonCode: "UPDATES_ONLY",
3722
+ message: "Skill is not installed and --updates-only is enabled."
3723
+ };
3724
+ }
3725
+ const manifest = await fetchSkillManifest(name).catch((err) => {
3726
+ throw new Error(`manifest-fetch:${err instanceof Error ? err.message : String(err)}`);
3727
+ });
3728
+ const compatibility = evaluateSkillCompatibility(manifest, { client: "claude" });
3729
+ if (!compatibility.allowed) {
3730
+ if (!(compatibility.requiresOverride && opts.allowBlocked)) {
3731
+ return fail(
3732
+ "skipped_compatibility",
3733
+ "COMPATIBILITY_BLOCKED",
3734
+ `${compatibility.reasonCode}: ${compatibility.message}`,
3735
+ installedVersion
3736
+ );
3737
+ }
3738
+ spinner?.warn(
3739
+ `${name}: compatibility override enabled (${compatibility.reasonCode}); continuing.`
3740
+ );
3741
+ }
3742
+ const licensed = await opts.licenseGate.ensure(manifest.isFree);
3743
+ if (!licensed) {
3744
+ return fail(
3745
+ "failed",
3746
+ installed ? "UPDATE_FAILED" : "INSTALL_FAILED",
3747
+ "License validation failed for paid skill operation.",
3748
+ installedVersion
3749
+ );
3750
+ }
3751
+ if (!installed) {
3752
+ const targetVersion = manifest.currentVersion ?? "0.0.0";
3753
+ const archive2 = await downloadSkill(name);
3754
+ const { path: path2 } = await installSkill(name, archive2);
3755
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3756
+ const after = loadConfig();
3757
+ const nextInstalled2 = {
3758
+ ...after.installedSkills,
3759
+ [name]: {
3760
+ name,
3761
+ version: targetVersion,
3762
+ installedAt: now2,
3763
+ updatedAt: now2
3764
+ }
3765
+ };
3766
+ const contentHash2 = hashInstalledSkillPath(path2);
3767
+ const nextManaged2 = buildNextManagedSkillState({
3768
+ name,
3769
+ version: targetVersion,
3770
+ installedPath: path2,
3771
+ previous: after.skillState[name],
3772
+ hash: contentHash2
3773
+ });
3774
+ updateConfig({
3775
+ installedSkills: nextInstalled2,
3776
+ skillState: { [name]: nextManaged2 }
3777
+ });
3778
+ spinner?.succeed(`${name}: installed v${targetVersion}.`);
3779
+ return {
3780
+ skill: name,
3781
+ status: "installed",
3782
+ fromVersion: null,
3783
+ toVersion: targetVersion,
3784
+ installedPath: path2
3785
+ };
3786
+ }
3787
+ const latest = manifest.currentVersion;
3788
+ if (!latest || latest === installedVersion) {
3789
+ spinner?.succeed(`${name}: already up to date (${installedVersion ?? "-"})`);
3790
+ return {
3791
+ skill: name,
3792
+ status: "up_to_date",
3793
+ fromVersion: installedVersion,
3794
+ toVersion: latest ?? installedVersion
3795
+ };
3796
+ }
3797
+ const previousManaged = config.skillState[name];
3798
+ const installedPathBefore = resolveInstalledSkillPath(name, previousManaged);
3799
+ const currentHash = hashInstalledSkillPath(installedPathBefore);
3800
+ const modified = isLocalSkillModified(previousManaged?.contentHash ?? null, currentHash);
3801
+ if (modified && !opts.forceModified) {
3802
+ return fail(
3803
+ "skipped_modified",
3804
+ "LOCAL_MODIFIED",
3805
+ "Local modifications detected. Re-run with --force-modified to back up and replace.",
3806
+ installedVersion
3807
+ );
3808
+ }
3809
+ let backupPath = null;
3810
+ if (currentHash) {
3811
+ backupPath = createSkillBackupSnapshot(name, installedPathBefore, {
3812
+ sourceVersion: installedVersion,
3813
+ sourceHash: currentHash
3814
+ });
3815
+ }
3816
+ const archive = await downloadSkill(name);
3817
+ const { path } = await installSkill(name, archive);
3818
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3819
+ const refreshed = loadConfig();
3820
+ const nextInstalled = {
3821
+ ...refreshed.installedSkills,
3822
+ [name]: {
3823
+ name,
3824
+ version: latest,
3825
+ installedAt: refreshed.installedSkills[name]?.installedAt ?? now,
3826
+ updatedAt: now
3827
+ }
3828
+ };
3829
+ const contentHash = hashInstalledSkillPath(path);
3830
+ const nextManaged = buildNextManagedSkillState({
3831
+ name,
3832
+ version: latest,
3833
+ installedPath: path,
3834
+ previous: refreshed.skillState[name],
3835
+ backupPath,
3836
+ hash: contentHash
3837
+ });
3838
+ updateConfig({
3839
+ installedSkills: nextInstalled,
3840
+ skillState: { [name]: nextManaged }
3841
+ });
3842
+ spinner?.succeed(`${name}: updated ${installedVersion ?? "-"} -> ${latest}.`);
3843
+ return {
3844
+ skill: name,
3845
+ status: "updated",
3846
+ fromVersion: installedVersion,
3847
+ toVersion: latest,
3848
+ backupPath,
3849
+ installedPath: path
3850
+ };
3851
+ } catch (err) {
3852
+ const message = err instanceof Error ? err.message : String(err);
3853
+ if (message.startsWith("manifest-fetch:")) {
3854
+ return fail(
3855
+ "failed",
3856
+ "MANIFEST_FETCH_FAILED",
3857
+ message.replace("manifest-fetch:", "")
3858
+ );
3859
+ }
3860
+ return fail(
3861
+ "failed",
3862
+ "INSTALL_FAILED",
3863
+ message
3864
+ );
3865
+ }
3866
+ }
3867
+ function syncCommand() {
3868
+ const cmd = new import_commander12.Command("sync").description(
3869
+ "Synchronize local skills with the registry (install missing + update installed)"
3870
+ ).argument("[name]", "Optional single skill name to sync").option(
3871
+ "--updates-only",
3872
+ "Only update already-installed skills (skip installs for missing skills)"
3873
+ ).option(
3874
+ "--force-modified",
3875
+ "Allow replacing locally modified skills during update (creates backup before overwrite)"
3876
+ ).option(
3877
+ "--allow-blocked",
3878
+ "Allow syncing compatibility-gated skills (explicit preview/testing only)"
3879
+ ).option("--json", "Output machine-readable JSON").action(
3880
+ async (name, opts) => {
3881
+ const json = !!opts.json;
3882
+ const config = loadConfig();
3883
+ const installedNames = Object.keys(config.installedSkills);
3884
+ let catalogNames = [];
3885
+ if (!name || !opts.updatesOnly) {
3886
+ try {
3887
+ const catalog = await fetchSkillCatalog();
3888
+ catalogNames = catalog.items.map((item) => item.name);
3889
+ } catch (err) {
3890
+ const error = `Could not fetch catalog: ${err instanceof Error ? err.message : String(err)}`;
3891
+ if (json) {
3892
+ console.log(JSON.stringify({ ok: false, error }));
3893
+ } else {
3894
+ console.error(error);
3895
+ }
3896
+ process.exit(1);
3897
+ }
3898
+ }
3899
+ const resolved = resolveSyncTargets(
3900
+ name,
3901
+ catalogNames,
3902
+ installedNames,
3903
+ !!opts.updatesOnly
3904
+ );
3905
+ if (resolved.targets.length === 0) {
3906
+ if (json) {
3907
+ console.log(
3908
+ JSON.stringify({
3909
+ ok: true,
3910
+ mode: resolved.mode,
3911
+ totals: {
3912
+ total: 0,
3913
+ installed: 0,
3914
+ updated: 0,
3915
+ up_to_date: 0,
3916
+ skipped_not_installed: 0,
3917
+ skipped_compatibility: 0,
3918
+ skipped_modified: 0,
3919
+ failed: 0
3920
+ },
3921
+ results: []
3922
+ })
3923
+ );
3924
+ } else {
3925
+ console.log(
3926
+ opts.updatesOnly ? "No installed skills found to sync." : "No catalog skills found to sync."
3927
+ );
3928
+ }
3929
+ return;
3930
+ }
3931
+ const licenseGate = new LicenseGate();
3932
+ const results = [];
3933
+ for (const skillName of resolved.targets) {
3934
+ const result = await syncOneSkill(skillName, {
3935
+ updatesOnly: !!opts.updatesOnly,
3936
+ forceModified: !!opts.forceModified,
3937
+ allowBlocked: !!opts.allowBlocked,
3938
+ json,
3939
+ licenseGate
3940
+ });
3941
+ results.push(result);
3942
+ if (!json) {
3943
+ if (result.status === "failed") {
3944
+ console.log(` - ${skillName}: failed (${result.message ?? "unknown error"})`);
3945
+ } else if (result.status === "skipped_compatibility" || result.status === "skipped_modified" || result.status === "skipped_not_installed") {
3946
+ console.log(` - ${skillName}: ${result.status} (${result.message ?? "n/a"})`);
3947
+ }
3948
+ }
3949
+ }
3950
+ const totals = {
3951
+ total: results.length,
3952
+ installed: results.filter((x) => x.status === "installed").length,
3953
+ updated: results.filter((x) => x.status === "updated").length,
3954
+ up_to_date: results.filter((x) => x.status === "up_to_date").length,
3955
+ skipped_not_installed: results.filter((x) => x.status === "skipped_not_installed").length,
3956
+ skipped_compatibility: results.filter((x) => x.status === "skipped_compatibility").length,
3957
+ skipped_modified: results.filter((x) => x.status === "skipped_modified").length,
3958
+ failed: results.filter((x) => x.status === "failed").length
3959
+ };
3960
+ const ok = results.every((x) => !SKIP_EXIT_BLOCKING_STATUSES.has(x.status));
3961
+ if (json) {
3962
+ console.log(
3963
+ JSON.stringify(
3964
+ {
3965
+ ok,
3966
+ mode: resolved.mode,
3967
+ updatesOnly: !!opts.updatesOnly,
3968
+ allowBlocked: !!opts.allowBlocked,
3969
+ forceModified: !!opts.forceModified,
3970
+ totals,
3971
+ results
3972
+ },
3973
+ null,
3974
+ 2
3975
+ )
3976
+ );
3977
+ } else {
3978
+ console.log("");
3979
+ console.log("Sync summary");
3980
+ console.log(` - total: ${totals.total}`);
3981
+ console.log(` - installed: ${totals.installed}`);
3982
+ console.log(` - updated: ${totals.updated}`);
3983
+ console.log(` - up_to_date: ${totals.up_to_date}`);
3984
+ console.log(` - skipped_not_installed: ${totals.skipped_not_installed}`);
3985
+ console.log(` - skipped_compatibility: ${totals.skipped_compatibility}`);
3986
+ console.log(` - skipped_modified: ${totals.skipped_modified}`);
3987
+ console.log(` - failed: ${totals.failed}`);
3988
+ }
3989
+ if (!ok) {
3990
+ process.exit(1);
3991
+ }
3992
+ }
3993
+ );
3994
+ return cmd;
3995
+ }
3996
+
3997
+ // src/lib/version.ts
3998
+ var import_fs14 = require("fs");
3999
+ var import_path15 = require("path");
4000
+ var FALLBACK_VERSION = "0.0.0";
4001
+ var cachedVersion = null;
4002
+ function candidatePackageJsonPaths(deps) {
4003
+ const cwd = deps.cwd ?? process.cwd();
4004
+ const scriptPath = deps.scriptPath ?? process.argv[1] ?? null;
4005
+ const env = deps.env ?? process.env;
4006
+ const candidates = [
4007
+ typeof env.PUSHREC_CLI_PACKAGE_JSON === "string" ? env.PUSHREC_CLI_PACKAGE_JSON : null,
4008
+ scriptPath ? (0, import_path15.join)((0, import_path15.dirname)((0, import_path15.resolve)(scriptPath)), "..", "package.json") : null,
4009
+ (0, import_path15.join)(cwd, "package.json"),
4010
+ (0, import_path15.join)(cwd, "cli", "package.json")
4011
+ ].filter((path) => typeof path === "string" && path.length > 0);
4012
+ const seen = /* @__PURE__ */ new Set();
4013
+ return candidates.filter((path) => {
4014
+ if (seen.has(path)) return false;
4015
+ seen.add(path);
4016
+ return true;
4017
+ });
4018
+ }
4019
+ function parseVersionFromPackageJson(raw) {
4020
+ try {
4021
+ const parsed = JSON.parse(raw);
4022
+ if (typeof parsed.version !== "string") return null;
4023
+ const version = parsed.version.trim();
4024
+ return version.length > 0 ? version : null;
4025
+ } catch {
4026
+ return null;
4027
+ }
4028
+ }
4029
+ function getCliVersion(deps = {}) {
4030
+ if (cachedVersion && deps.cwd === void 0 && deps.scriptPath === void 0 && deps.env === void 0 && deps.existsSyncFn === void 0 && deps.readFileSyncFn === void 0) {
4031
+ return cachedVersion;
4032
+ }
4033
+ const existsFn = deps.existsSyncFn ?? import_fs14.existsSync;
4034
+ const readFn = deps.readFileSyncFn ?? import_fs14.readFileSync;
4035
+ for (const packageJsonPath of candidatePackageJsonPaths(deps)) {
4036
+ if (!existsFn(packageJsonPath)) continue;
4037
+ try {
4038
+ const raw = readFn(packageJsonPath, "utf-8");
4039
+ const version = parseVersionFromPackageJson(raw);
4040
+ if (!version) continue;
4041
+ if (deps.cwd === void 0 && deps.scriptPath === void 0 && deps.env === void 0 && deps.existsSyncFn === void 0 && deps.readFileSyncFn === void 0) {
4042
+ cachedVersion = version;
4043
+ }
4044
+ return version;
4045
+ } catch {
4046
+ continue;
4047
+ }
4048
+ }
4049
+ return FALLBACK_VERSION;
4050
+ }
4051
+
1795
4052
  // src/index.ts
1796
- var program = new import_commander10.Command();
4053
+ var program = new import_commander13.Command();
1797
4054
  program.name("pushrec-skills").description(
1798
4055
  "Install, update, and manage premium Claude Code skills from Pushrec"
1799
- ).version("0.2.0");
4056
+ ).version(getCliVersion());
1800
4057
  program.addCommand(authCommand());
1801
4058
  program.addCommand(installCommand());
1802
4059
  program.addCommand(updateCommand());
@@ -1806,6 +4063,9 @@ program.addCommand(infoCommand());
1806
4063
  program.addCommand(createUninstallCommand());
1807
4064
  program.addCommand(healthCommand());
1808
4065
  program.addCommand(setupCommand());
4066
+ program.addCommand(dashboardCommand());
4067
+ program.addCommand(rollbackCommand());
4068
+ program.addCommand(syncCommand());
1809
4069
  program.parseAsync().catch((err) => {
1810
4070
  console.error(err instanceof Error ? err.message : String(err));
1811
4071
  process.exit(1);