@@ -39,6 +39,7 @@ function normalizeSampleArg(sample: string): string {
3939 if ( s . startsWith ( "samples/" ) ) return s . slice ( "samples/" . length ) ;
4040 return s ;
4141}
42+ export { normalizeSampleArg } ;
4243
4344async function pathExists ( p : string ) : Promise < boolean > {
4445 try {
@@ -115,6 +116,7 @@ function parseGitVersion(output: string): { major: number; minor: number; patch:
115116 if ( ! m ) return null ;
116117 return { major : Number ( m [ 1 ] ) , minor : Number ( m [ 2 ] ) , patch : Number ( m [ 3 ] ) } ;
117118}
119+ export { parseGitVersion } ;
118120
119121function versionGte (
120122 v : { major : number ; minor : number ; patch : number } ,
@@ -124,6 +126,7 @@ function versionGte(
124126 if ( v . minor !== min . minor ) return v . minor > min . minor ;
125127 return v . patch >= min . patch ;
126128}
129+ export { versionGte } ;
127130
128131async function ensureGit ( verbose ? : boolean ) : Promise < void > {
129132 let res : RunResult ;
@@ -161,6 +164,7 @@ function assertMethod(m: string | undefined): Method {
161164 if ( m === "auto" || m === "git" || m === "api" ) return m ;
162165 throw new Error ( `Invalid --method "${ m } ". Use "auto", "git", or "api".` ) ;
163166}
167+ export { assertMethod } ;
164168
165169async function copyDir ( src : string , dest : string ) : Promise < void > {
166170 await fs . mkdir ( dest , { recursive : true } ) ;
@@ -384,11 +388,13 @@ function assertMode(m: string | undefined): Mode {
384388 if ( m === "extract" || m === "repo" ) return m ;
385389 throw new Error ( `Invalid --mode "${ m } ". Use "extract" or "repo".` ) ;
386390}
391+ export { assertMode } ;
387392
388393function isGuid ( v : string ) : boolean {
389394 // Accepts RFC4122-ish GUIDs (case-insensitive)
390395 return / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 1 - 5 ] [ 0 - 9 a - f ] { 3 } - [ 89 ab ] [ 0 - 9 a - f ] { 3 } - [ 0 - 9 a - f ] { 12 } $ / i . test ( v ) ;
391396}
397+ export { isGuid } ;
392398
393399async function readJsonIfExists < T > ( filePath : string ) : Promise < T | null > {
394400 try {
@@ -732,6 +738,94 @@ program
732738 }
733739 } ) ;
734740
741+ /**
742+ * Testable handler for the `get` command. Allows injecting dependencies for unit testing.
743+ */
744+ export async function getCommandHandler ( sample : string , options : CliOptions , deps ?: {
745+ download ?: typeof downloadSampleViaGitHubSubtree ;
746+ fetchSparse ?: typeof fetchSampleViaSparseGitExtract ;
747+ sparseClone ?: typeof sparseCloneInto ;
748+ postProcess ?: typeof postProcessProject ;
749+ finalize ?: typeof finalizeExtraction ;
750+ isGitAvailable ?: typeof isGitAvailable ;
751+ ensureGit ?: typeof ensureGit ;
752+ } ) {
753+ const sampleFolder = normalizeSampleArg ( sample ) ;
754+ const ref = options . ref || DEFAULT_REF ;
755+ const repo = options . repo || DEFAULT_REPO ;
756+ const owner = options . owner || DEFAULT_OWNER ;
757+ const verbose = ! ! options . verbose ;
758+
759+ const download = deps ?. download ?? downloadSampleViaGitHubSubtree ;
760+ const fetchSparse = deps ?. fetchSparse ?? fetchSampleViaSparseGitExtract ;
761+ const sparseClone = deps ?. sparseClone ?? sparseCloneInto ;
762+ const postProcess = deps ?. postProcess ?? postProcessProject ;
763+ const finalize = deps ?. finalize ?? finalizeExtraction ;
764+ const gitAvailableFn = deps ?. isGitAvailable ?? isGitAvailable ;
765+ const ensureGitFn = deps ?. ensureGit ?? ensureGit ;
766+
767+ let mode : Mode ;
768+ try {
769+ mode = assertMode ( options . mode ) ;
770+ } catch ( e ) {
771+ throw e ;
772+ }
773+
774+ let method : Method ;
775+ try {
776+ method = assertMethod ( options . method ) ;
777+ } catch ( e ) {
778+ throw e ;
779+ }
780+
781+ const defaultDest = mode === "extract" ? `./${ sampleFolder } ` : `./${ repo } -${ sampleFolder } ` . replaceAll ( "/" , "-" ) ;
782+ const destDir = path . resolve ( options . dest ?? defaultDest ) ;
783+
784+ const gitAvailable = await gitAvailableFn ( verbose ) ;
785+ const chosen : Method = method === "auto" ? ( gitAvailable ? "git" : "api" ) : method ;
786+
787+ if ( chosen === "git" ) {
788+ await ensureGitFn ( verbose ) ;
789+ }
790+
791+ if ( chosen === "api " && mode === "repo ") {
792+ throw new Error ( `--mode repo requires --method git (API method cannot create a git working repo).` ) ;
793+ }
794+
795+ if ( await pathExists ( destDir ) ) {
796+ if ( ! options . force ) {
797+ const nonEmpty = await isDirNonEmpty ( destDir ) ;
798+ if ( nonEmpty ) throw new Error ( `Destination exists and is not empty: ${ destDir } ` ) ;
799+ } else {
800+ await fs . rm ( destDir , { recursive : true , force : true } ) ;
801+ }
802+ }
803+
804+ if ( chosen === "api" ) {
805+ await fs . mkdir ( destDir , { recursive : true } ) ;
806+ await download ( { owner, repo, ref, sampleFolder, destDir, concurrency : 8 , verbose, signal : undefined , onProgress : undefined } ) ;
807+ await postProcess ( destDir , options , undefined ) ;
808+ await finalize ( { spinner : undefined , successMessage : `Done` , projectPath : destDir } ) ;
809+ return ;
810+ }
811+
812+ if ( chosen === "git" ) {
813+ if ( mode === "extract" ) {
814+ await fetchSparse ( { owner, repo, ref, sampleFolder, destDir, verbose, spinner : undefined , signal : undefined } ) ;
815+ await postProcess ( destDir , options , undefined ) ;
816+ await finalize ( { spinner : undefined , successMessage : `Done` , projectPath : destDir } ) ;
817+ return ;
818+ } else {
819+ await fs . mkdir ( destDir , { recursive : true } ) ;
820+ await sparseClone ( { owner, repo, ref, sampleFolder, repoDir : destDir , verbose, spinner : undefined , signal : undefined } ) ;
821+ const samplePath = path . join ( destDir , "samples" , sampleFolder ) ;
822+ await postProcess ( samplePath , options , undefined ) ;
823+ await finalize ( { spinner : undefined , successMessage : `Done` , projectPath : samplePath , repoRoot : destDir } ) ;
824+ return ;
825+ }
826+ }
827+ }
828+
735829program
736830 . command ( "rename" )
737831 . argument ( "<path>" , "Path to previously downloaded sample folder (project root)" )
0 commit comments