@salesforce/b2c-tooling-sdk 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/skills/github.d.ts +2 -18
- package/dist/cjs/skills/github.js +195 -108
- package/dist/cjs/skills/github.js.map +1 -1
- package/dist/cjs/skills/index.d.ts +2 -1
- package/dist/cjs/skills/index.js +2 -0
- package/dist/cjs/skills/index.js.map +1 -1
- package/dist/cjs/skills/parser.d.ts +2 -2
- package/dist/cjs/skills/parser.js +13 -18
- package/dist/cjs/skills/parser.js.map +1 -1
- package/dist/cjs/skills/sources.d.ts +4 -0
- package/dist/cjs/skills/sources.js +44 -0
- package/dist/cjs/skills/sources.js.map +1 -0
- package/dist/cjs/skills/types.d.ts +16 -1
- package/dist/esm/skills/github.d.ts +2 -18
- package/dist/esm/skills/github.js +195 -108
- package/dist/esm/skills/github.js.map +1 -1
- package/dist/esm/skills/index.d.ts +2 -1
- package/dist/esm/skills/index.js +2 -0
- package/dist/esm/skills/index.js.map +1 -1
- package/dist/esm/skills/parser.d.ts +2 -2
- package/dist/esm/skills/parser.js +13 -18
- package/dist/esm/skills/parser.js.map +1 -1
- package/dist/esm/skills/sources.d.ts +4 -0
- package/dist/esm/skills/sources.js +44 -0
- package/dist/esm/skills/sources.js.map +1 -0
- package/dist/esm/skills/types.d.ts +16 -1
- package/package.json +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/skills/parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/skills/parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
function pluginsTag(version) {
|
|
7
|
+
const bare = version.replace(/^v/, '');
|
|
8
|
+
return `b2c-agent-plugins@${bare}`;
|
|
9
|
+
}
|
|
10
|
+
export const SKILL_SOURCES = {
|
|
11
|
+
b2c: {
|
|
12
|
+
id: 'b2c',
|
|
13
|
+
displayName: 'B2C Commerce development patterns',
|
|
14
|
+
type: 'release-artifact',
|
|
15
|
+
repo: 'SalesforceCommerceCloud/b2c-developer-tooling',
|
|
16
|
+
assetName: 'b2c-skills.zip',
|
|
17
|
+
tagPattern: pluginsTag,
|
|
18
|
+
},
|
|
19
|
+
'b2c-cli': {
|
|
20
|
+
id: 'b2c-cli',
|
|
21
|
+
displayName: 'B2C CLI commands and operations',
|
|
22
|
+
type: 'release-artifact',
|
|
23
|
+
repo: 'SalesforceCommerceCloud/b2c-developer-tooling',
|
|
24
|
+
assetName: 'b2c-cli-skills.zip',
|
|
25
|
+
tagPattern: pluginsTag,
|
|
26
|
+
},
|
|
27
|
+
'cap-dev': {
|
|
28
|
+
id: 'cap-dev',
|
|
29
|
+
displayName: 'Commerce Apps development skills',
|
|
30
|
+
type: 'repo-contents',
|
|
31
|
+
repo: 'SalesforceCommerceCloud/commerce-apps',
|
|
32
|
+
ref: 'main',
|
|
33
|
+
skillsPath: '.claude/skills',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
export function getSkillSource(skillSet) {
|
|
37
|
+
const source = SKILL_SOURCES[skillSet];
|
|
38
|
+
if (!source) {
|
|
39
|
+
throw new Error(`Unknown skill set: ${skillSet}`);
|
|
40
|
+
}
|
|
41
|
+
return source;
|
|
42
|
+
}
|
|
43
|
+
export const ALL_SKILL_SETS = Object.keys(SKILL_SOURCES);
|
|
44
|
+
//# sourceMappingURL=sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sources.js","sourceRoot":"","sources":["../../../src/skills/sources.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,SAAS,UAAU,CAAC,OAAe;IACjC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACvC,OAAO,qBAAqB,IAAI,EAAE,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAwC;IAChE,GAAG,EAAE;QACH,EAAE,EAAE,KAAK;QACT,WAAW,EAAE,mCAAmC;QAChD,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,+CAA+C;QACrD,SAAS,EAAE,gBAAgB;QAC3B,UAAU,EAAE,UAAU;KACvB;IACD,SAAS,EAAE;QACT,EAAE,EAAE,SAAS;QACb,WAAW,EAAE,iCAAiC;QAC9C,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,+CAA+C;QACrD,SAAS,EAAE,oBAAoB;QAC/B,UAAU,EAAE,UAAU;KACvB;IACD,SAAS,EAAE;QACT,EAAE,EAAE,SAAS;QACb,WAAW,EAAE,kCAAkC;QAC/C,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,uCAAuC;QAC7C,GAAG,EAAE,MAAM;QACX,UAAU,EAAE,gBAAgB;KAC7B;CACF,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,QAAkB;IAC/C,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAe,MAAM,CAAC,IAAI,CAAC,aAAa,CAAe,CAAC"}
|
|
@@ -5,7 +5,20 @@ export type IdeType = 'claude-code' | 'cursor' | 'windsurf' | 'vscode' | 'codex'
|
|
|
5
5
|
/**
|
|
6
6
|
* Skill set categories matching the plugins directory structure.
|
|
7
7
|
*/
|
|
8
|
-
export type SkillSet = 'b2c' | 'b2c-cli';
|
|
8
|
+
export type SkillSet = 'b2c' | 'b2c-cli' | 'cap-dev';
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for a skill source — defines how to fetch skills from a particular repository.
|
|
11
|
+
*/
|
|
12
|
+
export interface SkillSourceConfig {
|
|
13
|
+
id: SkillSet;
|
|
14
|
+
displayName: string;
|
|
15
|
+
type: 'release-artifact' | 'repo-contents';
|
|
16
|
+
repo: string;
|
|
17
|
+
assetName?: string;
|
|
18
|
+
tagPattern?: (version: string) => string;
|
|
19
|
+
ref?: string;
|
|
20
|
+
skillsPath?: string;
|
|
21
|
+
}
|
|
9
22
|
/**
|
|
10
23
|
* IDE path configuration for skill installation.
|
|
11
24
|
*/
|
|
@@ -133,4 +146,6 @@ export interface CachedArtifact {
|
|
|
133
146
|
path: string;
|
|
134
147
|
/** ISO date string when artifact was downloaded */
|
|
135
148
|
downloadedAt: string;
|
|
149
|
+
/** For repo-contents sources: the resolved commit SHA */
|
|
150
|
+
commitSha?: string;
|
|
136
151
|
}
|
|
@@ -2,46 +2,30 @@ import type { CachedArtifact, DownloadSkillsOptions, ReleaseInfo, SkillSet } fro
|
|
|
2
2
|
/**
|
|
3
3
|
* Get the cache directory for skills.
|
|
4
4
|
* Uses XDG_CACHE_HOME on Linux, ~/.cache otherwise.
|
|
5
|
-
*
|
|
6
|
-
* @returns Absolute path to cache directory
|
|
7
5
|
*/
|
|
8
6
|
export declare function getCacheDir(): string;
|
|
9
7
|
/**
|
|
10
8
|
* Fetch release information from GitHub API.
|
|
11
|
-
*
|
|
12
|
-
* @param version - 'latest' or specific version (e.g., 'v0.1.0')
|
|
13
|
-
* @returns Release information
|
|
14
|
-
* @throws Error if release not found or API request fails
|
|
15
9
|
*/
|
|
16
10
|
export declare function getRelease(version?: string): Promise<ReleaseInfo>;
|
|
17
11
|
/**
|
|
18
12
|
* List available releases with skills artifacts.
|
|
19
|
-
*
|
|
20
|
-
* @param limit - Maximum number of releases to return (default: 10)
|
|
21
|
-
* @returns Array of release information
|
|
22
13
|
*/
|
|
23
14
|
export declare function listReleases(limit?: number): Promise<ReleaseInfo[]>;
|
|
24
15
|
/**
|
|
25
16
|
* Get cached artifact metadata if available.
|
|
26
|
-
*
|
|
27
|
-
* @param version - Release version
|
|
28
|
-
* @param skillSet - Skill set to check
|
|
29
|
-
* @returns Cached artifact info or null if not cached
|
|
30
17
|
*/
|
|
31
18
|
export declare function getCachedArtifact(version: string, skillSet: SkillSet): CachedArtifact | null;
|
|
32
19
|
/**
|
|
33
20
|
* Download and extract skills artifact.
|
|
34
|
-
*
|
|
21
|
+
* Dispatches to the appropriate download strategy based on the skill source type.
|
|
35
22
|
*
|
|
36
|
-
* @param skillSet - Which skill set to download
|
|
23
|
+
* @param skillSet - Which skill set to download
|
|
37
24
|
* @param options - Download options
|
|
38
25
|
* @returns Path to extracted skills directory
|
|
39
|
-
* @throws Error if download fails or artifact not available
|
|
40
26
|
*/
|
|
41
27
|
export declare function downloadSkillsArtifact(skillSet: SkillSet, options?: DownloadSkillsOptions): Promise<string>;
|
|
42
28
|
/**
|
|
43
29
|
* Clear the skills cache.
|
|
44
|
-
*
|
|
45
|
-
* @param version - Optional specific version to clear (default: all)
|
|
46
30
|
*/
|
|
47
31
|
export declare function clearCache(version?: string): Promise<void>;
|
|
@@ -3,46 +3,25 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2
|
|
4
4
|
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
6
7
|
import * as fs from 'node:fs';
|
|
7
8
|
import * as os from 'node:os';
|
|
8
9
|
import * as path from 'node:path';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
9
11
|
import JSZip from 'jszip';
|
|
12
|
+
import { getSkillSource } from './sources.js';
|
|
10
13
|
import { getLogger } from '../logging/logger.js';
|
|
11
|
-
const
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
12
15
|
const GITHUB_API_BASE = 'https://api.github.com';
|
|
13
16
|
const GITHUB_DOWNLOAD_BASE = 'https://github.com';
|
|
14
17
|
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com';
|
|
15
18
|
const LATEST_VERSION_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
const ASSET_NAMES = {
|
|
20
|
-
b2c: 'b2c-skills.zip',
|
|
21
|
-
'b2c-cli': 'b2c-cli-skills.zip',
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Build a direct CDN download URL for a release asset.
|
|
25
|
-
* Assumes a concrete tag — call `resolveLatestVersion()` first if you have 'latest'.
|
|
26
|
-
*/
|
|
27
|
-
function buildDownloadUrl(tag, assetName) {
|
|
28
|
-
return `${GITHUB_DOWNLOAD_BASE}/${GITHUB_REPO}/releases/download/${tag}/${assetName}`;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Tag name used for skill-only releases (matches publish.yml).
|
|
32
|
-
*/
|
|
33
|
-
function pluginsTag(version) {
|
|
34
|
-
const bare = version.replace(/^v/, '');
|
|
35
|
-
return `b2c-agent-plugins@${bare}`;
|
|
19
|
+
function buildDownloadUrl(repo, tag, assetName) {
|
|
20
|
+
return `${GITHUB_DOWNLOAD_BASE}/${repo}/releases/download/${tag}/${assetName}`;
|
|
36
21
|
}
|
|
37
|
-
/**
|
|
38
|
-
* File path for the resolved-latest-version cache.
|
|
39
|
-
*/
|
|
40
22
|
function getLatestVersionCachePath() {
|
|
41
23
|
return path.join(getCacheDir(), '.latest-version.json');
|
|
42
24
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Read a cached "latest" version if still fresh (1-hour TTL).
|
|
45
|
-
*/
|
|
46
25
|
function readLatestVersionCache() {
|
|
47
26
|
try {
|
|
48
27
|
const p = getLatestVersionCachePath();
|
|
@@ -68,9 +47,6 @@ function writeLatestVersionCache(version) {
|
|
|
68
47
|
// Caching is best-effort
|
|
69
48
|
}
|
|
70
49
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Detect whether a fetch response is an unauthenticated GitHub rate-limit.
|
|
73
|
-
*/
|
|
74
50
|
function isRateLimited(response) {
|
|
75
51
|
if (response.status !== 403 && response.status !== 429)
|
|
76
52
|
return false;
|
|
@@ -78,27 +54,15 @@ function isRateLimited(response) {
|
|
|
78
54
|
return remaining === '0' || response.status === 429;
|
|
79
55
|
}
|
|
80
56
|
/**
|
|
81
|
-
* Resolve the version of the most recent skills release.
|
|
82
|
-
*
|
|
83
|
-
* Strategy:
|
|
84
|
-
* 1. API primary — paginate /releases and return the first entry that has
|
|
85
|
-
* a skills asset attached.
|
|
86
|
-
* 2. Rate-limit fallback — fetch skills/package.json from main via
|
|
87
|
-
* raw.githubusercontent.com. This is the version that will be tagged
|
|
88
|
-
* b2c-agent-plugins@<version> at release time.
|
|
89
|
-
*
|
|
90
|
-
* Caches the resolved version for 1 hour to avoid re-resolving on consecutive
|
|
91
|
-
* calls within the same session. Always returns a bare version string like
|
|
92
|
-
* "1.2.3" (no 'v' prefix, no tag format).
|
|
57
|
+
* Resolve the version of the most recent skills release from the b2c-developer-tooling repo.
|
|
93
58
|
*/
|
|
94
|
-
async function resolveLatestVersion() {
|
|
59
|
+
async function resolveLatestVersion(source) {
|
|
95
60
|
const logger = getLogger();
|
|
96
61
|
const cached = readLatestVersionCache();
|
|
97
62
|
if (cached) {
|
|
98
63
|
logger.debug({ version: cached }, 'Using cached latest version');
|
|
99
64
|
return cached;
|
|
100
65
|
}
|
|
101
|
-
// Primary — GitHub REST API with asset filtering
|
|
102
66
|
try {
|
|
103
67
|
const releases = await listReleases(30);
|
|
104
68
|
if (releases.length > 0) {
|
|
@@ -117,8 +81,7 @@ async function resolveLatestVersion() {
|
|
|
117
81
|
logger.warn({ err: err.message }, 'GitHub API unavailable; falling back to main branch for version resolution');
|
|
118
82
|
}
|
|
119
83
|
}
|
|
120
|
-
|
|
121
|
-
const rawUrl = `${GITHUB_RAW_BASE}/${GITHUB_REPO}/main/skills/package.json`;
|
|
84
|
+
const rawUrl = `${GITHUB_RAW_BASE}/${source.repo}/main/skills/package.json`;
|
|
122
85
|
logger.debug({ url: rawUrl }, 'Fetching version from raw.githubusercontent.com');
|
|
123
86
|
const response = await fetch(rawUrl, { headers: { 'User-Agent': 'b2c-cli' } });
|
|
124
87
|
if (!response.ok) {
|
|
@@ -134,20 +97,17 @@ async function resolveLatestVersion() {
|
|
|
134
97
|
/**
|
|
135
98
|
* Get the cache directory for skills.
|
|
136
99
|
* Uses XDG_CACHE_HOME on Linux, ~/.cache otherwise.
|
|
137
|
-
*
|
|
138
|
-
* @returns Absolute path to cache directory
|
|
139
100
|
*/
|
|
140
101
|
export function getCacheDir() {
|
|
141
102
|
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
142
103
|
const baseCache = xdgCache || path.join(os.homedir(), '.cache');
|
|
143
104
|
return path.join(baseCache, 'b2c-cli', 'skills');
|
|
144
105
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Parse GitHub API release response into ReleaseInfo.
|
|
147
|
-
*/
|
|
148
106
|
function parseRelease(release) {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
107
|
+
const b2cSource = getSkillSource('b2c');
|
|
108
|
+
const b2cCliSource = getSkillSource('b2c-cli');
|
|
109
|
+
const b2cAsset = release.assets.find((a) => a.name === b2cSource.assetName);
|
|
110
|
+
const b2cCliAsset = release.assets.find((a) => a.name === b2cCliSource.assetName);
|
|
151
111
|
return {
|
|
152
112
|
tagName: release.tag_name,
|
|
153
113
|
version: release.tag_name.replace(/^v/, ''),
|
|
@@ -158,29 +118,22 @@ function parseRelease(release) {
|
|
|
158
118
|
}
|
|
159
119
|
/**
|
|
160
120
|
* Fetch release information from GitHub API.
|
|
161
|
-
*
|
|
162
|
-
* @param version - 'latest' or specific version (e.g., 'v0.1.0')
|
|
163
|
-
* @returns Release information
|
|
164
|
-
* @throws Error if release not found or API request fails
|
|
165
121
|
*/
|
|
166
122
|
export async function getRelease(version = 'latest') {
|
|
167
123
|
const logger = getLogger();
|
|
168
|
-
|
|
169
|
-
// This avoids GitHub's /releases/latest endpoint, which returns whichever
|
|
170
|
-
// release GitHub flags as latest regardless of whether it contains skills.
|
|
124
|
+
const source = getSkillSource('b2c');
|
|
171
125
|
let tag;
|
|
172
126
|
if (version === 'latest') {
|
|
173
|
-
const resolved = await resolveLatestVersion();
|
|
174
|
-
tag =
|
|
127
|
+
const resolved = await resolveLatestVersion(source);
|
|
128
|
+
tag = source.tagPattern(resolved);
|
|
175
129
|
}
|
|
176
130
|
else if (/^b2c-agent-plugins@/.test(version) || version.includes('@')) {
|
|
177
|
-
// Caller passed an explicit tag (including other packages' tags, for tests/internal use).
|
|
178
131
|
tag = version;
|
|
179
132
|
}
|
|
180
133
|
else {
|
|
181
|
-
tag =
|
|
134
|
+
tag = source.tagPattern(version);
|
|
182
135
|
}
|
|
183
|
-
const endpoint = `${GITHUB_API_BASE}/repos/${
|
|
136
|
+
const endpoint = `${GITHUB_API_BASE}/repos/${source.repo}/releases/tags/${encodeURIComponent(tag)}`;
|
|
184
137
|
logger.debug({ endpoint }, 'Fetching release info');
|
|
185
138
|
const response = await fetch(endpoint, {
|
|
186
139
|
headers: {
|
|
@@ -202,13 +155,11 @@ export async function getRelease(version = 'latest') {
|
|
|
202
155
|
}
|
|
203
156
|
/**
|
|
204
157
|
* List available releases with skills artifacts.
|
|
205
|
-
*
|
|
206
|
-
* @param limit - Maximum number of releases to return (default: 10)
|
|
207
|
-
* @returns Array of release information
|
|
208
158
|
*/
|
|
209
159
|
export async function listReleases(limit = 10) {
|
|
210
160
|
const logger = getLogger();
|
|
211
|
-
const
|
|
161
|
+
const source = getSkillSource('b2c');
|
|
162
|
+
const endpoint = `${GITHUB_API_BASE}/repos/${source.repo}/releases?per_page=${limit}`;
|
|
212
163
|
logger.debug({ endpoint }, 'Listing releases');
|
|
213
164
|
const response = await fetch(endpoint, {
|
|
214
165
|
headers: {
|
|
@@ -220,15 +171,10 @@ export async function listReleases(limit = 10) {
|
|
|
220
171
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
221
172
|
}
|
|
222
173
|
const data = (await response.json());
|
|
223
|
-
// Only return releases that have at least one skills artifact
|
|
224
174
|
return data.map(parseRelease).filter((r) => r.b2cSkillsAssetUrl || r.b2cCliSkillsAssetUrl);
|
|
225
175
|
}
|
|
226
176
|
/**
|
|
227
177
|
* Get cached artifact metadata if available.
|
|
228
|
-
*
|
|
229
|
-
* @param version - Release version
|
|
230
|
-
* @param skillSet - Skill set to check
|
|
231
|
-
* @returns Cached artifact info or null if not cached
|
|
232
178
|
*/
|
|
233
179
|
export function getCachedArtifact(version, skillSet) {
|
|
234
180
|
const cacheDir = getCacheDir();
|
|
@@ -245,58 +191,40 @@ export function getCachedArtifact(version, skillSet) {
|
|
|
245
191
|
}
|
|
246
192
|
}
|
|
247
193
|
/**
|
|
248
|
-
* Download and extract
|
|
249
|
-
* Uses direct download URLs to avoid GitHub API rate limits.
|
|
250
|
-
*
|
|
251
|
-
* @param skillSet - Which skill set to download ('b2c' or 'b2c-cli')
|
|
252
|
-
* @param options - Download options
|
|
253
|
-
* @returns Path to extracted skills directory
|
|
254
|
-
* @throws Error if download fails or artifact not available
|
|
194
|
+
* Download and extract a release-artifact skill set (b2c or b2c-cli).
|
|
255
195
|
*/
|
|
256
|
-
|
|
196
|
+
async function downloadReleaseArtifact(source, options = {}) {
|
|
257
197
|
const logger = getLogger();
|
|
258
198
|
const { version = 'latest', forceDownload = false } = options;
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
// This keeps the cache keyed to real version tags and avoids GitHub's opinionated
|
|
262
|
-
// /releases/latest endpoint, which may point at a release that has no skills zips.
|
|
263
|
-
const resolvedVersion = version === 'latest' ? await resolveLatestVersion() : version.replace(/^v/, '');
|
|
264
|
-
const tag = pluginsTag(resolvedVersion);
|
|
265
|
-
// Check cache for the resolved version
|
|
199
|
+
const resolvedVersion = version === 'latest' ? await resolveLatestVersion(source) : version.replace(/^v/, '');
|
|
200
|
+
const tag = source.tagPattern(resolvedVersion);
|
|
266
201
|
if (!forceDownload) {
|
|
267
|
-
const cached = getCachedArtifact(resolvedVersion,
|
|
202
|
+
const cached = getCachedArtifact(resolvedVersion, source.id);
|
|
268
203
|
if (cached && fs.existsSync(cached.path)) {
|
|
269
|
-
logger.debug({ version: resolvedVersion, skillSet, path: cached.path }, 'Using cached skills');
|
|
204
|
+
logger.debug({ version: resolvedVersion, skillSet: source.id, path: cached.path }, 'Using cached skills');
|
|
270
205
|
return cached.path;
|
|
271
206
|
}
|
|
272
207
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
logger.debug({ url: downloadUrl, skillSet }, 'Downloading skills artifact');
|
|
276
|
-
// Download artifact - GitHub will redirect to the actual file
|
|
208
|
+
const downloadUrl = buildDownloadUrl(source.repo, tag, source.assetName);
|
|
209
|
+
logger.debug({ url: downloadUrl, skillSet: source.id }, 'Downloading skills artifact');
|
|
277
210
|
const response = await fetch(downloadUrl, {
|
|
278
|
-
headers: {
|
|
279
|
-
'User-Agent': 'b2c-cli',
|
|
280
|
-
},
|
|
211
|
+
headers: { 'User-Agent': 'b2c-cli' },
|
|
281
212
|
redirect: 'follow',
|
|
282
213
|
});
|
|
283
214
|
if (!response.ok) {
|
|
284
215
|
if (response.status === 404) {
|
|
285
|
-
throw new Error(`Skills artifact '${assetName}' not found for release ${tag}`);
|
|
216
|
+
throw new Error(`Skills artifact '${source.assetName}' not found for release ${tag}`);
|
|
286
217
|
}
|
|
287
218
|
throw new Error(`Failed to download skills: ${response.status} ${response.statusText}`);
|
|
288
219
|
}
|
|
289
220
|
const zipBuffer = Buffer.from(await response.arrayBuffer());
|
|
290
221
|
logger.debug({ size: zipBuffer.length, version: resolvedVersion }, 'Downloaded skills archive');
|
|
291
|
-
// Extract to cache directory
|
|
292
222
|
const cacheDir = options.cacheDir || getCacheDir();
|
|
293
|
-
const extractDir = path.join(cacheDir, resolvedVersion,
|
|
294
|
-
// Clean existing extraction if present
|
|
223
|
+
const extractDir = path.join(cacheDir, resolvedVersion, source.id);
|
|
295
224
|
if (fs.existsSync(extractDir)) {
|
|
296
225
|
await fs.promises.rm(extractDir, { recursive: true });
|
|
297
226
|
}
|
|
298
227
|
await fs.promises.mkdir(extractDir, { recursive: true });
|
|
299
|
-
// Extract zip contents
|
|
300
228
|
const zip = await JSZip.loadAsync(zipBuffer);
|
|
301
229
|
let extractedCount = 0;
|
|
302
230
|
for (const [relativePath, zipEntry] of Object.entries(zip.files)) {
|
|
@@ -306,15 +234,12 @@ export async function downloadSkillsArtifact(skillSet, options = {}) {
|
|
|
306
234
|
}
|
|
307
235
|
const targetPath = path.join(extractDir, relativePath);
|
|
308
236
|
const targetDir = path.dirname(targetPath);
|
|
309
|
-
// Ensure parent directory exists
|
|
310
237
|
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
311
|
-
// Write file
|
|
312
238
|
const content = await zipEntry.async('nodebuffer');
|
|
313
239
|
await fs.promises.writeFile(targetPath, content);
|
|
314
240
|
extractedCount++;
|
|
315
241
|
}
|
|
316
242
|
logger.debug({ extractDir, fileCount: extractedCount }, 'Extracted skills');
|
|
317
|
-
// Write cache manifest
|
|
318
243
|
const manifest = {
|
|
319
244
|
version: resolvedVersion,
|
|
320
245
|
path: extractDir,
|
|
@@ -324,9 +249,171 @@ export async function downloadSkillsArtifact(skillSet, options = {}) {
|
|
|
324
249
|
return extractDir;
|
|
325
250
|
}
|
|
326
251
|
/**
|
|
327
|
-
*
|
|
252
|
+
* Resolve a git ref to a commit SHA via the GitHub API.
|
|
253
|
+
*/
|
|
254
|
+
async function resolveCommitSha(repo, ref) {
|
|
255
|
+
const response = await fetch(`${GITHUB_API_BASE}/repos/${repo}/commits/${encodeURIComponent(ref)}`, {
|
|
256
|
+
headers: { 'User-Agent': 'b2c-cli', Accept: 'application/vnd.github.v3.sha' },
|
|
257
|
+
});
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
throw new Error(`Failed to resolve ref '${ref}' for ${repo}: ${response.status} ${response.statusText}`);
|
|
260
|
+
}
|
|
261
|
+
return (await response.text()).trim();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Recursively fetch directory contents from the GitHub Contents API and write to disk.
|
|
265
|
+
*/
|
|
266
|
+
async function fetchContentsRecursive(repo, repoPath, ref, destDir) {
|
|
267
|
+
const logger = getLogger();
|
|
268
|
+
const endpoint = `${GITHUB_API_BASE}/repos/${repo}/contents/${encodeURIComponent(repoPath)}?ref=${encodeURIComponent(ref)}`;
|
|
269
|
+
const response = await fetch(endpoint, {
|
|
270
|
+
headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'b2c-cli' },
|
|
271
|
+
});
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
if (isRateLimited(response)) {
|
|
274
|
+
throw Object.assign(new Error('GitHub API rate-limited'), { rateLimited: true });
|
|
275
|
+
}
|
|
276
|
+
throw new Error(`GitHub Contents API error for ${repoPath}: ${response.status} ${response.statusText}`);
|
|
277
|
+
}
|
|
278
|
+
const entries = (await response.json());
|
|
279
|
+
let fileCount = 0;
|
|
280
|
+
await fs.promises.mkdir(destDir, { recursive: true });
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
const targetPath = path.join(destDir, entry.name);
|
|
283
|
+
if (entry.type === 'dir') {
|
|
284
|
+
fileCount += await fetchContentsRecursive(repo, entry.path, ref, targetPath);
|
|
285
|
+
}
|
|
286
|
+
else if (entry.type === 'file' && entry.download_url) {
|
|
287
|
+
const fileResponse = await fetch(entry.download_url, { headers: { 'User-Agent': 'b2c-cli' } });
|
|
288
|
+
if (!fileResponse.ok) {
|
|
289
|
+
logger.warn({ path: entry.path }, 'Failed to download file, skipping');
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const content = Buffer.from(await fileResponse.arrayBuffer());
|
|
293
|
+
await fs.promises.writeFile(targetPath, content);
|
|
294
|
+
fileCount++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return fileCount;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Recursively copy a directory tree.
|
|
301
|
+
*/
|
|
302
|
+
async function copyDirRecursive(src, dest) {
|
|
303
|
+
await fs.promises.mkdir(dest, { recursive: true });
|
|
304
|
+
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
const srcPath = path.join(src, entry.name);
|
|
307
|
+
const destPath = path.join(dest, entry.name);
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
await copyDirRecursive(srcPath, destPath);
|
|
310
|
+
}
|
|
311
|
+
else if (entry.isFile()) {
|
|
312
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Fallback: use sparse git checkout to fetch only the skills subtree.
|
|
318
|
+
*/
|
|
319
|
+
async function fetchViaSparseCheckout(repo, ref, skillsPath, destDir) {
|
|
320
|
+
const logger = getLogger();
|
|
321
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'b2c-skills-'));
|
|
322
|
+
try {
|
|
323
|
+
const repoUrl = `${GITHUB_DOWNLOAD_BASE}/${repo}.git`;
|
|
324
|
+
logger.debug({ repoUrl, skillsPath }, 'Falling back to sparse git checkout');
|
|
325
|
+
await execFileAsync('git', [
|
|
326
|
+
'clone',
|
|
327
|
+
'--no-checkout',
|
|
328
|
+
'--depth',
|
|
329
|
+
'1',
|
|
330
|
+
'--filter=blob:none',
|
|
331
|
+
'--sparse',
|
|
332
|
+
repoUrl,
|
|
333
|
+
tmpDir,
|
|
334
|
+
]);
|
|
335
|
+
await execFileAsync('git', ['-C', tmpDir, 'sparse-checkout', 'set', skillsPath]);
|
|
336
|
+
await execFileAsync('git', ['-C', tmpDir, 'checkout']);
|
|
337
|
+
const skillsSrc = path.join(tmpDir, skillsPath);
|
|
338
|
+
if (!fs.existsSync(skillsSrc)) {
|
|
339
|
+
throw new Error(`Skills path '${skillsPath}' not found in repo ${repo}`);
|
|
340
|
+
}
|
|
341
|
+
await copyDirRecursive(skillsSrc, destDir);
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Download skills from a repo-contents source (e.g., commerce-apps).
|
|
349
|
+
* Primary: GitHub Contents API. Fallback: sparse git checkout.
|
|
350
|
+
*/
|
|
351
|
+
async function downloadRepoContents(source, options = {}) {
|
|
352
|
+
const logger = getLogger();
|
|
353
|
+
const ref = options.version || source.ref || 'main';
|
|
354
|
+
const forceDownload = options.forceDownload ?? false;
|
|
355
|
+
const skillsPath = source.skillsPath || '.claude/skills';
|
|
356
|
+
const commitSha = await resolveCommitSha(source.repo, ref);
|
|
357
|
+
const cacheKey = `${ref}-${commitSha.slice(0, 8)}`;
|
|
358
|
+
if (!forceDownload) {
|
|
359
|
+
const cached = getCachedArtifact(cacheKey, source.id);
|
|
360
|
+
if (cached && fs.existsSync(cached.path)) {
|
|
361
|
+
logger.debug({ cacheKey, skillSet: source.id, path: cached.path }, 'Using cached skills');
|
|
362
|
+
return cached.path;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const cacheDir = options.cacheDir || getCacheDir();
|
|
366
|
+
const extractDir = path.join(cacheDir, cacheKey, source.id);
|
|
367
|
+
const skillsDestDir = path.join(extractDir, 'skills');
|
|
368
|
+
if (fs.existsSync(extractDir)) {
|
|
369
|
+
await fs.promises.rm(extractDir, { recursive: true });
|
|
370
|
+
}
|
|
371
|
+
await fs.promises.mkdir(skillsDestDir, { recursive: true });
|
|
372
|
+
try {
|
|
373
|
+
logger.debug({ repo: source.repo, skillsPath, ref }, 'Fetching skills via Contents API');
|
|
374
|
+
const fileCount = await fetchContentsRecursive(source.repo, skillsPath, ref, skillsDestDir);
|
|
375
|
+
logger.debug({ extractDir, fileCount }, 'Fetched skills via Contents API');
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
const isRateLimit = err instanceof Error && 'rateLimited' in err;
|
|
379
|
+
if (isRateLimit) {
|
|
380
|
+
logger.warn('Contents API rate-limited; falling back to sparse git checkout');
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
logger.warn({ err: err.message }, 'Contents API failed; falling back to sparse git checkout');
|
|
384
|
+
}
|
|
385
|
+
if (fs.existsSync(skillsDestDir)) {
|
|
386
|
+
await fs.promises.rm(skillsDestDir, { recursive: true });
|
|
387
|
+
}
|
|
388
|
+
await fs.promises.mkdir(skillsDestDir, { recursive: true });
|
|
389
|
+
await fetchViaSparseCheckout(source.repo, ref, skillsPath, skillsDestDir);
|
|
390
|
+
}
|
|
391
|
+
const manifest = {
|
|
392
|
+
version: cacheKey,
|
|
393
|
+
path: extractDir,
|
|
394
|
+
downloadedAt: new Date().toISOString(),
|
|
395
|
+
commitSha,
|
|
396
|
+
};
|
|
397
|
+
await fs.promises.writeFile(path.join(extractDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
398
|
+
return extractDir;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Download and extract skills artifact.
|
|
402
|
+
* Dispatches to the appropriate download strategy based on the skill source type.
|
|
328
403
|
*
|
|
329
|
-
* @param
|
|
404
|
+
* @param skillSet - Which skill set to download
|
|
405
|
+
* @param options - Download options
|
|
406
|
+
* @returns Path to extracted skills directory
|
|
407
|
+
*/
|
|
408
|
+
export async function downloadSkillsArtifact(skillSet, options = {}) {
|
|
409
|
+
const source = getSkillSource(skillSet);
|
|
410
|
+
if (source.type === 'release-artifact') {
|
|
411
|
+
return downloadReleaseArtifact(source, options);
|
|
412
|
+
}
|
|
413
|
+
return downloadRepoContents(source, options);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Clear the skills cache.
|
|
330
417
|
*/
|
|
331
418
|
export async function clearCache(version) {
|
|
332
419
|
const cacheDir = getCacheDir();
|