@@ -33,6 +33,40 @@ function resolveGithubArchiveZipUrl(inputUrl) {
3333 return `https://github.com/${ encodeURIComponent ( owner ) } /${ encodeURIComponent ( repo ) } /archive/refs/heads/${ encodeURIComponent ( ref ) } .zip` ;
3434}
3535
36+ function buildGithubArchiveZipCandidates ( inputUrl ) {
37+ const raw = typeof inputUrl === 'string' ? inputUrl . trim ( ) : '' ;
38+ if ( ! raw ) return [ ] ;
39+ let parsed ;
40+ try {
41+ parsed = new URL ( raw ) ;
42+ } catch ( _ ) {
43+ return [ ] ;
44+ }
45+ if ( parsed . protocol !== 'http:' && parsed . protocol !== 'https:' ) {
46+ return [ ] ;
47+ }
48+ if ( parsed . hostname !== 'github.com' ) {
49+ return [ ] ;
50+ }
51+ const parts = parsed . pathname . split ( '/' ) . filter ( Boolean ) ;
52+ if ( parts . length < 2 ) return [ ] ;
53+ const owner = parts [ 0 ] ;
54+ const repo = ( parts [ 1 ] || '' ) . endsWith ( '.git' ) ? parts [ 1 ] . slice ( 0 , - 4 ) : parts [ 1 ] ;
55+ if ( ! owner || ! repo ) return [ ] ;
56+ const base = `https://github.com/${ encodeURIComponent ( owner ) } /${ encodeURIComponent ( repo ) } /archive/refs` ;
57+ if ( parts [ 2 ] === 'tree' && parts [ 3 ] ) {
58+ const ref = encodeURIComponent ( parts [ 3 ] ) ;
59+ return [
60+ `${ base } /heads/${ ref } .zip` ,
61+ `${ base } /tags/${ ref } .zip`
62+ ] ;
63+ }
64+ return [
65+ `${ base } /heads/main.zip` ,
66+ `${ base } /heads/master.zip`
67+ ] ;
68+ }
69+
3670function redactUrlForLog ( inputUrl ) {
3771 const raw = typeof inputUrl === 'string' ? inputUrl . trim ( ) : '' ;
3872 if ( ! raw ) return '' ;
@@ -147,19 +181,44 @@ function downloadUrlToFile(targetUrl, filePath, options = {}) {
147181 } ) ;
148182}
149183
184+ function extractHttpStatusFromError ( err ) {
185+ const message = err && err . message ? String ( err . message ) : '' ;
186+ const matched = message . match ( / \b H T T P \s + ( \d { 3 } ) \b / ) ;
187+ if ( ! matched ) return 0 ;
188+ const value = Number ( matched [ 1 ] ) ;
189+ return Number . isFinite ( value ) ? value : 0 ;
190+ }
191+
192+ function printImportSkillsUsage ( ) {
193+ process . stdout . write ( '\n用法:\n' ) ;
194+ process . stdout . write ( ' codexmate import-skills <URL> [--target-app codex|claude] [--name <NAME>] [--timeout-ms <MS>]\n' ) ;
195+ process . stdout . write ( '\n示例:\n' ) ;
196+ process . stdout . write ( ' codexmate import-skills https://github.com/<owner>/<repo>\n' ) ;
197+ process . stdout . write ( ' codexmate import-skills https://github.com/<owner>/<repo>/tree/dev\n' ) ;
198+ process . stdout . write ( ' codexmate import-skills https://github.com/<owner>/<repo>/archive/refs/heads/main.zip\n' ) ;
199+ }
200+
150201function parseImportSkillsCommandArgs ( argv = [ ] ) {
151202 const options = {
152203 url : '' ,
153204 targetApp : 'codex' ,
154205 name : '' ,
155- timeoutMs : 30000
206+ timeoutMs : 30000 ,
207+ help : false
156208 } ;
157- if ( argv [ 0 ] && ! String ( argv [ 0 ] ) . startsWith ( '--' ) ) {
158- options . url = String ( argv [ 0 ] ) . trim ( ) ;
159- }
160- let cursor = 1 ;
209+ let cursor = 0 ;
161210 while ( cursor < argv . length ) {
162211 const token = String ( argv [ cursor ] || '' ) ;
212+ if ( token && ! token . startsWith ( '--' ) && ! options . url ) {
213+ options . url = token . trim ( ) ;
214+ cursor += 1 ;
215+ continue ;
216+ }
217+ if ( token === '--help' || token === '-h' ) {
218+ options . help = true ;
219+ cursor += 1 ;
220+ continue ;
221+ }
163222 if ( token === '--target-app' ) {
164223 const value = String ( argv [ cursor + 1 ] || '' ) . trim ( ) . toLowerCase ( ) ;
165224 if ( ! value || value . startsWith ( '--' ) ) {
@@ -189,40 +248,77 @@ function parseImportSkillsCommandArgs(argv = []) {
189248 }
190249 cursor += 1 ;
191250 }
192- if ( ! options . url ) {
193- throw new Error ( '错误: 缺少 URL(例如: https://github.com/<owner>/<repo>/archive/refs/heads/main.zip)' ) ;
194- }
195251 return options ;
196252}
197253
198254async function cmdImportSkills ( argv = [ ] ) {
199255 const options = parseImportSkillsCommandArgs ( argv ) ;
200- const resolvedGithubUrl = resolveGithubArchiveZipUrl ( options . url ) ;
201- const zipUrl = resolvedGithubUrl || options . url ;
202- if ( ! isValidHttpUrl ( zipUrl ) ) {
256+ if ( options . help ) {
257+ printImportSkillsUsage ( ) ;
258+ return ;
259+ }
260+ if ( ! options . url || options . url . trim ( ) . startsWith ( '--' ) ) {
261+ printImportSkillsUsage ( ) ;
262+ throw new Error ( '错误: 缺少 URL(例如: https://github.com/<owner>/<repo>/archive/refs/heads/main.zip)' ) ;
263+ }
264+
265+ const candidates = [ ] ;
266+ const githubCandidates = buildGithubArchiveZipCandidates ( options . url ) ;
267+ if ( githubCandidates . length ) {
268+ candidates . push ( ...githubCandidates ) ;
269+ } else {
270+ const resolvedGithubUrl = resolveGithubArchiveZipUrl ( options . url ) ;
271+ candidates . push ( resolvedGithubUrl || options . url ) ;
272+ }
273+ const uniqueCandidates = Array . from ( new Set ( candidates . filter ( Boolean ) ) ) ;
274+ if ( ! uniqueCandidates . length || ! uniqueCandidates . every ( isValidHttpUrl ) ) {
203275 throw new Error ( '错误: URL 非法(仅支持 http/https)' ) ;
204276 }
205277
206278 const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'codexmate-skills-url-' ) ) ;
207279 const zipPath = path . join ( tempDir , 'skills.zip' ) ;
208- const fallbackName = options . name || path . basename ( new URL ( zipUrl ) . pathname ) || 'skills.zip' ;
209-
210- console . log ( `\n[Skills] Download: ${ redactUrlForLog ( zipUrl ) } ` ) ;
211- await downloadUrlToFile ( zipUrl , zipPath , {
212- maxBytes : MAX_SKILLS_ZIP_UPLOAD_SIZE ,
213- timeoutMs : options . timeoutMs ,
214- maxRedirects : 5
215- } ) ;
216- const result = await importSkillsFromZipFile ( zipPath , {
217- tempDir,
218- targetApp : options . targetApp ,
219- fallbackName
220- } ) ;
221- process . stdout . write ( JSON . stringify ( result , null , 2 ) + '\n' ) ;
280+ let lastError = null ;
281+ let finalUrl = uniqueCandidates [ 0 ] ;
282+ try {
283+ for ( const candidateUrl of uniqueCandidates ) {
284+ finalUrl = candidateUrl ;
285+ console . log ( `\n[Skills] Download: ${ redactUrlForLog ( candidateUrl ) } ` ) ;
286+ try {
287+ await downloadUrlToFile ( candidateUrl , zipPath , {
288+ maxBytes : MAX_SKILLS_ZIP_UPLOAD_SIZE ,
289+ timeoutMs : options . timeoutMs ,
290+ maxRedirects : 5
291+ } ) ;
292+ lastError = null ;
293+ break ;
294+ } catch ( e ) {
295+ lastError = e ;
296+ const status = extractHttpStatusFromError ( e ) ;
297+ if ( status === 404 ) {
298+ continue ;
299+ }
300+ throw e ;
301+ }
302+ }
303+ if ( lastError ) {
304+ throw lastError ;
305+ }
306+ const fallbackName = options . name || path . basename ( new URL ( finalUrl ) . pathname ) || 'skills.zip' ;
307+ const result = await importSkillsFromZipFile ( zipPath , {
308+ tempDir,
309+ targetApp : options . targetApp ,
310+ fallbackName
311+ } ) ;
312+ process . stdout . write ( JSON . stringify ( result , null , 2 ) + '\n' ) ;
313+ } finally {
314+ try {
315+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
316+ } catch ( _ ) { }
317+ }
222318}
223319
224320module . exports = {
225321 resolveGithubArchiveZipUrl,
322+ buildGithubArchiveZipCandidates,
226323 cmdImportSkills
227324} ;
228-
0 commit comments