skills-package-manager 0.3.0 → 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 (3) hide show
  1. package/README.md +15 -8
  2. package/dist/index.js +659 -91
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -31,7 +31,9 @@ spm add owner/repo --skill find-skills
31
31
 
32
32
  # Direct specifier — skip discovery
33
33
  spm add https://github.com/owner/repo.git#path:/skills/my-skill
34
- spm add file:./local-source#path:/skills/my-skill
34
+ spm add link:./local-source/skills/my-skill
35
+ spm add file:./skills-package.tgz#path:/skills/my-skill
36
+ spm add npm:@scope/skills-package#path:/skills/my-skill
35
37
  ```
36
38
 
37
39
  After `spm add`, the newly added skills are resolved, materialized into `installDir`, and linked to each configured `linkTarget` immediately.
@@ -86,7 +88,7 @@ This resolves each skill from its specifier, materializes it into `installDir` (
86
88
 
87
89
  ### `spm update`
88
90
 
89
- Refresh git-based skills declared in `skills.json` without changing the manifest:
91
+ Refresh resolvable skills declared in `skills.json` without changing the manifest:
90
92
 
91
93
  ```bash
92
94
  spm update
@@ -96,8 +98,8 @@ spm update find-skills rspress-custom-theme
96
98
  Behavior:
97
99
 
98
100
  - Uses `skills.json` as the source of truth
99
- - Re-resolves git refs to the latest commit
100
- - Skips `file:` skills
101
+ - Re-resolves git refs and npm package targets
102
+ - Skips `link:` skills
101
103
  - Fails immediately for unknown skill names
102
104
  - Writes `skills-lock.yaml` only after fetch and link succeed
103
105
 
@@ -123,13 +125,14 @@ const skills = await listRepoSkills('vercel-labs', 'skills')
123
125
 
124
126
  ## Specifier Format
125
127
 
126
- ```
127
- <source>#[ref&]path:<skill-path>
128
+ ```text
129
+ git/file/npm: <source>#[ref&]path:<skill-path>
130
+ link: link:<path-to-skill-dir>
128
131
  ```
129
132
 
130
133
  | Part | Description | Example |
131
134
  |------|-------------|---------|
132
- | `source` | Git URL or `file:` path | `https://github.com/o/r.git`, `file:./local` |
135
+ | `source` | Git URL, direct `link:` skill path, `file:` tarball, or `npm:` package name | `https://github.com/o/r.git`, `link:./local/skills/my-skill`, `file:./skills.tgz`, `npm:@scope/pkg` |
133
136
  | `ref` | Optional git ref | `main`, `v1.0.0`, `HEAD`, `6cb0992`, `6cb0992a176f2ca142e19f64dca8ac12025b035e` |
134
137
  | `path` | Path to skill directory within source | `/skills/my-skill` |
135
138
 
@@ -138,7 +141,11 @@ const skills = await listRepoSkills('vercel-labs', 'skills')
138
141
  ### Resolution Types
139
142
 
140
143
  - **`git`** — Clones the repo, resolves commit hash, copies skill files
141
- - **`file`** — Reads from local filesystem, computes content digest
144
+ - **`link`** — Reads from a local directory and copies the selected skill
145
+ - **`file`** — Extracts a local `tgz` package and copies the selected skill
146
+ - **`npm`** — Resolves a package from the configured npm registry, locks the tarball URL/version/integrity, and installs from the downloaded tarball
147
+
148
+ `npm:` reads `registry` and scoped `@scope:registry` values from `.npmrc`. Matching `:_authToken`, `:_auth`, or `username` + `:_password` entries are also used for private registry requests.
142
149
 
143
150
  ## Architecture
144
151
 
package/dist/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import { cac } from "cac";
2
2
  import picocolors from "picocolors";
3
- import { access, cp as promises_cp, lstat, mkdir, mkdtemp, readFile, readdir, rm as promises_rm, stat as promises_stat, symlink, writeFile } from "node:fs/promises";
4
- import node_path, { join } from "node:path";
3
+ import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat as promises_stat, symlink, writeFile } from "node:fs/promises";
4
+ import node_path, { basename, join } from "node:path";
5
5
  import yaml from "yaml";
6
6
  import { execFile } from "node:child_process";
7
- import { tmpdir } from "node:os";
7
+ import { homedir, tmpdir } from "node:os";
8
8
  import { promisify } from "node:util";
9
9
  import { createHash } from "node:crypto";
10
+ import semver from "semver";
11
+ import { createReadStream } from "node:fs";
12
+ import { x } from "tar";
10
13
  import * as __rspack_external__clack_prompts_3cae1695 from "@clack/prompts";
11
14
  var package_namespaceObject = {
12
- rE: "0.3.0"
15
+ rE: "0.4.0"
13
16
  };
14
17
  const UNIVERSAL_AGENT_NAMES = [
15
18
  'Amp',
@@ -368,7 +371,9 @@ function formatErrorForDisplay(error) {
368
371
  output += `\nExpected formats:`;
369
372
  output += `\n - owner/repo (GitHub shorthand)`;
370
373
  output += `\n - https://github.com/owner/repo.git`;
371
- output += `\n - file:./path/to/skill`;
374
+ output += `\n - link:./path/to/skill-dir`;
375
+ output += `\n - file:./path/to/skill-package.tgz#path:/skills/my-skill`;
376
+ output += `\n - npm:@scope/skill-package#path:/skills/my-skill`;
372
377
  }
373
378
  } else if (error instanceof ManifestError) {
374
379
  if (error.code === codes_ErrorCode.LOCKFILE_OUTDATED) {
@@ -449,6 +454,213 @@ async function readSkillsManifest(rootDir) {
449
454
  });
450
455
  }
451
456
  }
457
+ const resolvedNpmPackageCache = new Map();
458
+ function normalizeRegistryUrl(url) {
459
+ return url.endsWith('/') ? url : `${url}/`;
460
+ }
461
+ function interpolateEnv(value) {
462
+ return value.replace(/\$\{([^}]+)\}/g, (_match, key)=>process.env[key] ?? '');
463
+ }
464
+ async function readNpmRc(filePath) {
465
+ try {
466
+ const content = await readFile(filePath, 'utf8');
467
+ const entries = new Map();
468
+ for (const rawLine of content.split(/\r?\n/)){
469
+ const line = rawLine.trim();
470
+ if (!line || line.startsWith('#') || line.startsWith(';')) continue;
471
+ const separator = line.indexOf('=');
472
+ if (!(separator < 0)) entries.set(line.slice(0, separator).trim(), interpolateEnv(line.slice(separator + 1).trim()));
473
+ }
474
+ return entries;
475
+ } catch {
476
+ return new Map();
477
+ }
478
+ }
479
+ function getCandidateDirs(cwd) {
480
+ const resolvedCwd = node_path.resolve(cwd);
481
+ if ('win32' === process.platform) {
482
+ const parsed = node_path.parse(resolvedCwd);
483
+ const relative = resolvedCwd.slice(parsed.root.length);
484
+ const parts = relative.split(node_path.sep).filter(Boolean);
485
+ return [
486
+ parsed.root,
487
+ ...parts.map((_part, index)=>node_path.join(parsed.root, ...parts.slice(0, index + 1)))
488
+ ];
489
+ }
490
+ const parts = resolvedCwd.split(node_path.sep).filter(Boolean);
491
+ return [
492
+ '/',
493
+ ...parts.map((_part, index)=>node_path.join('/', ...parts.slice(0, index + 1)))
494
+ ];
495
+ }
496
+ function buildRegistryAuthEntries(settings) {
497
+ const registryAuthConfigs = new Map();
498
+ for (const [key, value] of settings){
499
+ const match = key.match(/^(\/\/.+\/):(_authToken|_auth|username|_password)$/);
500
+ if (!match) continue;
501
+ const [, prefix, field] = match;
502
+ const config = registryAuthConfigs.get(prefix) ?? {};
503
+ registryAuthConfigs.set(prefix, {
504
+ ...config,
505
+ ['_authToken' === field ? 'authToken' : '_auth' === field ? 'auth' : 'username' === field ? 'username' : 'password']: value
506
+ });
507
+ }
508
+ return [
509
+ ...registryAuthConfigs.entries()
510
+ ].map(([prefix, config])=>{
511
+ if (config.authToken) return {
512
+ prefix,
513
+ authorization: `Bearer ${config.authToken}`
514
+ };
515
+ if (config.auth) return {
516
+ prefix,
517
+ authorization: `Basic ${config.auth}`
518
+ };
519
+ if (config.username && config.password) {
520
+ const decodedPassword = Buffer.from(config.password, 'base64').toString('utf8');
521
+ return {
522
+ prefix,
523
+ authorization: `Basic ${Buffer.from(`${config.username}:${decodedPassword}`).toString('base64')}`
524
+ };
525
+ }
526
+ return null;
527
+ }).filter((entry)=>null !== entry).sort((a, b)=>b.prefix.length - a.prefix.length);
528
+ }
529
+ async function loadNpmConfig(cwd) {
530
+ const configs = new Map();
531
+ for (const [key, value] of (await readNpmRc(node_path.join(homedir(), '.npmrc'))))configs.set(key, value);
532
+ for (const candidateDir of getCandidateDirs(cwd))for (const [key, value] of (await readNpmRc(node_path.join(candidateDir, '.npmrc'))))configs.set(key, value);
533
+ return {
534
+ settings: configs,
535
+ authEntries: buildRegistryAuthEntries(configs)
536
+ };
537
+ }
538
+ function resolveRegistryConfig(config, packageName) {
539
+ const scopeMatch = packageName.match(/^(@[^/]+)\//);
540
+ if (scopeMatch) {
541
+ const scopeRegistry = config.settings.get(`${scopeMatch[1]}:registry`);
542
+ if (scopeRegistry) return normalizeRegistryUrl(scopeRegistry);
543
+ }
544
+ return normalizeRegistryUrl(config.settings.get('registry') ?? 'https://registry.npmjs.org/');
545
+ }
546
+ function resolveAuthorizationHeader(config, requestUrl) {
547
+ const url = new URL(requestUrl);
548
+ const requestKey = `//${url.host}${url.pathname}`;
549
+ const matched = config.authEntries.find((entry)=>requestKey.startsWith(entry.prefix));
550
+ return matched?.authorization;
551
+ }
552
+ function createRequestHeaders(config, requestUrl) {
553
+ const authorization = resolveAuthorizationHeader(config, requestUrl);
554
+ if (!authorization) return;
555
+ return {
556
+ authorization
557
+ };
558
+ }
559
+ function parseRegistryPackageSpecifier(specifier) {
560
+ const scopedMatch = specifier.match(/^(@[^/]+\/[^@]+)(?:@(.+))?$/);
561
+ if (scopedMatch) return {
562
+ packageName: scopedMatch[1],
563
+ requestedVersion: scopedMatch[2] ?? null
564
+ };
565
+ const unscopedMatch = specifier.match(/^([^@/:][^@]*?)(?:@(.+))?$/);
566
+ if (unscopedMatch) return {
567
+ packageName: unscopedMatch[1],
568
+ requestedVersion: unscopedMatch[2] ?? null
569
+ };
570
+ throw new Error(`Unsupported npm specifier: ${specifier}`);
571
+ }
572
+ function resolveVersionFromMetadata(metadata, requestedVersion) {
573
+ const versions = metadata.versions ?? {};
574
+ const versionKeys = Object.keys(versions);
575
+ const requested = requestedVersion ?? 'latest';
576
+ const taggedVersion = metadata['dist-tags']?.[requested];
577
+ if (taggedVersion && versions[taggedVersion]) return taggedVersion;
578
+ if (semver.valid(requested) && versions[requested]) return requested;
579
+ const matched = semver.maxSatisfying(versionKeys, requested);
580
+ if (matched) return matched;
581
+ throw new Error(`Unable to resolve npm version "${requested}"`);
582
+ }
583
+ async function resolveNpmPackageUncached(cwd, specifier) {
584
+ const config = await loadNpmConfig(cwd);
585
+ const { packageName, requestedVersion } = parseRegistryPackageSpecifier(specifier);
586
+ const registry = resolveRegistryConfig(config, packageName);
587
+ const metadataUrl = new URL(encodeURIComponent(packageName), registry);
588
+ const response = await fetch(metadataUrl, {
589
+ headers: createRequestHeaders(config, metadataUrl.toString())
590
+ });
591
+ if (!response.ok) throw new Error(`Failed to fetch npm metadata for ${packageName}: ${response.status}`);
592
+ const metadata = await response.json();
593
+ const version = resolveVersionFromMetadata(metadata, requestedVersion);
594
+ const manifest = metadata.versions?.[version];
595
+ const tarballUrl = manifest?.dist?.tarball;
596
+ if (!manifest?.name || !manifest.version || !tarballUrl) throw new Error(`Invalid npm metadata for ${packageName}@${version}`);
597
+ return {
598
+ name: manifest.name,
599
+ version: manifest.version,
600
+ tarballUrl,
601
+ integrity: manifest.dist?.integrity,
602
+ registry
603
+ };
604
+ }
605
+ async function resolveNpmPackage(cwd, specifier) {
606
+ const cacheKey = `${node_path.resolve(cwd)}\0${specifier}`;
607
+ const cached = resolvedNpmPackageCache.get(cacheKey);
608
+ if (cached) return cached;
609
+ const pending = resolveNpmPackageUncached(cwd, specifier);
610
+ resolvedNpmPackageCache.set(cacheKey, pending);
611
+ try {
612
+ return await pending;
613
+ } catch (error) {
614
+ resolvedNpmPackageCache.delete(cacheKey);
615
+ throw error;
616
+ }
617
+ }
618
+ function verifyIntegrity(buffer, integrity) {
619
+ for (const entry of integrity.split(/\s+/).filter(Boolean)){
620
+ const separatorIndex = entry.indexOf('-');
621
+ if (separatorIndex <= 0) continue;
622
+ const algorithm = entry.slice(0, separatorIndex);
623
+ const expectedDigest = entry.slice(separatorIndex + 1);
624
+ try {
625
+ const actualDigest = createHash(algorithm).update(buffer).digest('base64');
626
+ if (actualDigest === expectedDigest) return true;
627
+ } catch {}
628
+ }
629
+ return false;
630
+ }
631
+ async function downloadNpmPackageTarball(cwd, tarballUrl, expectedIntegrity) {
632
+ const downloadRoot = await mkdtemp(node_path.join(tmpdir(), 'skills-pm-npm-download-'));
633
+ try {
634
+ const config = await loadNpmConfig(cwd);
635
+ const response = await fetch(tarballUrl, {
636
+ headers: createRequestHeaders(config, tarballUrl)
637
+ });
638
+ if (!response.ok) throw new Error(`Failed to download npm tarball: ${response.status}`);
639
+ const tarballBuffer = Buffer.from(await response.arrayBuffer());
640
+ if (expectedIntegrity && !verifyIntegrity(tarballBuffer, expectedIntegrity)) throw new Error(`Integrity check failed for npm tarball ${tarballUrl}`);
641
+ const tarballPath = node_path.join(downloadRoot, node_path.basename(new URL(tarballUrl).pathname) || 'package.tgz');
642
+ await writeFile(tarballPath, tarballBuffer);
643
+ return tarballPath;
644
+ } catch (error) {
645
+ await rm(downloadRoot, {
646
+ recursive: true,
647
+ force: true
648
+ }).catch(()=>{});
649
+ throw new Error(`Failed to download npm tarball ${tarballUrl}: ${error.message}`, {
650
+ cause: error
651
+ });
652
+ }
653
+ }
654
+ async function cleanupPackedNpmPackage(tarballPath) {
655
+ await rm(node_path.dirname(tarballPath), {
656
+ recursive: true,
657
+ force: true
658
+ }).catch(()=>{});
659
+ }
660
+ function normalizeLinkSource(sourcePart) {
661
+ const linkPath = sourcePart.slice(5).replace(/\\/g, '/').replace(/\/+$/, '');
662
+ return `link:${linkPath}`;
663
+ }
452
664
  function parseSpecifier(specifier) {
453
665
  const firstHashIndex = specifier.indexOf('#');
454
666
  const secondHashIndex = firstHashIndex >= 0 ? specifier.indexOf('#', firstHashIndex + 1) : -1;
@@ -487,6 +699,11 @@ function parseSpecifier(specifier) {
487
699
  };
488
700
  }
489
701
  function normalizeSpecifier(specifier) {
702
+ if (specifier.startsWith('link:') && specifier.includes('#')) throw new ParseError({
703
+ code: codes_ErrorCode.INVALID_SPECIFIER,
704
+ message: 'Invalid link specifier: link: must point directly to a skill directory',
705
+ content: specifier
706
+ });
490
707
  let parsed;
491
708
  try {
492
709
  parsed = parseSpecifier(specifier);
@@ -499,7 +716,20 @@ function normalizeSpecifier(specifier) {
499
716
  cause: error
500
717
  });
501
718
  }
502
- const type = parsed.sourcePart.startsWith('file:') ? 'file' : parsed.sourcePart.startsWith('npm:') ? 'npm' : 'git';
719
+ const type = parsed.sourcePart.startsWith('link:') ? 'link' : parsed.sourcePart.startsWith('file:') ? 'file' : parsed.sourcePart.startsWith('npm:') ? 'npm' : 'git';
720
+ if ('link' === type) {
721
+ const linkSource = normalizeLinkSource(parsed.sourcePart);
722
+ const linkPath = linkSource.slice(5);
723
+ const skillName = node_path.posix.basename(linkPath);
724
+ return {
725
+ type,
726
+ source: linkSource,
727
+ ref: null,
728
+ path: '/',
729
+ normalized: linkSource,
730
+ skillName
731
+ };
732
+ }
503
733
  const skillPath = parsed.path || '/';
504
734
  const skillName = node_path.posix.basename(skillPath);
505
735
  const normalized = parsed.ref ? `${parsed.sourcePart}#${parsed.ref}&path:${skillPath}` : parsed.path ? `${parsed.sourcePart}#path:${skillPath}` : parsed.sourcePart;
@@ -515,7 +745,50 @@ function normalizeSpecifier(specifier) {
515
745
  function sha256(content) {
516
746
  return `sha256-${createHash('sha256').update(content).digest('hex')}`;
517
747
  }
748
+ function toPortablePath(filePath) {
749
+ return '/' === node_path.sep ? filePath : filePath.split(node_path.sep).join('/');
750
+ }
751
+ async function hashDirectoryEntry(hash, rootDir, currentDir) {
752
+ const entries = await readdir(currentDir, {
753
+ withFileTypes: true
754
+ });
755
+ entries.sort((a, b)=>a.name.localeCompare(b.name));
756
+ for (const entry of entries){
757
+ const absolutePath = node_path.join(currentDir, entry.name);
758
+ const relativePath = toPortablePath(node_path.relative(rootDir, absolutePath));
759
+ const stats = await lstat(absolutePath);
760
+ if (stats.isSymbolicLink()) {
761
+ hash.update(`symlink:${relativePath}\n`);
762
+ hash.update(await readlink(absolutePath));
763
+ hash.update('\n');
764
+ continue;
765
+ }
766
+ if (stats.isDirectory()) {
767
+ hash.update(`dir:${relativePath}\n`);
768
+ await hashDirectoryEntry(hash, rootDir, absolutePath);
769
+ continue;
770
+ }
771
+ hash.update(`file:${relativePath}\n`);
772
+ hash.update(await readFile(absolutePath));
773
+ hash.update('\n');
774
+ }
775
+ }
776
+ async function sha256Directory(rootDir) {
777
+ const hash = createHash('sha256');
778
+ await hashDirectoryEntry(hash, rootDir, rootDir);
779
+ return `sha256-${hash.digest('hex')}`;
780
+ }
781
+ async function sha256File(filePath, suffix = '') {
782
+ const hash = createHash('sha256');
783
+ for await (const chunk of createReadStream(filePath))hash.update(chunk);
784
+ if (suffix) hash.update(suffix);
785
+ return `sha256-${hash.digest('hex')}`;
786
+ }
518
787
  const execFileAsync = promisify(execFile);
788
+ function toPortableRelativePath(from, to) {
789
+ const relativePath = node_path.relative(from, to) || '.';
790
+ return '/' === node_path.sep ? relativePath : relativePath.split(node_path.sep).join('/');
791
+ }
519
792
  async function resolveGitCommitByLsRemote(url, target) {
520
793
  try {
521
794
  const { stdout } = await execFileAsync('git', [
@@ -553,7 +826,7 @@ async function resolveGitCommitByClone(url, target) {
553
826
  } catch {
554
827
  return null;
555
828
  } finally{
556
- await promises_rm(checkoutRoot, {
829
+ await rm(checkoutRoot, {
557
830
  recursive: true,
558
831
  force: true
559
832
  }).catch(()=>{});
@@ -587,17 +860,32 @@ async function resolveLockEntry(cwd, specifier, skillName) {
587
860
  });
588
861
  }
589
862
  const finalSkillName = skillName || normalized.skillName;
590
- if ('file' === normalized.type) {
863
+ if ('link' === normalized.type) {
591
864
  const sourceRoot = node_path.resolve(cwd, normalized.source.slice(5));
865
+ return {
866
+ skillName: finalSkillName,
867
+ entry: {
868
+ specifier: normalized.normalized,
869
+ resolution: {
870
+ type: 'link',
871
+ path: toPortableRelativePath(cwd, sourceRoot)
872
+ },
873
+ digest: await sha256Directory(sourceRoot)
874
+ }
875
+ };
876
+ }
877
+ if ('file' === normalized.type) {
878
+ const tarballPath = node_path.resolve(cwd, normalized.source.slice(5));
592
879
  return {
593
880
  skillName: finalSkillName,
594
881
  entry: {
595
882
  specifier: normalized.normalized,
596
883
  resolution: {
597
884
  type: 'file',
598
- path: node_path.relative(cwd, sourceRoot) || '.'
885
+ tarball: toPortableRelativePath(cwd, tarballPath),
886
+ path: normalized.path
599
887
  },
600
- digest: sha256(`${sourceRoot}:${normalized.path}`)
888
+ digest: await sha256File(tarballPath, `:${normalized.path}`)
601
889
  }
602
890
  };
603
891
  }
@@ -617,15 +905,46 @@ async function resolveLockEntry(cwd, specifier, skillName) {
617
905
  }
618
906
  };
619
907
  }
908
+ if ('npm' === normalized.type) {
909
+ const packageSpecifier = normalized.source.slice(4);
910
+ const resolved = await resolveNpmPackage(cwd, packageSpecifier);
911
+ return {
912
+ skillName: finalSkillName,
913
+ entry: {
914
+ specifier: normalized.normalized,
915
+ resolution: {
916
+ type: 'npm',
917
+ packageName: resolved.name,
918
+ version: resolved.version,
919
+ path: normalized.path,
920
+ tarball: resolved.tarballUrl,
921
+ integrity: resolved.integrity,
922
+ registry: resolved.registry
923
+ },
924
+ digest: sha256([
925
+ resolved.name,
926
+ resolved.version,
927
+ resolved.tarballUrl,
928
+ resolved.integrity ?? '',
929
+ resolved.registry ?? '',
930
+ normalized.path
931
+ ].join(':'))
932
+ }
933
+ };
934
+ }
620
935
  throw new ParseError({
621
936
  code: codes_ErrorCode.INVALID_SPECIFIER,
622
937
  message: `Unsupported specifier type in 0.1.0 core flow: ${normalized.type}`,
623
938
  content: specifier
624
939
  });
625
940
  }
626
- async function syncSkillsLock(cwd, manifest, _existingLock) {
941
+ async function syncSkillsLock(cwd, manifest, _existingLock, options) {
627
942
  const entries = await Promise.all(Object.entries(manifest.skills).map(async ([skillName, specifier])=>{
628
943
  const { skillName: resolvedName, entry } = await resolveLockEntry(cwd, specifier, skillName);
944
+ options?.onProgress?.({
945
+ type: 'resolved',
946
+ skillName: resolvedName
947
+ });
629
948
  return [
630
949
  resolvedName,
631
950
  entry
@@ -700,7 +1019,7 @@ async function parseSkillDir(dir, relativePath) {
700
1019
  try {
701
1020
  const content = await readFile(join(dir, 'SKILL.md'), 'utf8');
702
1021
  const meta = parseSkillFrontmatter(content);
703
- const dirName = dir.split('/').pop() ?? '';
1022
+ const dirName = basename(dir);
704
1023
  return {
705
1024
  name: meta.name || dirName,
706
1025
  description: meta.description,
@@ -760,14 +1079,14 @@ async function cloneAndDiscover(gitUrl, ref) {
760
1079
  return {
761
1080
  skills,
762
1081
  cleanup: async ()=>{
763
- await promises_rm(tempDir, {
1082
+ await rm(tempDir, {
764
1083
  recursive: true,
765
1084
  force: true
766
1085
  }).catch(()=>{});
767
1086
  }
768
1087
  };
769
1088
  } catch (error) {
770
- await promises_rm(tempDir, {
1089
+ await rm(tempDir, {
771
1090
  recursive: true,
772
1091
  force: true
773
1092
  }).catch(()=>{});
@@ -819,10 +1138,11 @@ function parseGitHubUrl(input) {
819
1138
  }
820
1139
  function parseForComparison(specifier) {
821
1140
  const parsed = parseSpecifier(specifier);
1141
+ const isLink = parsed.sourcePart.startsWith('link:');
822
1142
  return {
823
- sourcePart: parsed.sourcePart,
824
- ref: parsed.ref,
825
- path: parsed.path || '/'
1143
+ sourcePart: isLink ? normalizeLinkSource(parsed.sourcePart) : parsed.sourcePart,
1144
+ ref: isLink ? null : parsed.ref,
1145
+ path: isLink ? '/' : parsed.path || '/'
826
1146
  };
827
1147
  }
828
1148
  function isSpecifierCompatible(manifestSpecifier, lockSpecifier) {
@@ -862,8 +1182,17 @@ async function ensureDir(dirPath) {
862
1182
  recursive: true
863
1183
  });
864
1184
  }
1185
+ async function replaceDir(from, to) {
1186
+ await rm(to, {
1187
+ recursive: true,
1188
+ force: true
1189
+ });
1190
+ await cp(from, to, {
1191
+ recursive: true
1192
+ });
1193
+ }
865
1194
  async function replaceSymlink(target, linkPath) {
866
- await promises_rm(linkPath, {
1195
+ await rm(linkPath, {
867
1196
  recursive: true,
868
1197
  force: true
869
1198
  });
@@ -872,18 +1201,19 @@ async function replaceSymlink(target, linkPath) {
872
1201
  async function writeJson(filePath, value) {
873
1202
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
874
1203
  }
875
- async function readInstallState(rootDir) {
876
- const filePath = node_path.join(rootDir, '.agents/skills/.skills-pm-install-state.json');
1204
+ const INSTALL_STATE_FILE = '.skills-pm-install-state.json';
1205
+ async function readInstallState(rootDir, installDir) {
1206
+ const filePath = node_path.join(rootDir, installDir, INSTALL_STATE_FILE);
877
1207
  try {
878
1208
  return JSON.parse(await readFile(filePath, 'utf8'));
879
1209
  } catch {
880
1210
  return null;
881
1211
  }
882
1212
  }
883
- async function writeInstallState(rootDir, value) {
884
- const dirPath = node_path.join(rootDir, '.agents/skills');
1213
+ async function writeInstallState(rootDir, installDir, value) {
1214
+ const dirPath = node_path.join(rootDir, installDir);
885
1215
  await ensureDir(dirPath);
886
- const filePath = node_path.join(dirPath, '.skills-pm-install-state.json');
1216
+ const filePath = node_path.join(dirPath, INSTALL_STATE_FILE);
887
1217
  await writeJson(filePath, value);
888
1218
  }
889
1219
  async function linkSkill(rootDir, installDir, linkTarget, skillName) {
@@ -905,10 +1235,7 @@ async function materializeLocalSkill(rootDir, skillName, sourceRoot, sourcePath,
905
1235
  if (!skillDoc) throw new Error(`Invalid skill at ${absoluteSkillPath}: missing SKILL.md`);
906
1236
  const targetDir = node_path.join(rootDir, installDir, skillName);
907
1237
  await ensureDir(node_path.dirname(targetDir));
908
- await promises_cp(absoluteSkillPath, targetDir, {
909
- recursive: true,
910
- force: true
911
- });
1238
+ await replaceDir(absoluteSkillPath, targetDir);
912
1239
  await writeJson(node_path.join(targetDir, '.skills-pm.json'), {
913
1240
  name: skillName,
914
1241
  installedBy: 'skills-package-manager',
@@ -1009,12 +1336,33 @@ async function materializeGitSkill(rootDir, skillName, repoUrl, commit, sourcePa
1009
1336
  await readFile(skillDocPath, 'utf8');
1010
1337
  await materializeLocalSkill(rootDir, skillName, checkoutRoot, sourcePath, installDir);
1011
1338
  } finally{
1012
- await promises_rm(checkoutRoot, {
1339
+ await rm(checkoutRoot, {
1013
1340
  recursive: true,
1014
1341
  force: true
1015
1342
  });
1016
1343
  }
1017
1344
  }
1345
+ async function materializePackedSkill(rootDir, skillName, tarballPath, sourcePath, installDir) {
1346
+ const extractRoot = await mkdtemp(node_path.join(tmpdir(), 'skills-pm-packed-skill-'));
1347
+ try {
1348
+ await mkdir(node_path.join(extractRoot, 'package'), {
1349
+ recursive: true
1350
+ });
1351
+ await x({
1352
+ file: tarballPath,
1353
+ cwd: node_path.join(extractRoot, 'package'),
1354
+ strip: 1,
1355
+ preservePaths: false,
1356
+ strict: true
1357
+ });
1358
+ await materializeLocalSkill(rootDir, skillName, node_path.join(extractRoot, 'package'), sourcePath, installDir);
1359
+ } finally{
1360
+ await rm(extractRoot, {
1361
+ recursive: true,
1362
+ force: true
1363
+ }).catch(()=>{});
1364
+ }
1365
+ }
1018
1366
  async function isManagedSkillDir(dirPath) {
1019
1367
  try {
1020
1368
  const marker = JSON.parse(await readFile(node_path.join(dirPath, '.skills-pm.json'), 'utf8'));
@@ -1033,7 +1381,7 @@ async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillN
1033
1381
  const skillDir = node_path.join(absoluteInstallDir, entry);
1034
1382
  if (await isManagedSkillDir(skillDir)) {
1035
1383
  if (!wanted.has(entry)) {
1036
- await promises_rm(skillDir, {
1384
+ await rm(skillDir, {
1037
1385
  recursive: true,
1038
1386
  force: true
1039
1387
  });
@@ -1041,7 +1389,7 @@ async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillN
1041
1389
  const linkPath = node_path.join(rootDir, linkTarget, entry);
1042
1390
  try {
1043
1391
  const stat = await lstat(linkPath);
1044
- if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) await promises_rm(linkPath, {
1392
+ if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) await rm(linkPath, {
1045
1393
  recursive: true,
1046
1394
  force: true
1047
1395
  });
@@ -1055,50 +1403,98 @@ async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillN
1055
1403
  const installStageHooks = {
1056
1404
  beforeFetch: async (_rootDir, _manifest, _lockfile)=>{}
1057
1405
  };
1058
- function extractSkillPath(specifier, skillName) {
1059
- const marker = '#path:';
1060
- const index = specifier.indexOf(marker);
1061
- if (index >= 0) return specifier.slice(index + marker.length);
1062
- return `/${skillName}`;
1406
+ async function areManagedSkillsInstalled(rootDir, installDir, skillNames) {
1407
+ for (const skillName of skillNames)try {
1408
+ await access(node_path.join(rootDir, installDir, skillName, 'SKILL.md'));
1409
+ } catch {
1410
+ return false;
1411
+ }
1412
+ return true;
1063
1413
  }
1064
- async function fetchSkillsFromLock(rootDir, manifest, lockfile) {
1414
+ async function fetchSkillsFromLock(rootDir, manifest, lockfile, options) {
1065
1415
  await installStageHooks.beforeFetch(rootDir, manifest, lockfile);
1416
+ const installDir = manifest.installDir ?? '.agents/skills';
1417
+ const linkTargets = manifest.linkTargets ?? [];
1418
+ await pruneManagedSkills(rootDir, installDir, linkTargets, Object.keys(lockfile.skills));
1066
1419
  const lockDigest = sha256(JSON.stringify(lockfile));
1067
- const state = await readInstallState(rootDir);
1068
- if (state?.lockDigest === lockDigest) return {
1420
+ const state = await readInstallState(rootDir, installDir);
1421
+ if (state?.lockDigest === lockDigest && await areManagedSkillsInstalled(rootDir, installDir, Object.keys(lockfile.skills))) return {
1069
1422
  status: 'skipped',
1070
1423
  reason: 'up-to-date'
1071
1424
  };
1072
- const installDir = manifest.installDir ?? '.agents/skills';
1073
- const linkTargets = manifest.linkTargets ?? [];
1074
- await pruneManagedSkills(rootDir, installDir, linkTargets, Object.keys(lockfile.skills));
1075
- for (const [skillName, entry] of Object.entries(lockfile.skills)){
1076
- if ('file' === entry.resolution.type) {
1077
- await materializeLocalSkill(rootDir, skillName, node_path.resolve(rootDir, entry.resolution.path), extractSkillPath(entry.specifier, skillName), installDir);
1078
- continue;
1079
- }
1080
- if ('git' === entry.resolution.type) {
1081
- await materializeGitSkill(rootDir, skillName, entry.resolution.url, entry.resolution.commit, entry.resolution.path, installDir);
1082
- continue;
1425
+ const downloadedTarballs = new Map();
1426
+ try {
1427
+ for (const [skillName, entry] of Object.entries(lockfile.skills)){
1428
+ if ('link' === entry.resolution.type) {
1429
+ await materializeLocalSkill(rootDir, skillName, node_path.resolve(rootDir, entry.resolution.path), '/', installDir);
1430
+ options?.onProgress?.({
1431
+ type: 'added',
1432
+ skillName
1433
+ });
1434
+ continue;
1435
+ }
1436
+ if ('file' === entry.resolution.type) {
1437
+ await materializePackedSkill(rootDir, skillName, node_path.resolve(rootDir, entry.resolution.tarball), entry.resolution.path, installDir);
1438
+ options?.onProgress?.({
1439
+ type: 'added',
1440
+ skillName
1441
+ });
1442
+ continue;
1443
+ }
1444
+ if ('git' === entry.resolution.type) {
1445
+ await materializeGitSkill(rootDir, skillName, entry.resolution.url, entry.resolution.commit, entry.resolution.path, installDir);
1446
+ options?.onProgress?.({
1447
+ type: 'added',
1448
+ skillName
1449
+ });
1450
+ continue;
1451
+ }
1452
+ if ('npm' === entry.resolution.type) {
1453
+ const cacheKey = `${entry.resolution.tarball}\0${entry.resolution.integrity ?? ''}`;
1454
+ let tarballPathPromise = downloadedTarballs.get(cacheKey);
1455
+ if (!tarballPathPromise) {
1456
+ tarballPathPromise = downloadNpmPackageTarball(rootDir, entry.resolution.tarball, entry.resolution.integrity);
1457
+ downloadedTarballs.set(cacheKey, tarballPathPromise);
1458
+ }
1459
+ const tarballPath = await tarballPathPromise;
1460
+ await materializePackedSkill(rootDir, skillName, tarballPath, entry.resolution.path, installDir);
1461
+ options?.onProgress?.({
1462
+ type: 'added',
1463
+ skillName
1464
+ });
1465
+ continue;
1466
+ }
1467
+ throw new Error(`Unsupported resolution type in 0.1.0 core flow: ${entry.resolution.type}`);
1083
1468
  }
1084
- throw new Error(`Unsupported resolution type in 0.1.0 core flow: ${entry.resolution.type}`);
1469
+ await writeInstallState(rootDir, installDir, {
1470
+ lockDigest,
1471
+ installDir,
1472
+ linkTargets,
1473
+ installerVersion: '0.1.0',
1474
+ installedAt: new Date().toISOString()
1475
+ });
1476
+ } finally{
1477
+ const settledTarballs = await Promise.allSettled(downloadedTarballs.values());
1478
+ const downloadedPaths = new Set(settledTarballs.filter((result)=>'fulfilled' === result.status).map((result)=>result.value));
1479
+ await Promise.all([
1480
+ ...downloadedPaths
1481
+ ].map((tarballPath)=>cleanupPackedNpmPackage(tarballPath)));
1085
1482
  }
1086
- await writeInstallState(rootDir, {
1087
- lockDigest,
1088
- installDir,
1089
- linkTargets,
1090
- installerVersion: '0.1.0',
1091
- installedAt: new Date().toISOString()
1092
- });
1093
1483
  return {
1094
1484
  status: 'fetched',
1095
1485
  fetched: Object.keys(lockfile.skills)
1096
1486
  };
1097
1487
  }
1098
- async function linkSkillsFromLock(rootDir, manifest, lockfile) {
1488
+ async function linkSkillsFromLock(rootDir, manifest, lockfile, options) {
1099
1489
  const installDir = manifest.installDir ?? '.agents/skills';
1100
1490
  const linkTargets = manifest.linkTargets ?? [];
1101
- for (const skillName of Object.keys(lockfile.skills))for (const linkTarget of linkTargets)await linkSkill(rootDir, installDir, linkTarget, skillName);
1491
+ for (const skillName of Object.keys(lockfile.skills)){
1492
+ for (const linkTarget of linkTargets)await linkSkill(rootDir, installDir, linkTarget, skillName);
1493
+ options?.onProgress?.({
1494
+ type: 'installed',
1495
+ skillName
1496
+ });
1497
+ }
1102
1498
  return {
1103
1499
  status: 'linked',
1104
1500
  linked: Object.keys(lockfile.skills)
@@ -1116,9 +1512,19 @@ async function installSkills(rootDir, options) {
1116
1512
  if (!currentLock) throw new Error('Lockfile is required in frozen mode but none was found');
1117
1513
  if (!isLockInSync(manifest, currentLock)) throw new Error('Lockfile is out of sync with manifest. Run install without --frozen-lockfile to update.');
1118
1514
  lockfile = currentLock;
1119
- } else lockfile = await syncSkillsLock(rootDir, manifest, currentLock);
1120
- await fetchSkillsFromLock(rootDir, manifest, lockfile);
1121
- await linkSkillsFromLock(rootDir, manifest, lockfile);
1515
+ for (const skillName of Object.keys(lockfile.skills))options?.onProgress?.({
1516
+ type: 'resolved',
1517
+ skillName
1518
+ });
1519
+ } else lockfile = await syncSkillsLock(rootDir, manifest, currentLock, {
1520
+ onProgress: options?.onProgress
1521
+ });
1522
+ await fetchSkillsFromLock(rootDir, manifest, lockfile, {
1523
+ onProgress: options?.onProgress
1524
+ });
1525
+ await linkSkillsFromLock(rootDir, manifest, lockfile, {
1526
+ onProgress: options?.onProgress
1527
+ });
1122
1528
  if (!options?.frozenLockfile) await writeSkillsLock(rootDir, lockfile);
1123
1529
  return {
1124
1530
  status: 'installed',
@@ -1180,7 +1586,9 @@ async function addCommand(options) {
1180
1586
  const skillPath = found?.path ?? `/${skill}`;
1181
1587
  const gitSpecifier = buildGitHubSpecifier(owner, repo, skillPath);
1182
1588
  const result = await addSingleSkill(cwd, gitSpecifier);
1589
+ spinner.start('Installing skills...');
1183
1590
  await installSkills(cwd);
1591
+ spinner.stop('Installed skills');
1184
1592
  __rspack_external__clack_prompts_3cae1695.outro(`Added ${picocolors.cyan(result.skillName)}`);
1185
1593
  return result;
1186
1594
  }
@@ -1204,12 +1612,17 @@ async function addCommand(options) {
1204
1612
  results.push(result);
1205
1613
  __rspack_external__clack_prompts_3cae1695.log.success(`Added ${picocolors.cyan(result.skillName)}`);
1206
1614
  }
1615
+ spinner.start('Installing skills...');
1207
1616
  await installSkills(cwd);
1617
+ spinner.stop('Installed skills');
1208
1618
  __rspack_external__clack_prompts_3cae1695.outro('Done');
1209
1619
  return 1 === results.length ? results[0] : results;
1210
1620
  }
1211
1621
  const result = await addSingleSkill(cwd, specifier);
1622
+ const spinner = __rspack_external__clack_prompts_3cae1695.spinner();
1623
+ spinner.start('Installing skills...');
1212
1624
  await installSkills(cwd);
1625
+ spinner.stop('Installed skills');
1213
1626
  return result;
1214
1627
  }
1215
1628
  async function assertManifestMissing(cwd) {
@@ -1249,6 +1662,117 @@ async function initCommand(options, promptInit = promptInitManifestOptions) {
1249
1662
  await writeSkillsManifest(options.cwd, manifest);
1250
1663
  return manifest;
1251
1664
  }
1665
+ const phaseLabelMap = {
1666
+ resolving: 'Resolving',
1667
+ fetching: 'Fetching',
1668
+ linking: 'Linking',
1669
+ finalizing: 'Finalizing',
1670
+ done: 'Done'
1671
+ };
1672
+ function clampCount(value, total) {
1673
+ if (value < 0) return 0;
1674
+ if (value > total) return total;
1675
+ return value;
1676
+ }
1677
+ function calculatePercent(snapshot) {
1678
+ if ('done' === snapshot.phase) return 100;
1679
+ if (0 === snapshot.total) return 0;
1680
+ const maxSteps = 3 * snapshot.total;
1681
+ const completed = snapshot.resolved + snapshot.added + snapshot.installed;
1682
+ return Math.floor(completed / maxSteps * 100);
1683
+ }
1684
+ function formatSummary(snapshot) {
1685
+ const total = snapshot.total;
1686
+ return `resolved ${snapshot.resolved}/${total}, added ${snapshot.added}/${total}, installed ${snapshot.installed}/${total}`;
1687
+ }
1688
+ function formatTTYLine(snapshot) {
1689
+ const percent = calculatePercent(snapshot);
1690
+ const progress = Math.round(percent / 100 * 20);
1691
+ const filled = '='.repeat(progress);
1692
+ const empty = '-'.repeat(Math.max(0, 20 - progress));
1693
+ const phase = phaseLabelMap[snapshot.phase];
1694
+ const summary = formatSummary(snapshot);
1695
+ const skill = snapshot.currentSkill ? `, skill: ${snapshot.currentSkill}` : '';
1696
+ return `[${filled}${empty}] ${percent}% ${phase} ${summary}${skill}`;
1697
+ }
1698
+ function createInstallProgressReporter(options = {}) {
1699
+ const write = options.write ?? ((text)=>process.stderr.write(text));
1700
+ const info = options.info ?? ((text)=>console.info(text));
1701
+ const useTTY = options.isTTY ?? true === process.stderr.isTTY;
1702
+ const snapshot = {
1703
+ total: 0,
1704
+ resolved: 0,
1705
+ added: 0,
1706
+ installed: 0,
1707
+ phase: 'resolving'
1708
+ };
1709
+ let renderedTTY = false;
1710
+ let lastLineLength = 0;
1711
+ function renderTTY() {
1712
+ const line = formatTTYLine(snapshot);
1713
+ const clearPadding = lastLineLength > line.length ? ' '.repeat(lastLineLength - line.length) : '';
1714
+ write(`\r${line}${clearPadding}`);
1715
+ lastLineLength = line.length;
1716
+ renderedTTY = true;
1717
+ }
1718
+ function logStage(phase) {
1719
+ info(`spm install: ${phaseLabelMap[phase].toLowerCase()}...`);
1720
+ }
1721
+ function render() {
1722
+ if (useTTY) return void renderTTY();
1723
+ }
1724
+ return {
1725
+ start (total) {
1726
+ snapshot.total = Math.max(0, total);
1727
+ snapshot.resolved = 0;
1728
+ snapshot.added = 0;
1729
+ snapshot.installed = 0;
1730
+ snapshot.phase = 'resolving';
1731
+ snapshot.currentSkill = void 0;
1732
+ if (useTTY) renderTTY();
1733
+ else {
1734
+ const noun = 1 === snapshot.total ? 'skill' : 'skills';
1735
+ info(`spm install: starting (${snapshot.total} ${noun})`);
1736
+ logStage('resolving');
1737
+ }
1738
+ },
1739
+ setPhase (phase) {
1740
+ snapshot.phase = phase;
1741
+ snapshot.currentSkill = void 0;
1742
+ render();
1743
+ if (!useTTY && 'finalizing' !== phase) logStage(phase);
1744
+ },
1745
+ onProgress (event) {
1746
+ snapshot.currentSkill = event.skillName;
1747
+ switch(event.type){
1748
+ case 'resolved':
1749
+ snapshot.resolved = clampCount(snapshot.resolved + 1, snapshot.total);
1750
+ break;
1751
+ case 'added':
1752
+ snapshot.added = clampCount(snapshot.added + 1, snapshot.total);
1753
+ break;
1754
+ case 'installed':
1755
+ snapshot.installed = clampCount(snapshot.installed + 1, snapshot.total);
1756
+ break;
1757
+ default:
1758
+ }
1759
+ render();
1760
+ },
1761
+ complete () {
1762
+ snapshot.phase = 'done';
1763
+ snapshot.currentSkill = void 0;
1764
+ const summary = formatSummary(snapshot);
1765
+ if (useTTY) {
1766
+ renderTTY();
1767
+ write('\n');
1768
+ }
1769
+ info(`spm install: ${summary}`);
1770
+ },
1771
+ fail () {
1772
+ if (useTTY && renderedTTY) write('\n');
1773
+ }
1774
+ };
1775
+ }
1252
1776
  async function installCommand(options) {
1253
1777
  const manifest = await readSkillsManifest(options.cwd);
1254
1778
  if (!manifest) throw new ManifestError({
@@ -1257,32 +1781,67 @@ async function installCommand(options) {
1257
1781
  message: 'No skills.json found in the current directory. Run "spm init" to create one.'
1258
1782
  });
1259
1783
  const currentLock = await readSkillsLock(options.cwd);
1260
- if (options.frozenLockfile) {
1261
- if (!currentLock) throw new ManifestError({
1262
- code: codes_ErrorCode.LOCKFILE_NOT_FOUND,
1263
- filePath: `${options.cwd}/skills-lock.yaml`,
1264
- message: 'Lockfile is required in frozen mode but none was found. Run "spm install" first.'
1784
+ const totalSkills = Object.keys(manifest.skills).length;
1785
+ const reporter = createInstallProgressReporter();
1786
+ const onProgress = (event)=>reporter.onProgress(event);
1787
+ let started = false;
1788
+ try {
1789
+ if (options.frozenLockfile) {
1790
+ if (!currentLock) throw new ManifestError({
1791
+ code: codes_ErrorCode.LOCKFILE_NOT_FOUND,
1792
+ filePath: `${options.cwd}/skills-lock.yaml`,
1793
+ message: 'Lockfile is required in frozen mode but none was found. Run "spm install" first.'
1794
+ });
1795
+ if (!isLockInSync(manifest, currentLock)) throw new ManifestError({
1796
+ code: codes_ErrorCode.LOCKFILE_OUTDATED,
1797
+ filePath: `${options.cwd}/skills-lock.yaml`,
1798
+ message: 'Lockfile is out of sync with manifest. Run install without --frozen-lockfile to update.'
1799
+ });
1800
+ reporter.start(totalSkills);
1801
+ started = true;
1802
+ for (const skillName of Object.keys(currentLock.skills))onProgress({
1803
+ type: 'resolved',
1804
+ skillName
1805
+ });
1806
+ reporter.setPhase('fetching');
1807
+ await fetchSkillsFromLock(options.cwd, manifest, currentLock, {
1808
+ onProgress
1809
+ });
1810
+ reporter.setPhase('linking');
1811
+ await linkSkillsFromLock(options.cwd, manifest, currentLock, {
1812
+ onProgress
1813
+ });
1814
+ reporter.setPhase('finalizing');
1815
+ reporter.complete();
1816
+ return {
1817
+ status: 'installed',
1818
+ installed: Object.keys(currentLock.skills)
1819
+ };
1820
+ }
1821
+ reporter.start(totalSkills);
1822
+ started = true;
1823
+ const lockfile = await syncSkillsLock(options.cwd, manifest, currentLock, {
1824
+ onProgress
1825
+ });
1826
+ reporter.setPhase('fetching');
1827
+ await fetchSkillsFromLock(options.cwd, manifest, lockfile, {
1828
+ onProgress
1265
1829
  });
1266
- if (!isLockInSync(manifest, currentLock)) throw new ManifestError({
1267
- code: codes_ErrorCode.LOCKFILE_OUTDATED,
1268
- filePath: `${options.cwd}/skills-lock.yaml`,
1269
- message: 'Lockfile is out of sync with manifest. Run install without --frozen-lockfile to update.'
1830
+ reporter.setPhase('linking');
1831
+ await linkSkillsFromLock(options.cwd, manifest, lockfile, {
1832
+ onProgress
1270
1833
  });
1271
- await fetchSkillsFromLock(options.cwd, manifest, currentLock);
1272
- await linkSkillsFromLock(options.cwd, manifest, currentLock);
1834
+ reporter.setPhase('finalizing');
1835
+ await writeSkillsLock(options.cwd, lockfile);
1836
+ reporter.complete();
1273
1837
  return {
1274
1838
  status: 'installed',
1275
- installed: Object.keys(currentLock.skills)
1839
+ installed: Object.keys(lockfile.skills)
1276
1840
  };
1841
+ } catch (error) {
1842
+ if (started) reporter.fail();
1843
+ throw error;
1277
1844
  }
1278
- const lockfile = await syncSkillsLock(options.cwd, manifest, currentLock);
1279
- await fetchSkillsFromLock(options.cwd, manifest, lockfile);
1280
- await linkSkillsFromLock(options.cwd, manifest, lockfile);
1281
- await writeSkillsLock(options.cwd, lockfile);
1282
- return {
1283
- status: 'installed',
1284
- installed: Object.keys(lockfile.skills)
1285
- };
1286
1845
  }
1287
1846
  function createEmptyResult() {
1288
1847
  return {
@@ -1327,17 +1886,26 @@ async function updateCommand(options) {
1327
1886
  candidateLock.linkTargets = manifest.linkTargets ?? [];
1328
1887
  for (const skillName of targetSkills){
1329
1888
  const specifier = manifest.skills[skillName];
1330
- if (specifier.startsWith('file:')) {
1331
- result.skipped.push({
1332
- name: skillName,
1333
- reason: 'file-specifier'
1334
- });
1335
- continue;
1336
- }
1337
1889
  try {
1890
+ const normalized = normalizeSpecifier(specifier);
1891
+ if ('link' === normalized.type) {
1892
+ result.skipped.push({
1893
+ name: skillName,
1894
+ reason: 'link-specifier'
1895
+ });
1896
+ continue;
1897
+ }
1338
1898
  const { entry } = await resolveLockEntry(options.cwd, specifier);
1339
1899
  const previous = currentLock?.skills[skillName];
1340
- if (previous?.resolution.type === 'git' && 'git' === entry.resolution.type && previous.resolution.commit === entry.resolution.commit) {
1900
+ if (previous?.resolution.type === 'git' && 'git' === entry.resolution.type && previous.specifier === entry.specifier && previous.resolution.url === entry.resolution.url && previous.resolution.commit === entry.resolution.commit && previous.resolution.path === entry.resolution.path) {
1901
+ result.unchanged.push(skillName);
1902
+ continue;
1903
+ }
1904
+ if (previous?.resolution.type === 'npm' && 'npm' === entry.resolution.type && previous.specifier === entry.specifier && previous.resolution.packageName === entry.resolution.packageName && previous.resolution.version === entry.resolution.version && previous.resolution.path === entry.resolution.path && previous.resolution.tarball === entry.resolution.tarball && previous.resolution.integrity === entry.resolution.integrity && previous.resolution.registry === entry.resolution.registry) {
1905
+ result.unchanged.push(skillName);
1906
+ continue;
1907
+ }
1908
+ if (previous?.resolution.type === 'file' && 'file' === entry.resolution.type && previous.specifier === entry.specifier && previous.digest === entry.digest) {
1341
1909
  result.unchanged.push(skillName);
1342
1910
  continue;
1343
1911
  }
@@ -1428,4 +1996,4 @@ async function runCli(argv, context = {}) {
1428
1996
  throw error;
1429
1997
  }
1430
1998
  }
1431
- export { FileSystemError, GitError, ManifestError, NetworkError, ParseError, SkillError, SpmError, addCommand, cloneAndDiscover, codes_ErrorCode as ErrorCode, convertNodeError, discoverSkillsInDir, fetchSkillsFromLock, formatErrorForDisplay, getExitCode, initCommand, installCommand, installSkills, installStageHooks, isLockInSync, isSpmError, linkSkillsFromLock, listRepoSkills, normalizeSpecifier, parseGitHubUrl, parseOwnerRepo, parseSpecifier, readSkillsLock, readSkillsManifest, resolveLockEntry, runCli, updateCommand, writeSkillsLock, writeSkillsManifest };
1999
+ export { FileSystemError, GitError, ManifestError, NetworkError, ParseError, SkillError, SpmError, addCommand, cloneAndDiscover, codes_ErrorCode as ErrorCode, convertNodeError, createInstallProgressReporter, discoverSkillsInDir, fetchSkillsFromLock, formatErrorForDisplay, getExitCode, initCommand, installCommand, installSkills, installStageHooks, isLockInSync, isSpmError, linkSkillsFromLock, listRepoSkills, normalizeSpecifier, parseGitHubUrl, parseOwnerRepo, parseSpecifier, readSkillsLock, readSkillsManifest, resolveLockEntry, runCli, updateCommand, writeSkillsLock, writeSkillsManifest };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-package-manager",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,8 @@
21
21
  "@clack/prompts": "^1.1.0",
22
22
  "cac": "^7.0.0",
23
23
  "picocolors": "^1.1.1",
24
+ "semver": "^7.7.2",
25
+ "tar": "^7.4.3",
24
26
  "yaml": "^2.8.1"
25
27
  },
26
28
  "devDependencies": {