diff --git a/cli/commands/bump.ts b/cli/commands/bump.ts new file mode 100644 index 0000000..43050e9 --- /dev/null +++ b/cli/commands/bump.ts @@ -0,0 +1,38 @@ +import { Colors } from "../deps.ts"; + +import { getMeta, setMeta } from "../global.ts"; + +export default async function bump( + options: any, + type: "minor" | "major" | "patch" +) { + const meta = await getMeta(); + + let [major = 0, minor = 0, patch = 0] = meta.version.split(".").map(Number); + + switch (type) { + case "major": + major++; + minor = 0; + patch = 0; + break; + case "minor": + minor++; + patch = 0; + break; + case "patch": + patch++; + break; + default: + throw new Error("type must be either major, minor or patch"); + } + const newVersion = [major, minor, patch].join("."); + console.log( + "Bumping version from", + Colors.blue(meta.version), + "to", + Colors.blue(newVersion) + ); + meta.version = newVersion; + await setMeta(meta); +} diff --git a/cli/commands/init.ts b/cli/commands/init.ts new file mode 100644 index 0000000..0fd824c --- /dev/null +++ b/cli/commands/init.ts @@ -0,0 +1,41 @@ +import { Cliffy, Path, FS, Compress, Base64 } from "../deps.ts"; +import { + getMeta, + setMeta, + IMeta, + getConfig, + isInteractive, +} from "../global.ts"; + +export default async function init() { + let existing = {}; + try { + existing = await getMeta(); + } catch (err) {} + let meta: IMeta = { + name: Path.basename(Deno.cwd()).toLowerCase().replace(/\s+/g, "-"), + version: "0.0.1", + description: "", + author: getConfig("author"), + contributors: [], + files: ["**/*.ts", "**/*.js", "importmap.json"], + ...existing, + }; + + if (isInteractive()) { + meta.name = await Cliffy.Input.prompt({ + message: "What's the name of your package?", + default: meta.name, + }); + meta.description = await Cliffy.Input.prompt({ + message: "What's the description of your package?", + default: meta.description, + }); + meta.author = await Cliffy.Input.prompt({ + message: "Who's the author of your package?", + default: meta.author, + }); + } + + await setMeta(meta); +} diff --git a/cli/commands/publish.ts b/cli/commands/publish.ts new file mode 100644 index 0000000..429475f --- /dev/null +++ b/cli/commands/publish.ts @@ -0,0 +1,74 @@ +import { Colors, Path, FS, Compress, Base64 } from "../deps.ts"; +import { getMeta, IMeta, log, getConfig } from "../global.ts"; + +export default async function publish() { + const meta: IMeta = await getMeta(); + + if (!meta.name) throw new Error("name is not set in meta.json"); + if (!meta.version) throw new Error("version is not set in meta.json"); + if (!meta.files || !Array.isArray(meta.files) || meta.files.length <= 0) + throw new Error("files is not set or empty in meta.json"); + + const tmpDir = await Deno.makeTempDir(); + const packedFile = await Deno.makeTempFile(); + + try { + const walker = FS.walk(".", { + includeDirs: false, + includeFiles: true, + match: meta.files.map((file) => Path.globToRegExp(file)), + }); + + log("Copying files to package to", tmpDir); + + const copy = async (path: string) => { + const dest = Path.join(tmpDir, path); + await FS.ensureDir(Path.dirname(dest)); + await FS.copy(path, dest); + }; + + await copy("meta.json"); + + for await (const file of walker) { + await copy(file.path); + } + + log("Compressing file"); + + await Compress.Tar.compress(tmpDir, packedFile, { + excludeSrc: true, + }); + + const url = new URL(getConfig("registry")); + url.pathname = "/api/package/" + meta.name; + + log("Uploading new package version"); + + const res = await fetch(url, { + method: "POST", + body: await Deno.readFile(packedFile), + headers: { + Authorization: + "Basic " + + Base64.encode( + getConfig("username") + ":" + getConfig("password") + ), + }, + }).then((res) => (res.status === 200 ? res.json() : res.statusText)); + + log("Upload finished. Result:", res); + + if (typeof res === "string" || res.error) { + console.log( + Colors.red("Error: " + (typeof res == "string" ? res : res.error)) + ); + } else { + if (res.success) { + console.log(Colors.green("Upload successfull")); + } + } + } finally { + await Deno.remove(tmpDir, { recursive: true }); + await Deno.remove(packedFile); + } +} diff --git a/cli/commands/setup.ts b/cli/commands/setup.ts new file mode 100644 index 0000000..ca38b4e --- /dev/null +++ b/cli/commands/setup.ts @@ -0,0 +1,29 @@ +import { Cliffy } from "../deps.ts"; + +import { getConfig, setConfig } from "../global.ts"; + +export default async function setup() { + const registry = await Cliffy.Input.prompt({ + message: "What's your registry?", + default: getConfig("registry"), + }); + const username = await Cliffy.Input.prompt({ + message: "What's your username?", + default: getConfig("username"), + }); + const password = await Cliffy.Secret.prompt({ + message: "What's your password?", + hidden: true, + default: getConfig("password"), + }); + + const author = await Cliffy.Input.prompt({ + message: "Who are you? (optional) Name ", + default: getConfig("author"), + }); + + await setConfig("registry", registry); + await setConfig("username", username); + await setConfig("password", password); + await setConfig("author", author); +} diff --git a/cli/denreg.ts b/cli/denreg.ts new file mode 100644 index 0000000..af83ec8 --- /dev/null +++ b/cli/denreg.ts @@ -0,0 +1,297 @@ +import { Ini, Cliffy, Compress, Base64 } from "./deps.ts"; +import * as Colors from "https://deno.land/std@0.62.0/fmt/colors.ts"; +import * as Path from "https://deno.land/std@0.62.0/path/mod.ts"; +import * as FS from "https://deno.land/std@0.62.0/fs/mod.ts"; +import { init } from "./global.ts"; + +import setupCMD from "./commands/setup.ts"; +import initCMD from "./commands/init.ts"; +import bumpCMD from "./commands/bump.ts"; +import publishCMD from "./commands/publish.ts"; + +const HOME_FOLDER = Deno.env.get("HOME") || Deno.env.get("USERPROFILE") || ""; + +type CommandHandler = (...opts: any[]) => void | Promise; + +let command: CommandHandler | undefined = undefined; +let opts: any[] = []; +// const debounce = (fnc: any) => (...args: any[]) => { +// new Promise((y) => setTimeout(y, 0)).then(() => fnc(...args)); +// }; //Put function call into queue + +const commandWrapper = (cmd: CommandHandler) => { + return (...params: any[]) => { + command = cmd; + opts = params; + }; +}; + +const flags = await new Cliffy.Command() + .name("denreg") + .version("0.0.1") + .description("CLI for the Open Source DenReg package registry") + .option("-i, --interactive [interactive:boolean]", "Interactive mode", { + default: true, + global: true, + }) + .option("-c, --config ", "Config file", { + default: Path.resolve(HOME_FOLDER, ".denreg"), + global: true, + }) + .option("-v, --verbose", "Verbose", { + default: false, + global: true, + }) + .command( + "setup", + new Cliffy.Command() + .description("Configure cli") + .action(commandWrapper(setupCMD)) + ) + .command( + "publish", + new Cliffy.Command() + .description("Upload package") + .action(commandWrapper(publishCMD)) + ) + .command( + "init", + new Cliffy.Command() + .description("Create meta.json") + .action(commandWrapper(initCMD)) + ) + .command( + "bump", + new Cliffy.Command() + .complete("major|minor|patch", () => ["major", "minor", "patch"]) + .arguments("") + .description("Change package version") + .action(commandWrapper(bumpCMD)) + ) + .command("completions", new Cliffy.CompletionsCommand()) + .parse(Deno.args); + +await init(flags.options); + +if (command) { + await Promise.resolve((command as CommandHandler)(...opts)); +} + +// function log(...args: any[]) { +// if (flags.options.verbose) console.log(...args); +// } + +// const CONFIG_LOCATION = flags.options.config; + +// function loadConfigSync() { +// try { +// const data = Deno.readFileSync(CONFIG_LOCATION); +// return Ini.decode(new TextDecoder().decode(data)); +// } catch (err) { +// return {}; +// } +// } + +// const config = loadConfigSync(); + +// const { username, password, registry } = config; + +// async function setConfig(name: string, value: string) { +// config[name] = value; + +// const data = Ini.encode(config); + +// await Deno.writeFile(CONFIG_LOCATION, new TextEncoder().encode(data), { +// create: true, +// }); +// } + +// async function setup() { +// const registry = await Cliffy.Input.prompt({ +// message: "What's your registry?", +// default: config.registry, +// }); +// const username = await Cliffy.Input.prompt({ +// message: "What's your username?", +// default: config.username, +// }); +// const password = await Cliffy.Secret.prompt({ +// message: "What's your password?", +// hidden: true, +// default: config.password, +// }); + +// const author = await Cliffy.Input.prompt({ +// message: "Who are you? (optional) Name ", +// default: config.author, +// }); + +// await setConfig("registry", registry); +// await setConfig("username", username); +// await setConfig("password", password); +// await setConfig("author", author); +// } + +// interface IMeta { +// name: string; +// version: string; +// description?: string; +// author?: string; +// contributors?: string[]; +// files: string[]; +// } + +// async function init() { +// let existing = {}; +// try { +// existing = await _getMeta(); +// } catch (err) {} +// let meta: IMeta = { +// name: Path.basename(Deno.cwd()).toLowerCase().replace(/\s+/g, "-"), +// version: "0.0.1", +// description: "", +// author: config.author, +// contributors: [], +// files: ["**/*.ts", "**/*.js", "importmap.json"], +// ...existing, +// }; + +// if (flags.options.interactive) { +// meta.name = await Cliffy.Input.prompt({ +// message: "What's the name of your package?", +// default: meta.name, +// }); +// meta.description = await Cliffy.Input.prompt({ +// message: "What's the description of your package?", +// default: meta.description, +// }); +// meta.author = await Cliffy.Input.prompt({ +// message: "Who's the author of your package?", +// default: meta.author, +// }); +// } + +// await _setMeta(meta); +// } + +// async function bump(options: any, type: "minor" | "major" | "patch") { +// const meta = await _getMeta(); + +// let [major = 0, minor = 0, patch = 0] = meta.version.split(".").map(Number); + +// switch (type) { +// case "major": +// major++; +// break; +// case "minor": +// minor++; +// break; +// case "patch": +// patch++; +// break; +// default: +// throw new Error("type must be either major, minor or patch"); +// } +// const newVersion = [major, minor, patch].join("."); +// console.log( +// "Bumping version from", +// Colors.blue(meta.version), +// "to", +// Colors.blue(newVersion) +// ); +// meta.version = newVersion; +// await _setMeta(meta); +// } + +// async function uploadPackage() { +// const meta: IMeta = await _getMeta(); + +// if (!meta.name) throw new Error("name is not set in meta.json"); +// if (!meta.version) throw new Error("version is not set in meta.json"); +// if (!meta.files || !Array.isArray(meta.files) || meta.files.length <= 0) +// throw new Error("files is not set or empty in meta.json"); + +// const tmpDir = await Deno.makeTempDir(); +// const packedFile = await Deno.makeTempFile(); + +// try { +// const walker = FS.walk(".", { +// includeDirs: false, +// includeFiles: true, +// match: meta.files.map((file) => Path.globToRegExp(file)), +// }); + +// log("Copying files to package to", tmpDir); + +// const copy = async (path: string) => { +// const dest = Path.join(tmpDir, path); +// await FS.ensureDir(Path.dirname(dest)); +// await FS.copy(path, dest); +// }; + +// await copy("meta.json"); + +// for await (const file of walker) { +// await copy(file.path); +// } + +// log("Compressing file"); + +// await Compress.Tar.compress(tmpDir, packedFile, { +// excludeSrc: true, +// }); + +// const url = new URL(config.registry); +// url.pathname = "/api/package/" + meta.name; + +// log("Uploading new package version"); + +// const res = await fetch(url, { +// method: "POST", +// body: await Deno.readFile(packedFile), +// headers: { +// Authorization: +// "Basic " + +// Base64.encode(config.username + ":" + config.password), +// }, +// }).then((res) => (res.status === 200 ? res.json() : res.statusText)); + +// log("Upload finished. Result:", res); + +// if (typeof res === "string" || res.error) { +// console.log( +// Colors.red("Error: " + (typeof res == "string" ? res : res.error)) +// ); +// } else { +// if (res.success) { +// console.log(Colors.green("Upload successfull")); +// } +// } +// } finally { +// await Deno.remove(tmpDir, { recursive: true }); +// await Deno.remove(packedFile); +// } +// } + +// async function _getMeta(): Promise { +// log("Reading meta.json"); +// return (await FS.readJson("meta.json")) as IMeta; +// } + +// async function _setMeta(meta: IMeta): Promise { +// log("Saving meta.json"); +// return FS.writeJson("meta.json", meta, { +// spaces: " ", +// }); +// } + +// if (!username || !password || !registry) { +// if (!flags.options.interactive) { +// console.error( +// Colors.red("Run setup or set necessary value in " + CONFIG_LOCATION) +// ); +// } else { +// log("Running setup"); +// await setup(); +// } +// } diff --git a/cli/deps.ts b/cli/deps.ts new file mode 100644 index 0000000..064150f --- /dev/null +++ b/cli/deps.ts @@ -0,0 +1,7 @@ +export * as Compress from "https://git.stamm.me/Deno/DenReg/raw/branch/master/tar/mod.ts"; +export * as Ini from "https://deno.land/x/ini/mod.ts"; +export * as Base64 from "https://deno.land/std@0.62.0/encoding/base64.ts"; +export * as Cliffy from "https://deno.land/x/cliffy/mod.ts"; +export * as FS from "https://deno.land/std@0.62.0/fs/mod.ts"; +export * as Colors from "https://deno.land/std@0.62.0/fmt/colors.ts"; +export * as Path from "https://deno.land/std@0.62.0/path/mod.ts"; diff --git a/cli/global.ts b/cli/global.ts new file mode 100644 index 0000000..980b7d3 --- /dev/null +++ b/cli/global.ts @@ -0,0 +1,81 @@ +import { Ini, Cliffy, Compress, Base64, FS, Colors } from "./deps.ts"; +import setupCMD from "./commands/setup.ts"; + +export interface IMeta { + name: string; + version: string; + description?: string; + author?: string; + contributors?: string[]; + files: string[]; +} + +let verbose = false; + +export function log(...args: any) { + if (verbose) console.log(...args); +} + +let config: any = {}; +let configLocation = ""; +let configInitialized = false; + +export function getConfig(name: string) { + if (!configInitialized) throw new Error("Not initialized!"); + return config[name]; +} + +export async function setConfig(name: string, value: string) { + if (!configInitialized) throw new Error("Not initialized!"); + config[name] = value; + + const data = Ini.encode(config); + + await Deno.writeFile(configLocation, new TextEncoder().encode(data), { + create: true, + }); +} + +export async function getMeta() { + log("Reading meta.json"); + return (await FS.readJson("meta.json")) as IMeta; +} + +export async function setMeta(meta: IMeta): Promise { + log("Saving meta.json"); + return FS.writeJson("meta.json", meta, { + spaces: " ", + }); +} + +let interactive = true; + +export function isInteractive() { + return interactive; +} + +export async function init(globalOptions: any) { + configLocation = globalOptions.config; + interactive = globalOptions.interactive; + verbose = globalOptions.verbose; + + try { + const data = Deno.readFileSync(configLocation); + config = Ini.decode(new TextDecoder().decode(data)); + } catch (err) { + log("Error loading config:"); + log(err); + } + + configInitialized = true; + + if (!config.username || !config.registry || !config.password) { + if (!isInteractive()) { + console.error( + Colors.red("Run setup or set necessary value in " + configLocation) + ); + } else { + await setupCMD(); + } + } +} diff --git a/cli/meta.json b/cli/meta.json new file mode 100644 index 0000000..44a2651 --- /dev/null +++ b/cli/meta.json @@ -0,0 +1,11 @@ +{ + "name": "@denreg-cli", + "version": "0.1.2", + "description": "CLI for the DenReg package registry", + "author": "Fabian Stamm ", + "contributors": [], + "files": [ + "**/*.ts", + "**/*.js" + ] +} \ No newline at end of file