skillex 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +262 -1
  2. package/README.md +57 -10
  3. package/dist/auto-sync.d.ts +66 -0
  4. package/dist/auto-sync.js +91 -0
  5. package/dist/catalog.js +5 -29
  6. package/dist/cli.d.ts +13 -0
  7. package/dist/cli.js +247 -141
  8. package/dist/confirm.js +3 -1
  9. package/dist/direct-github.d.ts +60 -0
  10. package/dist/direct-github.js +177 -0
  11. package/dist/doctor.d.ts +31 -0
  12. package/dist/doctor.js +172 -0
  13. package/dist/downloader.d.ts +42 -0
  14. package/dist/downloader.js +41 -0
  15. package/dist/fs.d.ts +21 -1
  16. package/dist/fs.js +30 -3
  17. package/dist/http.d.ts +28 -7
  18. package/dist/http.js +143 -42
  19. package/dist/install.d.ts +23 -9
  20. package/dist/install.js +75 -348
  21. package/dist/lockfile.d.ts +46 -0
  22. package/dist/lockfile.js +169 -0
  23. package/dist/output.d.ts +11 -0
  24. package/dist/output.js +49 -0
  25. package/dist/recommended.d.ts +13 -0
  26. package/dist/recommended.js +21 -0
  27. package/dist/runner.js +9 -9
  28. package/dist/skill.d.ts +2 -0
  29. package/dist/skill.js +3 -0
  30. package/dist/sync.js +12 -9
  31. package/dist/types.d.ts +39 -0
  32. package/dist/types.js +28 -0
  33. package/dist/ui.js +1 -1
  34. package/dist/user-config.d.ts +5 -0
  35. package/dist/user-config.js +22 -1
  36. package/dist/web-ui.js +5 -0
  37. package/dist-ui/assets/CatalogPage-CbtMTkxd.js +1 -0
  38. package/dist-ui/assets/CatalogPage-W5MqylAz.css +1 -0
  39. package/dist-ui/assets/DoctorPage-oUZyX91t.js +1 -0
  40. package/dist-ui/assets/Skeleton-B_xm5L3P.js +1 -0
  41. package/dist-ui/assets/Skeleton-_Ooiw1nN.css +1 -0
  42. package/dist-ui/assets/SkillDetailPage-5JHQLq3q.js +1 -0
  43. package/dist-ui/assets/SkillDetailPage-CBAaWpcc.css +1 -0
  44. package/dist-ui/assets/{index-UBECch6X.css → index-CWm7zQTg.css} +1 -1
  45. package/dist-ui/assets/index-I0b-syhc.js +26 -0
  46. package/dist-ui/assets/recommended-D_i10hwH.js +1 -0
  47. package/dist-ui/index.html +2 -2
  48. package/package.json +2 -2
  49. package/dist-ui/assets/CatalogPage-B_qic36n.js +0 -1
  50. package/dist-ui/assets/SkillDetailPage-BJ3onKk4.js +0 -1
  51. package/dist-ui/assets/index-DN-z--cR.js +0 -25
package/dist/install.js CHANGED
@@ -1,13 +1,34 @@
1
+ /**
2
+ * Install / update / remove orchestration for catalog and direct-GitHub skills.
3
+ *
4
+ * Historically this module owned every install-related concern. Lockfile
5
+ * shape, direct-GitHub install, auto-sync, and the shared file downloader
6
+ * have been extracted into focused modules. This file now contains only
7
+ * orchestration and re-exports the moved symbols so existing imports keep
8
+ * working until callers migrate to the canonical paths.
9
+ *
10
+ * Re-export shim → canonical module mapping:
11
+ * - lockfile shape and source-list helpers → ./lockfile.js
12
+ * - direct GitHub parsing / fetch / download → ./direct-github.js
13
+ * - auto-sync orchestration → ./auto-sync.js
14
+ * - shared per-file download helper → ./downloader.js
15
+ */
1
16
  import * as path from "node:path";
2
- import { DEFAULT_INSTALL_SCOPE, DEFAULT_REF, DEFAULT_REPO, getScopedStatePaths, } from "./config.js";
3
- import { confirmAction } from "./confirm.js";
4
- import { ensureDir, pathExists, readJson, removePath, writeJson, writeText } from "./fs.js";
5
- import { fetchOptionalJson, fetchOptionalText, fetchText } from "./http.js";
6
- import { buildRawGitHubUrl, loadCatalog, resolveSource } from "./catalog.js";
17
+ import { DEFAULT_AGENT_SKILLS_DIR, DEFAULT_INSTALL_SCOPE, DEFAULT_REF, DEFAULT_REPO, getScopedStatePaths, } from "./config.js";
18
+ import { ensureDir, isPathInside, pathExists, readJson, removePath, writeJson, writeText } from "./fs.js";
19
+ import { loadCatalog, resolveSource } from "./catalog.js";
7
20
  import { resolveAdapterState } from "./adapters.js";
8
21
  import { loadInstalledSkillDocuments, syncAdapterFiles } from "./sync.js";
9
- import { parseSkillFrontmatter } from "./skill.js";
22
+ import { downloadSkillFiles, writeDownloadedManifest, } from "./downloader.js";
23
+ import { createBaseLockfile, dedupeSources, getLockfileSources, normalizeLockfile, normalizeSyncHistory, parseCatalogSource, PLACEHOLDER_REPOS, toLockfileSource, } from "./lockfile.js";
24
+ import { confirmDirectInstall, downloadDirectGitHubSkill, fetchDirectGitHubSkill, normalizeDirectManifest, parseDirectGitHubRef, parseGitHubSource, } from "./direct-github.js";
25
+ import { maybeAutoSync, maybeSyncAfterRemove, resolveSyncAdapterIds, } from "./auto-sync.js";
10
26
  import { CliError, InstallError } from "./types.js";
27
+ // Re-export everything moved to focused modules so existing imports keep working.
28
+ export { createBaseLockfile, dedupeSources, getLockfileSources, normalizeLockfile, normalizeSyncHistory, parseCatalogSource, PLACEHOLDER_REPOS, toLockfileSource, } from "./lockfile.js";
29
+ export { confirmDirectInstall, downloadDirectGitHubSkill, fetchDirectGitHubSkill, normalizeDirectManifest, parseDirectGitHubRef, parseGitHubSource, } from "./direct-github.js";
30
+ export { maybeAutoSync, maybeSyncAfterRemove, resolveSyncAdapterIds, } from "./auto-sync.js";
31
+ export { downloadSkillFiles, writeDownloadedManifest, } from "./downloader.js";
11
32
  /**
12
33
  * Initializes the local Skillex workspace state.
13
34
  *
@@ -105,7 +126,12 @@ export async function installSkills(requestedSkillIds, options = {}) {
105
126
  }
106
127
  }
107
128
  else if (!directRefs.length) {
108
- throw new InstallError("Provide at least one skill-id, use --all, or pass owner/repo[@ref].", "INSTALL_REQUIRES_SKILL");
129
+ throw new InstallError([
130
+ "No install target provided. Pick one of:",
131
+ " • skillex install <skill-id> [<skill-id> ...] from a configured catalog source",
132
+ " • skillex install --all install every skill in the catalog",
133
+ " • skillex install owner/repo[@ref] --trust install directly from a GitHub repo",
134
+ ].join("\n"), "INSTALL_REQUIRES_SKILL");
109
135
  }
110
136
  for (const directRef of directRefs) {
111
137
  if (!options.trust) {
@@ -132,7 +158,7 @@ export async function installSkills(requestedSkillIds, options = {}) {
132
158
  now,
133
159
  changed: installedSkills.length > 0,
134
160
  mode: options.mode,
135
- }, options.agentSkillsDir));
161
+ }, options.agentSkillsDir), syncInstalledSkills);
136
162
  return {
137
163
  installedCount: installedSkills.length,
138
164
  installedSkills,
@@ -221,7 +247,7 @@ export async function updateInstalledSkills(requestedSkillIds, options = {}) {
221
247
  now,
222
248
  changed: updatedSkills.length > 0,
223
249
  mode: options.mode,
224
- }, options.agentSkillsDir));
250
+ }, options.agentSkillsDir), syncInstalledSkills);
225
251
  return {
226
252
  statePaths,
227
253
  updatedSkills,
@@ -265,13 +291,17 @@ export async function removeSkills(requestedSkillIds, options = {}) {
265
291
  missingSkills.push(skillId);
266
292
  continue;
267
293
  }
268
- await removePath(resolveInstalledSkillPath(cwd, metadata.path));
294
+ const resolvedSkillPath = resolveInstalledSkillPath(cwd, metadata.path);
295
+ if (!isPathInside(resolvedSkillPath, statePaths.skillsDirPath)) {
296
+ throw new InstallError(`Refusing to remove "${skillId}": lockfile path "${metadata.path}" resolves outside the managed skills store (${statePaths.skillsDirPath}).`, "INSTALL_PATH_UNSAFE");
297
+ }
298
+ await removePath(resolvedSkillPath);
269
299
  delete lockfile.installed[skillId];
270
300
  removedSkills.push(skillId);
271
301
  }
272
302
  lockfile.updatedAt = now();
273
303
  await writeJson(statePaths.lockfilePath, lockfile);
274
- const autoSync = await maybeSyncAfterRemove(withAgentSkillsDir({
304
+ const autoSyncs = await maybeSyncAfterRemove(withAgentSkillsDir({
275
305
  cwd,
276
306
  scope: options.scope,
277
307
  adapters: lockfile.adapters,
@@ -282,12 +312,13 @@ export async function removeSkills(requestedSkillIds, options = {}) {
282
312
  now,
283
313
  changed: removedSkills.length > 0,
284
314
  mode: options.mode,
285
- }, options.agentSkillsDir));
315
+ }, options.agentSkillsDir), syncInstalledSkills);
286
316
  return {
287
317
  statePaths,
288
318
  removedSkills,
289
319
  missingSkills,
290
- autoSync,
320
+ autoSync: autoSyncs?.[0] ?? null,
321
+ autoSyncs: autoSyncs ?? [],
291
322
  };
292
323
  }
293
324
  catch (error) {
@@ -550,244 +581,24 @@ export async function listProjectSources(options = {}) {
550
581
  const fallbackSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
551
582
  return getLockfileSources(existing, fallbackSource);
552
583
  }
553
- /**
554
- * Parses a direct GitHub install reference in `owner/repo[@ref]` format.
555
- *
556
- * @param input - User-supplied install argument.
557
- * @returns Parsed direct GitHub reference or `null` when the value is not a direct ref.
558
- */
559
- export function parseDirectGitHubRef(input) {
560
- if (!input || input.startsWith("http://") || input.startsWith("https://")) {
561
- return null;
562
- }
563
- const match = input.trim().match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:@(.+))?$/);
564
- if (!match) {
565
- return null;
566
- }
567
- return {
568
- owner: match[1],
569
- repo: match[2],
570
- ref: match[3] || "main",
571
- };
572
- }
573
- function createBaseLockfile(source, now) {
574
- return {
575
- formatVersion: 1,
576
- createdAt: now(),
577
- updatedAt: now(),
578
- sources: [toLockfileSource(source)],
579
- adapters: {
580
- active: null,
581
- detected: [],
582
- },
583
- settings: {
584
- autoSync: true,
585
- },
586
- sync: null,
587
- syncHistory: {},
588
- syncMode: null,
589
- installed: {},
590
- };
591
- }
592
584
  async function downloadSkill(skill, catalog, skillsDirPath) {
593
585
  const skillTargetDir = path.join(skillsDirPath, skill.id);
594
- await removePath(skillTargetDir);
595
- await ensureDir(skillTargetDir);
596
- for (const relativePath of skill.files) {
597
- const remotePath = skill.path ? path.posix.join(skill.path, relativePath) : relativePath;
598
- const rawUrl = buildRawGitHubUrl(catalog.repo, catalog.ref, remotePath);
599
- const content = await fetchText(rawUrl, { headers: { Accept: "text/plain" } });
600
- const localPath = path.join(skillTargetDir, relativePath);
601
- await writeText(localPath, content);
602
- }
603
- await writeDownloadedManifest(skillTargetDir, {
586
+ await downloadSkillFiles({
587
+ repo: catalog.repo,
588
+ ref: catalog.ref,
589
+ skillRelPath: skill.path,
590
+ files: skill.files,
591
+ targetDir: skillTargetDir,
592
+ });
593
+ const downloaded = {
604
594
  ...skill,
605
595
  source: {
606
596
  repo: catalog.repo,
607
597
  ref: catalog.ref,
608
598
  path: skill.path,
609
599
  },
610
- });
611
- }
612
- async function downloadDirectGitHubSkill(skill, skillsDirPath) {
613
- const skillTargetDir = path.join(skillsDirPath, skill.manifest.id);
614
- await removePath(skillTargetDir);
615
- await ensureDir(skillTargetDir);
616
- for (const relativePath of skill.manifest.files) {
617
- const remotePath = skill.manifest.path ? path.posix.join(skill.manifest.path, relativePath) : relativePath;
618
- const rawUrl = buildRawGitHubUrl(skill.repo, skill.ref, remotePath);
619
- const content = await fetchText(rawUrl, { headers: { Accept: "text/plain" } });
620
- await writeText(path.join(skillTargetDir, relativePath), content);
621
- }
622
- await writeDownloadedManifest(skillTargetDir, {
623
- ...skill.manifest,
624
- source: {
625
- repo: skill.repo,
626
- ref: skill.ref,
627
- path: skill.manifest.path,
628
- },
629
- });
630
- }
631
- async function writeDownloadedManifest(skillTargetDir, manifest) {
632
- await writeJson(path.join(skillTargetDir, "skill.json"), manifest);
633
- }
634
- async function fetchDirectGitHubSkill(reference) {
635
- const repoId = `${reference.owner}/${reference.repo}`;
636
- const manifestUrl = buildRawGitHubUrl(repoId, reference.ref, "skill.json");
637
- const manifest = await fetchOptionalJson(manifestUrl, {
638
- headers: { Accept: "application/json" },
639
- });
640
- if (manifest) {
641
- return {
642
- repo: repoId,
643
- ref: reference.ref,
644
- source: `github:${repoId}@${reference.ref}`,
645
- manifest: normalizeDirectManifest(manifest, reference),
646
- };
647
- }
648
- const skillMarkdown = await fetchOptionalText(buildRawGitHubUrl(repoId, reference.ref, "SKILL.md"), {
649
- headers: { Accept: "text/plain" },
650
- });
651
- if (!skillMarkdown) {
652
- throw new InstallError(`No skill.json or SKILL.md found at ${repoId}@${reference.ref}.`, "DIRECT_SKILL_NOT_FOUND");
653
- }
654
- const frontmatter = parseSkillFrontmatter(skillMarkdown);
655
- return {
656
- repo: repoId,
657
- ref: reference.ref,
658
- source: `github:${repoId}@${reference.ref}`,
659
- manifest: {
660
- id: normalizeRepoSkillId(reference.repo),
661
- name: frontmatter.name || toTitleCase(reference.repo),
662
- version: "0.1.0",
663
- description: frontmatter.description || `Skill instalada diretamente de ${repoId}.`,
664
- author: reference.owner,
665
- tags: [],
666
- compatibility: [],
667
- entry: "SKILL.md",
668
- path: "",
669
- files: ["SKILL.md"],
670
- },
671
- };
672
- }
673
- function normalizeDirectManifest(manifest, reference) {
674
- return {
675
- id: manifest.id || normalizeRepoSkillId(reference.repo),
676
- name: manifest.name || toTitleCase(reference.repo),
677
- version: manifest.version || "0.1.0",
678
- description: manifest.description || `Skill instalada diretamente de ${reference.owner}/${reference.repo}.`,
679
- author: manifest.author || reference.owner,
680
- tags: Array.isArray(manifest.tags) ? manifest.tags : [],
681
- compatibility: Array.isArray(manifest.compatibility) ? manifest.compatibility : [],
682
- entry: manifest.entry || "SKILL.md",
683
- path: manifest.path || "",
684
- files: Array.isArray(manifest.files) && manifest.files.length > 0 ? manifest.files : [manifest.entry || "SKILL.md"],
685
- ...(manifest.scripts ? { scripts: manifest.scripts } : {}),
686
- };
687
- }
688
- function normalizeLockfile(existing, source, now) {
689
- if (!existing) {
690
- return createBaseLockfile(source, now);
691
- }
692
- const detectedAdapters = Array.isArray(existing.adapters)
693
- ? existing.adapters
694
- : Array.isArray(existing.adapters?.detected)
695
- ? existing.adapters.detected
696
- : [];
697
- const activeAdapter = Array.isArray(existing.adapters)
698
- ? existing.adapters[0] || null
699
- : existing.adapters?.active || detectedAdapters[0] || null;
700
- return {
701
- formatVersion: Number(existing.formatVersion || 1),
702
- createdAt: existing.createdAt || now(),
703
- updatedAt: existing.updatedAt || now(),
704
- sources: getLockfileSources(existing, source),
705
- adapters: {
706
- active: activeAdapter,
707
- detected: [...new Set(detectedAdapters.filter(Boolean))],
708
- },
709
- settings: {
710
- autoSync: existing.settings?.autoSync ?? true,
711
- },
712
- sync: existing.sync || null,
713
- syncHistory: normalizeSyncHistory(existing),
714
- syncMode: existing.syncMode || null,
715
- installed: existing.installed || {},
716
- };
717
- }
718
- function normalizeSyncHistory(existing) {
719
- const history = {};
720
- const candidate = existing && "syncHistory" in existing && existing.syncHistory && typeof existing.syncHistory === "object"
721
- ? existing.syncHistory
722
- : null;
723
- if (candidate) {
724
- for (const [adapterId, metadata] of Object.entries(candidate)) {
725
- if (!metadata || typeof metadata !== "object") {
726
- continue;
727
- }
728
- if (!("adapter" in metadata) || !("targetPath" in metadata) || !("syncedAt" in metadata)) {
729
- continue;
730
- }
731
- history[adapterId] = metadata;
732
- }
733
- }
734
- if (existing?.sync?.adapter && !history[existing.sync.adapter]) {
735
- history[existing.sync.adapter] = existing.sync;
736
- }
737
- return history;
738
- }
739
- /** Repos that are known placeholder values written by older versions and must be ignored. */
740
- const PLACEHOLDER_REPOS = new Set(["owner/repo"]);
741
- function getLockfileSources(existing, fallbackSource) {
742
- const legacyCatalog = getLegacyCatalog(existing);
743
- const configuredSources = Array.isArray(existing?.sources)
744
- ? existing.sources
745
- .filter((entry) => Boolean(entry?.repo))
746
- .filter((entry) => !PLACEHOLDER_REPOS.has(entry.repo))
747
- .map((entry) => ({
748
- repo: entry.repo,
749
- ref: entry.ref || DEFAULT_REF,
750
- ...(entry.label ? { label: entry.label } : {}),
751
- }))
752
- : [];
753
- if (configuredSources.length > 0) {
754
- return dedupeSources(configuredSources);
755
- }
756
- if (legacyCatalog?.repo && !PLACEHOLDER_REPOS.has(legacyCatalog.repo)) {
757
- return dedupeSources([
758
- {
759
- repo: legacyCatalog.repo,
760
- ref: legacyCatalog.ref || DEFAULT_REF,
761
- },
762
- ]);
763
- }
764
- return [toLockfileSource(fallbackSource)];
765
- }
766
- function getLegacyCatalog(existing) {
767
- if (!existing || !("catalog" in existing)) {
768
- return null;
769
- }
770
- const legacyState = existing;
771
- return legacyState.catalog || null;
772
- }
773
- function dedupeSources(sources) {
774
- const unique = new Map();
775
- for (const source of sources) {
776
- const key = `${source.repo}@${source.ref}`;
777
- if (!unique.has(key)) {
778
- unique.set(key, source);
779
- }
780
- }
781
- return [...unique.values()];
782
- }
783
- function toLockfileSource(source, label) {
784
- return {
785
- repo: source.repo,
786
- ref: source.ref,
787
- ...((label || source.repo === DEFAULT_REPO) && (label || source.repo === DEFAULT_REPO)
788
- ? { label: label || "official" }
789
- : {}),
790
600
  };
601
+ await writeDownloadedManifest(skillTargetDir, downloaded);
791
602
  }
792
603
  function resolvePrimarySourceOverride(options, existing) {
793
604
  const sources = getLockfileSources(existing, resolveSource(toCatalogSourceInput(options)));
@@ -849,16 +660,6 @@ async function resolveInstalledCatalogSelection(skillId, sourceRef, options, loc
849
660
  }
850
661
  return null;
851
662
  }
852
- function parseCatalogSource(source) {
853
- const match = source.match(/^catalog:([^@]+\/[^@]+)@(.+)$/);
854
- if (!match) {
855
- return null;
856
- }
857
- return {
858
- repo: match[1],
859
- ref: match[2],
860
- };
861
- }
862
663
  function buildInstalledMetadata(skill, context) {
863
664
  return {
864
665
  name: skill.name,
@@ -894,69 +695,6 @@ function getNow(options) {
894
695
  function toPosix(value) {
895
696
  return value.split(path.sep).join("/");
896
697
  }
897
- async function maybeAutoSync(options) {
898
- if (!options.enabled || !options.changed) {
899
- return null;
900
- }
901
- if (resolveSyncAdapterIds(options.adapters, options.adapterOverride).length === 0) {
902
- return null;
903
- }
904
- return syncInstalledSkills({
905
- cwd: options.cwd,
906
- scope: options.scope || DEFAULT_INSTALL_SCOPE,
907
- ...(options.agentSkillsDir ? { agentSkillsDir: options.agentSkillsDir } : {}),
908
- ...(options.adapterOverride ? { adapter: options.adapterOverride } : {}),
909
- ...(options.mode ? { mode: options.mode } : {}),
910
- now: options.now,
911
- });
912
- }
913
- async function maybeSyncAfterRemove(options) {
914
- if (!options.changed) {
915
- return null;
916
- }
917
- const adapters = new Set();
918
- for (const adapterId of Object.keys(options.syncHistory || {})) {
919
- adapters.add(adapterId);
920
- }
921
- if (options.legacySync?.adapter) {
922
- adapters.add(options.legacySync.adapter);
923
- }
924
- if (options.adapterOverride) {
925
- adapters.add(options.adapterOverride);
926
- }
927
- else if (options.enabled) {
928
- for (const adapterId of resolveSyncAdapterIds(options.adapters)) {
929
- adapters.add(adapterId);
930
- }
931
- }
932
- let result = null;
933
- for (const adapterId of adapters) {
934
- result = await syncInstalledSkills({
935
- cwd: options.cwd,
936
- scope: options.scope || DEFAULT_INSTALL_SCOPE,
937
- ...(options.agentSkillsDir ? { agentSkillsDir: options.agentSkillsDir } : {}),
938
- adapter: adapterId,
939
- ...(options.mode ? { mode: options.mode } : {}),
940
- now: options.now,
941
- });
942
- }
943
- return result;
944
- }
945
- function resolveSyncAdapterIds(adapters, adapterOverride) {
946
- if (adapterOverride) {
947
- return [adapterOverride];
948
- }
949
- const adapterIds = [];
950
- if (adapters.active) {
951
- adapterIds.push(adapters.active);
952
- }
953
- for (const adapterId of adapters.detected || []) {
954
- if (!adapterIds.includes(adapterId)) {
955
- adapterIds.push(adapterId);
956
- }
957
- }
958
- return adapterIds;
959
- }
960
698
  function toCatalogSourceInput(options, overrides = {}) {
961
699
  const input = {};
962
700
  if (options.owner) {
@@ -997,36 +735,6 @@ function resolveStatePathsForOptions(cwd, options) {
997
735
  function resolveInstalledSkillPath(cwd, skillPath) {
998
736
  return path.isAbsolute(skillPath) ? skillPath : path.resolve(cwd, skillPath);
999
737
  }
1000
- async function confirmDirectInstall(skillRef, options) {
1001
- const warning = `Warning: ${skillRef} will be installed directly from GitHub and has not been verified by the active catalog.`;
1002
- (options.warn || console.error)(warning);
1003
- const confirm = options.confirm || (() => confirmAction("Continuar com a instalacao direta?"));
1004
- const accepted = await confirm();
1005
- if (!accepted) {
1006
- throw new InstallError("Instalacao direta cancelada pelo usuario.", "INSTALL_CANCELLED");
1007
- }
1008
- }
1009
- function parseGitHubSource(source) {
1010
- if (!source.startsWith("github:")) {
1011
- return null;
1012
- }
1013
- const withoutPrefix = source.slice("github:".length);
1014
- const separatorIndex = withoutPrefix.lastIndexOf("@");
1015
- if (separatorIndex <= 0) {
1016
- return null;
1017
- }
1018
- return parseDirectGitHubRef(`${withoutPrefix.slice(0, separatorIndex)}@${withoutPrefix.slice(separatorIndex + 1)}`);
1019
- }
1020
- function normalizeRepoSkillId(repo) {
1021
- return repo.trim().toLowerCase();
1022
- }
1023
- function toTitleCase(skillId) {
1024
- return skillId
1025
- .split("-")
1026
- .filter(Boolean)
1027
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
1028
- .join(" ");
1029
- }
1030
738
  function toInstallError(error, fallbackMessage) {
1031
739
  if (error instanceof InstallError) {
1032
740
  return error;
@@ -1034,6 +742,25 @@ function toInstallError(error, fallbackMessage) {
1034
742
  if (error instanceof CliError) {
1035
743
  return new InstallError(error.message, error.code);
1036
744
  }
1037
- const message = error instanceof Error ? error.message : String(error);
1038
- return new InstallError(`${fallbackMessage}: ${message}`);
1039
- }
745
+ // Preserve the underlying error code when present (HttpError, NodeJS.ErrnoException, etc.)
746
+ // so callers can react programmatically to specific failure modes such as
747
+ // HTTP_RATE_LIMIT, HTTP_TIMEOUT, EACCES, ENOENT, etc.
748
+ const underlyingCode = error && typeof error === "object" && "code" in error
749
+ ? String(error.code)
750
+ : null;
751
+ const baseMessage = error instanceof Error ? error.message : String(error);
752
+ const annotatedMessage = underlyingCode
753
+ ? `${fallbackMessage}: ${baseMessage} (${underlyingCode})`
754
+ : `${fallbackMessage}: ${baseMessage}`;
755
+ return new InstallError(annotatedMessage, underlyingCode || "INSTALL_ERROR");
756
+ }
757
+ // Silence unused-import warnings while keeping the symbols available as named re-exports.
758
+ void DEFAULT_AGENT_SKILLS_DIR;
759
+ void DEFAULT_REF;
760
+ void DEFAULT_REPO;
761
+ void dedupeSources;
762
+ void normalizeSyncHistory;
763
+ void normalizeDirectManifest;
764
+ void createBaseLockfile;
765
+ void PLACEHOLDER_REPOS;
766
+ void resolveSyncAdapterIds;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Lockfile shape, normalization, source-list management, and migration helpers.
3
+ *
4
+ * Keeps `install.ts` focused on install/update/remove orchestration. All
5
+ * callers should import from here directly; `install.ts` re-exports these
6
+ * symbols for backward compatibility with existing test imports.
7
+ */
8
+ import type { CatalogSource, LockfileSource, LockfileState, NowFn, SyncHistory } from "./types.js";
9
+ /** Repos that are known placeholder values written by older versions and must be ignored. */
10
+ export declare const PLACEHOLDER_REPOS: Set<string>;
11
+ /**
12
+ * Builds an empty lockfile seeded with a single source and `autoSync` on by default.
13
+ */
14
+ export declare function createBaseLockfile(source: CatalogSource, now: NowFn): LockfileState;
15
+ /**
16
+ * Normalizes a possibly-legacy lockfile shape into the current `LockfileState`.
17
+ * Handles arrays-of-strings adapters from very old versions, missing
18
+ * `syncHistory`, and the deprecated single `sync` field.
19
+ */
20
+ export declare function normalizeLockfile(existing: LockfileState | null, source: CatalogSource, now: NowFn): LockfileState;
21
+ /**
22
+ * Coerces the historic and current shapes of `syncHistory` into the
23
+ * normalized form, including a fallback that rebuilds the history from a
24
+ * legacy single-`sync` field.
25
+ */
26
+ export declare function normalizeSyncHistory(existing: LockfileState | null): SyncHistory;
27
+ /**
28
+ * Resolves the configured source list, dropping placeholders and falling back
29
+ * to legacy single-catalog metadata when no `sources` array exists.
30
+ */
31
+ export declare function getLockfileSources(existing: LockfileState | null, fallbackSource: CatalogSource): LockfileSource[];
32
+ /**
33
+ * Deduplicates a source list keyed by `${repo}@${ref}`, preserving the
34
+ * first occurrence (which carries any label).
35
+ */
36
+ export declare function dedupeSources(sources: LockfileSource[]): LockfileSource[];
37
+ /**
38
+ * Converts a `CatalogSource` to a `LockfileSource`, attaching a label only
39
+ * when one is explicitly provided or when the source is the default
40
+ * first-party repo (which gets the `official` label automatically).
41
+ */
42
+ export declare function toLockfileSource(source: CatalogSource, label?: string): LockfileSource;
43
+ /**
44
+ * Parses a `catalog:owner/repo@ref` source string into a `LockfileSource`.
45
+ */
46
+ export declare function parseCatalogSource(source: string): LockfileSource | null;