From 04bc3e7246f62e9357940d6c56f55f1c1872db3b Mon Sep 17 00:00:00 2001 From: EGOIST <0x142857@gmail.com> Date: Thu, 23 Dec 2021 20:49:37 +0800 Subject: [PATCH] feat: support downloading a sub folder BREAKING CHANGE: Removed Deno/Go support --- README.md | 17 +------ aho.code-workspace | 17 ------- aho.ts | 3 -- cli-node.js | 4 -- deno/README.md | 1 - deno/deno/lib.ts | 71 ---------------------------- deno/shared.ts | 1 - package.json | 10 ++-- scripts/build-deno.ts | 21 --------- deno/index.ts => src/cli.ts | 61 ++++++------------------ src/deno/.vscode/settings.json | 3 -- src/deno/lib.ts | 71 ---------------------------- src/index.ts | 85 +++++----------------------------- src/{node => }/lib.ts | 7 ++- src/shared.ts | 1 - 15 files changed, 36 insertions(+), 337 deletions(-) delete mode 100644 aho.code-workspace delete mode 100644 aho.ts delete mode 100644 cli-node.js delete mode 100644 deno/README.md delete mode 100644 deno/deno/lib.ts delete mode 100644 deno/shared.ts delete mode 100644 scripts/build-deno.ts rename deno/index.ts => src/cli.ts (51%) delete mode 100644 src/deno/.vscode/settings.json delete mode 100644 src/deno/lib.ts rename src/{node => }/lib.ts (91%) delete mode 100644 src/shared.ts diff --git a/README.md b/README.md index 5bc2226..3b05c17 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,6 @@ ## Usage -With Node.js: - ```bash # NPM npx aho user/repo [destination] @@ -19,15 +17,6 @@ npx aho user/repo [destination] pnpm dlx aho user/repo [destination] ``` -With Deno: - -```bash -deno install --allow-net --allow-read --allow-write --allow-run \ - https://denopkg.com/egoist/aho@latest/aho.ts - -aho user/repo [destination] -``` - ## Flags ### `aho [destination]` @@ -36,14 +25,12 @@ Generate a project from `` to `[destination]`, destination defaults to cur `` is in the format of `user/repo#branch_or_tag`, currently only GitHub repositories are supported. `#branch_or_tag` is optional. +You can also download a sub folder from the repo: `user/repo/sub_folder#dev`. + ### `--force` By default the command would abort if destination is not empty, use `--force` if you want to empty the dir before writing to it. -### `-p, --path ` - -Extract a sub directory from `` to `[destination]`. - ## Sponsors [![sponsors](https://sponsors-images.egoist.sh/sponsors.svg)](https://github.com/sponsors/egoist) diff --git a/aho.code-workspace b/aho.code-workspace deleted file mode 100644 index d7eac08..0000000 --- a/aho.code-workspace +++ /dev/null @@ -1,17 +0,0 @@ -{ - "folders": [ - { - "name": "Node project", - "path": "." - }, - { - "name": "Deno project", - "path": "./src/deno" - } - ], - "settings": { - "deno.suggest.imports.hosts": { - "https://deno.land": true - } - } -} diff --git a/aho.ts b/aho.ts deleted file mode 100644 index 6283743..0000000 --- a/aho.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { start } from './deno/index.ts' - -start() diff --git a/cli-node.js b/cli-node.js deleted file mode 100644 index fe524e0..0000000 --- a/cli-node.js +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env node -import { start } from './dist/index.js' - -start() diff --git a/deno/README.md b/deno/README.md deleted file mode 100644 index addb5b7..0000000 --- a/deno/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory is automatically generated by `npm run build-deno`, do not edit. \ No newline at end of file diff --git a/deno/deno/lib.ts b/deno/deno/lib.ts deleted file mode 100644 index 1a22a6f..0000000 --- a/deno/deno/lib.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - resolve as resolvePath, - join as joinPath, -} from 'https://deno.land/std@0.114.0/path/mod.ts' -import { writableStreamFromWriter } from 'https://deno.land/std@0.114.0/streams/mod.ts' -import { USER_AGENT } from '../shared.ts' -import { moveSync as _moveSync } from 'https://deno.land/std@0.114.0/fs/move.ts' -import { emptyDirSync } from 'https://deno.land/std@0.114.0/fs/empty_dir.ts' - -export { resolvePath, emptyDirSync, joinPath } - -export { cac } from 'https://unpkg.com/cac@6.7.12/mod.ts' - -export const moveSync = (from: string, to: string) => { - _moveSync(from, to, { overwrite: true }) -} - -export const fetchJSON = async (url: string) => { - const res = await fetch(url, { - headers: { - 'user-agent': USER_AGENT, - }, - }) - if (!res.ok) { - throw new Error(`failed to fetch ${url}: ${res.status}: ${res.statusText}`) - } - return res.json() -} - -export const downloadFile = async (url: string, outFilePath: string) => { - const res = await fetch(url) - const file = await Deno.open(outFilePath, { write: true, create: true }) - const writableStream = writableStreamFromWriter(file) - await res.body?.pipeTo(writableStream) -} - -export { ensureDirSync } from 'https://deno.land/std@0.114.0/fs/ensure_dir.ts' - -export { existsSync } from 'https://deno.land/std@0.114.0/fs/exists.ts' - -export const isEmptyDirSync = (dir: string) => { - for (const _ of Deno.readDirSync(dir)) { - return false - } - return true -} - -export const getTempDir = () => Deno.makeTempDirSync({ prefix: 'aho_' }) - -export const runCommand = async (cmd: string[]) => { - const p = Deno.run({ - cmd, - stdout: 'piped', - stderr: 'piped', - }) - const [status, output] = await Promise.all([p.status(), p.output()]) - p.close() - - if (!status.success) { - throw new Error(`Command failed with status ${status.code}: ${output}`) - } -} - -export const args = ['deno', 'cli', ...Deno.args] - -export const exit = Deno.exit - -export const getOwnVersion = () => { - const [, version] = /\@([a-z0-9\.]+)/.exec(import.meta.url) || [] - return version || 'unknown' -} diff --git a/deno/shared.ts b/deno/shared.ts deleted file mode 100644 index d7a514c..0000000 --- a/deno/shared.ts +++ /dev/null @@ -1 +0,0 @@ -export const USER_AGENT = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36` diff --git a/package.json b/package.json index e88b726..92d1f3e 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,14 @@ "access": "public" }, "type": "module", - "bin": "./cli-node.js", + "bin": "./dist/cli.js", "files": [ - "dist", - "/cli-node.js" + "dist" ], "scripts": { - "build": "rm -rf dist && tsup src/index.ts --format esm && npm run build-deno", + "build": "rm -rf dist && tsup src/cli.ts --format esm --target node14 --minify", "test": "echo lol", - "prepublishOnly": "npm run build", - "build-deno": "tsno scripts/build-deno.ts" + "prepublishOnly": "npm run build" }, "license": "MIT", "devDependencies": { diff --git a/scripts/build-deno.ts b/scripts/build-deno.ts deleted file mode 100644 index bd96338..0000000 --- a/scripts/build-deno.ts +++ /dev/null @@ -1,21 +0,0 @@ -import fs from 'fs' -import path from 'path' -import glob from 'fast-glob' - -async function main() { - const files = await glob(['src/**/*.ts', '!**/.*', '!**/node'], {}) - await Promise.all( - files.map(async (file) => { - const content = await fs.promises.readFile(file, 'utf-8') - const outFile = file.replace('src', 'deno') - await fs.promises.mkdir(path.dirname(outFile), { recursive: true }) - await fs.promises.writeFile( - outFile, - content.replace('./node/lib', './deno/lib.ts'), - 'utf8', - ) - }), - ) -} - -main() diff --git a/deno/index.ts b/src/cli.ts similarity index 51% rename from deno/index.ts rename to src/cli.ts index 02349c9..5146bca 100644 --- a/deno/index.ts +++ b/src/cli.ts @@ -1,22 +1,18 @@ +#!/usr/bin/env node +import { cac } from 'cac' import { - cac, downloadFile, - ensureDirSync, existsSync, - getTempDir, - isEmptyDirSync, - resolvePath, - runCommand, - fetchJSON, args, exit, getOwnVersion, - moveSync, - emptyDirSync, - joinPath, -} from './deno/lib.ts' + isEmptyDirSync, + resolvePath, + getTempDir, +} from './lib' +import { PrettyError, parseRepo, getDefaultBranchFromApi, extract } from './' -export const start = async () => { +const start = async () => { const cli = cac('aho') cli @@ -25,13 +21,13 @@ export const start = async () => { '-f, --force', `Force override desitination directory even if it's not empty`, ) - .option('-p, --path ', `Only extract a sub path within the repo`) .action(async (_repo, desitination, flags) => { if (!_repo) { throw new PrettyError('No repo provided') } - const { repo, tag } = parseRepo(_repo) + const { owner, repoName, subpath, tag } = parseRepo(_repo) + const repo = `${owner}/${repoName}` desitination = resolvePath(desitination || '.') @@ -45,7 +41,9 @@ export const start = async () => { ) } - console.log(`Downloading ${repo}`) + console.log( + `Downloading ${repo} ${subpath ? `(sub folder: ${subpath})` : ''}`, + ) const ref = tag || (await getDefaultBranchFromApi(repo)) @@ -55,7 +53,7 @@ export const start = async () => { tempTarFile, ) console.log(`Generating to ${desitination}`) - await extract(tempTarFile, desitination, { path: flags.path }) + await extract(tempTarFile, desitination, { path: subpath }) }) cli.version(getOwnVersion()) @@ -74,33 +72,4 @@ export const start = async () => { } } -class PrettyError extends Error { - constructor(message: string) { - super(message) - this.name = this.constructor.name - if (typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, this.constructor) - } else { - this.stack = new Error(message).stack - } - } -} - -async function extract(from: string, to: string, { path }: { path?: string }) { - const tempDir = getTempDir() + `/aho_temp_${Date.now()}` - ensureDirSync(tempDir) - const cmd = ['tar', 'xvzf', from, '-C', tempDir, '--strip-components', '1'] - await runCommand(cmd) - emptyDirSync(to) - moveSync(path ? joinPath(tempDir, path) : tempDir, to) -} - -function parseRepo(input: string) { - const [repo, tag] = input.split('#') - return { repo, tag } -} - -async function getDefaultBranchFromApi(repo: string): Promise { - const data = await fetchJSON(`https://api.github.com/repos/${repo}`) - return data.default_branch -} +start() diff --git a/src/deno/.vscode/settings.json b/src/deno/.vscode/settings.json deleted file mode 100644 index cbac569..0000000 --- a/src/deno/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "deno.enable": true -} diff --git a/src/deno/lib.ts b/src/deno/lib.ts deleted file mode 100644 index 1a22a6f..0000000 --- a/src/deno/lib.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - resolve as resolvePath, - join as joinPath, -} from 'https://deno.land/std@0.114.0/path/mod.ts' -import { writableStreamFromWriter } from 'https://deno.land/std@0.114.0/streams/mod.ts' -import { USER_AGENT } from '../shared.ts' -import { moveSync as _moveSync } from 'https://deno.land/std@0.114.0/fs/move.ts' -import { emptyDirSync } from 'https://deno.land/std@0.114.0/fs/empty_dir.ts' - -export { resolvePath, emptyDirSync, joinPath } - -export { cac } from 'https://unpkg.com/cac@6.7.12/mod.ts' - -export const moveSync = (from: string, to: string) => { - _moveSync(from, to, { overwrite: true }) -} - -export const fetchJSON = async (url: string) => { - const res = await fetch(url, { - headers: { - 'user-agent': USER_AGENT, - }, - }) - if (!res.ok) { - throw new Error(`failed to fetch ${url}: ${res.status}: ${res.statusText}`) - } - return res.json() -} - -export const downloadFile = async (url: string, outFilePath: string) => { - const res = await fetch(url) - const file = await Deno.open(outFilePath, { write: true, create: true }) - const writableStream = writableStreamFromWriter(file) - await res.body?.pipeTo(writableStream) -} - -export { ensureDirSync } from 'https://deno.land/std@0.114.0/fs/ensure_dir.ts' - -export { existsSync } from 'https://deno.land/std@0.114.0/fs/exists.ts' - -export const isEmptyDirSync = (dir: string) => { - for (const _ of Deno.readDirSync(dir)) { - return false - } - return true -} - -export const getTempDir = () => Deno.makeTempDirSync({ prefix: 'aho_' }) - -export const runCommand = async (cmd: string[]) => { - const p = Deno.run({ - cmd, - stdout: 'piped', - stderr: 'piped', - }) - const [status, output] = await Promise.all([p.status(), p.output()]) - p.close() - - if (!status.success) { - throw new Error(`Command failed with status ${status.code}: ${output}`) - } -} - -export const args = ['deno', 'cli', ...Deno.args] - -export const exit = Deno.exit - -export const getOwnVersion = () => { - const [, version] = /\@([a-z0-9\.]+)/.exec(import.meta.url) || [] - return version || 'unknown' -} diff --git a/src/index.ts b/src/index.ts index 346f6e7..fd061a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,80 +1,14 @@ import { - cac, - downloadFile, ensureDirSync, - existsSync, getTempDir, - isEmptyDirSync, - resolvePath, runCommand, fetchJSON, - args, - exit, - getOwnVersion, moveSync, emptyDirSync, joinPath, -} from './node/lib' +} from './lib' -export const start = async () => { - const cli = cac('aho') - - cli - .command('[repo] [desitination]', 'Download a repo') - .option( - '-f, --force', - `Force override desitination directory even if it's not empty`, - ) - .option('-p, --path ', `Only extract a sub path within the repo`) - .action(async (_repo, desitination, flags) => { - if (!_repo) { - throw new PrettyError('No repo provided') - } - - const { repo, tag } = parseRepo(_repo) - - desitination = resolvePath(desitination || '.') - - if ( - !flags.force && - existsSync(desitination) && - !isEmptyDirSync(desitination) - ) { - throw new PrettyError( - `Destination directory is not empty, use --force if you are sure about this`, - ) - } - - console.log(`Downloading ${repo}`) - - const ref = tag || (await getDefaultBranchFromApi(repo)) - - const tempTarFile = `${getTempDir()}/aho_${Date.now()}.tar.gz` - await downloadFile( - `https://codeload.github.com/${repo}/tar.gz/refs/heads/${ref}`, - tempTarFile, - ) - console.log(`Generating to ${desitination}`) - await extract(tempTarFile, desitination, { path: flags.path }) - }) - - cli.version(getOwnVersion()) - cli.help() - cli.parse(args, { run: false }) - - try { - await cli.runMatchedCommand() - } catch (error) { - if (error instanceof PrettyError) { - console.error(error.message) - } else { - console.error(error) - } - exit(1) - } -} - -class PrettyError extends Error { +export class PrettyError extends Error { constructor(message: string) { super(message) this.name = this.constructor.name @@ -86,7 +20,11 @@ class PrettyError extends Error { } } -async function extract(from: string, to: string, { path }: { path?: string }) { +export async function extract( + from: string, + to: string, + { path }: { path?: string }, +) { const tempDir = getTempDir() + `/aho_temp_${Date.now()}` ensureDirSync(tempDir) const cmd = ['tar', 'xvzf', from, '-C', tempDir, '--strip-components', '1'] @@ -95,12 +33,13 @@ async function extract(from: string, to: string, { path }: { path?: string }) { moveSync(path ? joinPath(tempDir, path) : tempDir, to) } -function parseRepo(input: string) { - const [repo, tag] = input.split('#') - return { repo, tag } +export function parseRepo(input: string) { + const [_repo, tag] = input.split('#') + const [, owner, repoName, subpath = ''] = /^(\w+)\/(\w+)(\/.+)?$/.exec(_repo)! + return { owner, repoName, subpath: subpath.slice(1), tag } } -async function getDefaultBranchFromApi(repo: string): Promise { +export async function getDefaultBranchFromApi(repo: string): Promise { const data = await fetchJSON(`https://api.github.com/repos/${repo}`) return data.default_branch } diff --git a/src/node/lib.ts b/src/lib.ts similarity index 91% rename from src/node/lib.ts rename to src/lib.ts index 4fd246f..aebae1e 100644 --- a/src/node/lib.ts +++ b/src/lib.ts @@ -3,10 +3,7 @@ import { tmpdir } from 'os' import { resolve, join } from 'path' import https from 'https' import fs from 'fs' -import { version } from '../../package.json' -import { USER_AGENT } from '../shared' - -export { cac } from 'cac' +import { version } from '../package.json' export const fetchJSON = (url: string): Promise => new Promise((resolve, reject) => { @@ -101,3 +98,5 @@ export const emptyDirSync = (path: string) => { fs.rmSync(path, { recursive: true, force: true }) ensureDirSync(path) } + +export const USER_AGENT = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36` diff --git a/src/shared.ts b/src/shared.ts deleted file mode 100644 index d7a514c..0000000 --- a/src/shared.ts +++ /dev/null @@ -1 +0,0 @@ -export const USER_AGENT = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36`