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.
- package/README.md +15 -8
- package/dist/index.js +659 -91
- 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
|
|
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
|
|
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
|
|
100
|
-
- Skips `
|
|
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
|
|
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
|
-
- **`
|
|
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
|
|
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.
|
|
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 -
|
|
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
|
|
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 ('
|
|
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
|
-
|
|
885
|
+
tarball: toPortableRelativePath(cwd, tarballPath),
|
|
886
|
+
path: normalized.path
|
|
599
887
|
},
|
|
600
|
-
digest:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
876
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1272
|
-
await
|
|
1834
|
+
reporter.setPhase('finalizing');
|
|
1835
|
+
await writeSkillsLock(options.cwd, lockfile);
|
|
1836
|
+
reporter.complete();
|
|
1273
1837
|
return {
|
|
1274
1838
|
status: 'installed',
|
|
1275
|
-
installed: Object.keys(
|
|
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
|
+
"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": {
|