UNPKG

create-defuss

Version:

Checks out git projects from sub-directories. Originally for jump-starting defuss projects from templates.

1 lines 6.88 kB
{"version":3,"file":"cli.mjs","sources":["../src/git.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport {\n resolve,\n join,\n normalize,\n isAbsolute,\n sep,\n basename,\n} from \"node:path\";\nimport {\n existsSync,\n rmSync,\n readdirSync,\n mkdtempSync,\n mkdirSync,\n copyFileSync,\n lstatSync,\n readlinkSync,\n symlinkSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\n\nexport const defaultScmHostPattern =\n /^(https:\\/\\/(?:github|gitlab|bitbucket)\\.com)\\/([^\\/]+)\\/([^\\/]+)\\/(?:tree|src)\\/([^\\/]+)\\/(.+)$/;\n\nexport const performSparseCheckout = (\n repoUrl: string,\n destFolder?: string,\n scmHostPattern = defaultScmHostPattern\n) => {\n try {\n const match = repoUrl.match(scmHostPattern);\n\n if (!match) {\n throw new Error(\n \"Invalid URL format. Use a subdirectory URL (https) from GitHub, GitLab, or Bitbucket.\"\n );\n }\n\n const [, platformUrl, owner, repo, branch, subdir] = match;\n\n // Validate inputs to prevent command injection and path traversal\n [owner, repo, branch].forEach((input) => {\n if (!/^[\\w\\-]+$/.test(input)) {\n throw new Error(`Invalid characters in input: ${input}`);\n }\n });\n\n // Validate subdir\n const subdirNormalized = normalize(subdir);\n\n // Check if subdir is absolute or contains path traversal\n if (\n isAbsolute(subdirNormalized) ||\n subdirNormalized.startsWith(\"..\") ||\n subdirNormalized.includes(`${sep}..${sep}`)\n ) {\n throw new Error(\"Invalid subdirectory path.\");\n }\n\n const sanitizedSubdir = subdirNormalized;\n\n const fallbackDestFolder = destFolder || basename(sanitizedSubdir);\n const targetPath = resolve(process.cwd(), fallbackDestFolder);\n\n if (existsSync(targetPath)) {\n throw new Error(`Destination folder \"${fallbackDestFolder}\" already exists.`);\n }\n\n // Create the destination directory\n mkdirSync(targetPath, { recursive: true });\n\n // Create a temporary directory for the clone\n const tempDir = mkdtempSync(join(tmpdir(), \"sparse-checkout-\"));\n\n console.log(\"Cloning repository with sparse checkout into temporary directory...\");\n const cloneResult = spawnSync(\n \"git\",\n [\"clone\", \"--no-checkout\", `${platformUrl}/${owner}/${repo}.git`, tempDir],\n { stdio: \"inherit\" }\n );\n if (cloneResult.status !== 0) {\n throw new Error(\"Git clone failed.\");\n }\n\n const subdirPath = resolve(tempDir, sanitizedSubdir);\n\n // Ensure subdirPath is within tempDir\n if (!subdirPath.startsWith(tempDir + sep) && subdirPath !== tempDir) {\n throw new Error(\"Subdirectory path traversal detected.\");\n }\n\n console.log(\"Initializing sparse-checkout...\");\n const initResult = spawnSync(\"git\", [\"-C\", tempDir, \"sparse-checkout\", \"init\"], {\n stdio: \"inherit\",\n });\n if (initResult.status !== 0) {\n throw new Error(\"Git sparse-checkout init failed.\");\n }\n\n console.log(`Setting sparse-checkout to subdirectory: ${sanitizedSubdir}`);\n const setResult = spawnSync(\n \"git\",\n [\"-C\", tempDir, \"sparse-checkout\", \"set\", sanitizedSubdir],\n { stdio: \"inherit\" }\n );\n if (setResult.status !== 0) {\n throw new Error(\"Git sparse-checkout set failed.\");\n }\n\n console.log(`Checking out branch: ${branch}...`);\n const checkoutResult = spawnSync(\"git\", [\"-C\", tempDir, \"checkout\", branch], {\n stdio: \"inherit\",\n });\n if (checkoutResult.status !== 0) {\n throw new Error(\"Git checkout failed.\");\n }\n\n if (!existsSync(subdirPath)) {\n throw new Error(`Subdirectory \"${sanitizedSubdir}\" does not exist in the repository.`);\n }\n\n console.log(\"Copying files to the destination directory...\");\n copyDirectoryContents(subdirPath, targetPath);\n\n console.log(\"Cleaning up temporary directory...\");\n rmSync(tempDir, { recursive: true, force: true });\n\n console.log(\"Initializing a new git repository...\");\n const initNewRepoResult = spawnSync(\"git\", [\"init\"], {\n cwd: targetPath,\n stdio: \"inherit\",\n });\n if (initNewRepoResult.status !== 0) {\n throw new Error(\"Initializing new git repository failed.\");\n }\n\n console.log(\"🎉 All done! Your new project has been set up!\");\n\n console.log(`\\nTo get started, run the following commands:\\n\\n cd ${fallbackDestFolder}\\n`);\n } catch (err) {\n console.error(\"Error during sparse checkout:\", (err as Error).message);\n process.exit(1);\n }\n};\n\n/**\n * Recursively copies all files and directories from `source` to `destination`.\n */\nfunction copyDirectoryContents(source: string, destination: string) {\n if (!existsSync(source)) {\n throw new Error(`Source directory \"${source}\" does not exist.`);\n }\n\n const items = readdirSync(source);\n\n for (const item of items) {\n const srcPath = join(source, item);\n const destPath = join(destination, item);\n const stats = lstatSync(srcPath);\n\n if (stats.isDirectory()) {\n mkdirSync(destPath, { recursive: true });\n copyDirectoryContents(srcPath, destPath);\n } else if (stats.isSymbolicLink()) {\n const symlink = readlinkSync(srcPath);\n symlinkSync(symlink, destPath);\n } else {\n copyFileSync(srcPath, destPath);\n }\n }\n}\n"],"names":[],"mappings":";;;;;;AAsBY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,qBAAqB,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;"}