UNPKG

@magicdawn/x-args

Version:

play with cli commands like a composer

294 lines (287 loc) 9.75 kB
// src/util/BaseCommand.ts import { Command, Option } from "clipanion"; var BaseCommand = class extends Command { /** * glob */ files = Option.String("-f,--files", { required: true, description: "files as input" }); ignoreCase = Option.Boolean("--ignore-case", true, { description: "ignore case for -f,--files, default true" }); globCwd = Option.String("--glob-cwd", { description: "cwd used in glob" }); // for safty yes = Option.Boolean("-y,--yes", false, { description: "exec commands, default false(only preview commands, aka dry run)" }); // for tokens showTokens = Option.Boolean("-t,--tokens,--show-tokens", false, { description: "show available tokens" }); }; // src/util/file.ts import path from "path"; import chalk from "chalk"; function getFilenameTokens(item) { const fullpath = path.resolve(item); const dir = path.dirname(fullpath); const file = path.basename(fullpath); let ext = path.extname(fullpath); const name = path.basename(fullpath, ext); ext = ext.slice(1); const pdir = path.basename(dir); const rname = item; return { fullpath, dir, file, name, ext, pdir, rname }; } function renderFilenameTokens(template, options) { const tokens = [ // this should be process first ":pdir", ":rname", // ":fullpath", ":dir", ":file", ":name", ":ext" ]; let result = template; for (const t of tokens) { const val = options[t.slice(1)]; if (!val) continue; result = result.replaceAll(new RegExp(t, "g"), val); } return result; } function printFilenameTokens(tokens) { const { fullpath, dir, file, name, ext, pdir, rname } = tokens; console.log(` token ${chalk.green(":fullpath")} ${chalk.cyan(fullpath)}`); console.log(` token ${chalk.green(":dir")} ${chalk.cyan(dir)}`); console.log(` token ${chalk.green(":file")} ${chalk.cyan(file)}`); console.log(` token ${chalk.green(":name")} ${chalk.cyan(name)}`); console.log(` token ${chalk.green(":ext")} ${chalk.cyan(ext)}`); console.log(` token ${chalk.green(":pdir")} ${chalk.cyan(pdir)}`); console.log(` token ${chalk.green(":rname")} ${chalk.cyan(rname)}`); } // src/commands/txt.ts import { execSync } from "child_process"; import path2 from "path"; import { subscribe } from "@parcel/watcher"; import chalk2 from "chalk"; import { Command as Command2, Option as Option2 } from "clipanion"; import Emittery from "emittery"; import { delay, once } from "es-toolkit"; import ms from "ms"; import { escapeShellArg } from "needle-kit"; import superjson from "superjson"; import { z } from "zod"; // src/libs.ts import { default as default2 } from "boxen"; import { default as default3 } from "fs-extra"; // src/util/parse-line.ts function parseLineToArgs(line) { const result = []; const regex = /'([^']*)'|"([^"]*)"|\S+/g; let match; while ((match = regex.exec(line)) !== null) { if (match[1] !== void 0) { result.push(match[1]); } else if (match[2] !== void 0) { result.push(match[2]); } else { result.push(match[0]); } } return result; } // src/commands/txt.ts function inspectArray(arr) { return arr.map((x) => `\`${x.toString()}\``).join(" | "); } var TxtCommand = class extends Command2 { static paths = [["txt"]]; static usage = { description: "xargs txt <txt-file>, use `:line` as placeholder of a line of txt file, use (`:arg0` or `:args0`) ... to replace a single value" }; txt = Option2.String({ name: "txt", required: true }); command = Option2.String("-c,--command", { required: true, description: "the command to execute" }); // argsSplit = Option.String('-s,--split,--args-split', defaultTxtCommandArgs.argsSplit.toString(), { // description: `char to split a line, type: regex or string; default: ${defaultTxtCommandArgs.argsSplit.toString()};`, // }) // for safty yes = Option2.Boolean("-y,--yes", false, { description: "exec commands, default false(only preview commands, aka dry run)" }); wait = Option2.Boolean("-w,--wait", false, { description: "wait new items when queue empty" }); waitTimeout = Option2.String("--wait-timeout,--WT", { description: "wait timeout, will pass to ms()" }); session = Option2.String("--session", "continue" /* Continue */, { description: `session handling: default \`${"continue" /* Continue */}\`; allowed values: ${inspectArray(Object.values(SessionControl))};` }); execute() { return startTxtCommand({ ...this, session: z.nativeEnum(SessionControl).parse(this.session) }); } }; var SessionControl = /* @__PURE__ */ ((SessionControl2) => { SessionControl2["Start"] = "start"; SessionControl2["ReStart"] = "restart"; SessionControl2["Continue"] = "continue"; return SessionControl2; })(SessionControl || {}); var defaultTxtCommandArgs = { session: "continue" /* Continue */ }; var lognsp = "x-args:txt-command"; async function startTxtCommand(args) { const { txt, command, wait, waitTimeout, yes } = args; const txtFile = path2.resolve(txt); console.log(""); console.log(`${chalk2.green("[x-args]")}: received`); console.log(` ${chalk2.cyan("txt file")}: ${chalk2.yellow(txtFile)}`); console.log(` ${chalk2.cyan("command")}: ${chalk2.yellow(command)}`); console.log(""); const sessionControl = args.session; const sessionFile = path2.join(path2.dirname(txtFile), `.x-args-session.${path2.basename(txtFile)}`); let processed = /* @__PURE__ */ new Set(); if (sessionControl === "start" /* Start */) { if (default3.existsSync(sessionFile) && default3.readFileSync(sessionFile, "utf-8").length) { console.error(`session already exists, use \`${"continue" /* Continue */}\` or \`${"restart" /* ReStart */}\``); process.exit(1); } } else if (sessionControl === "restart" /* ReStart */) { if (default3.existsSync(sessionFile)) { default3.removeSync(sessionFile); } } else if (sessionControl === "continue" /* Continue */ && default3.existsSync(sessionFile)) { const content = default3.readFileSync(sessionFile, "utf-8"); if (content) { let _processed; try { const parsed = superjson.parse(content); _processed = parsed.processed; } catch { } if (_processed) { processed = new Set(_processed); console.info(`${chalk2.green(`[${lognsp}:session]`)} loaded from file %s`, sessionFile); } } } function saveProcessed() { default3.outputFileSync(sessionFile, superjson.stringify({ processed })); } function getTxtNextLine() { const content = default3.readFileSync(txtFile, "utf8"); const lines = content.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !(line.startsWith("//") || line.startsWith("#"))).filter((line) => !processed.has(line)); if (lines.length) return lines[0]; } function getLineThenRunCommand() { let worked = false; let line; while (line = getTxtNextLine()) { worked = true; const splitedArgs = parseLineToArgs(line); const cmd = command.replaceAll(/:rawLine/gi, line).replaceAll(/:line/gi, escapeShellArg(line)).replaceAll(/:rawArgs?(\d)/gi, (match, index) => splitedArgs[index] || "").replaceAll(/:args?(\d)/gi, (match, index) => splitedArgs[index] ? escapeShellArg(splitedArgs[index]) : ""); console.log(""); console.log( default2( [ // `${chalk2.green(" line =>")} ${chalk2.yellow(line.padEnd(70, " "))}`, `${chalk2.green(" cmd =>")} ${chalk2.yellow(cmd)}` ].join("\n"), { borderColor: "green", title: chalk2.green(`${lognsp}:line`) } ) ); if (yes) { execSync(cmd, { stdio: "inherit" }); } processed.add(line); if (yes) { saveProcessed(); setProgramExitTs(); } } return worked; } const waitTimeoutMs = waitTimeout ? ms(waitTimeout) : 0; if (Number.isNaN(waitTimeoutMs)) { throw new TypeError("unrecognized --wait-timeout format, pls check https://npm.im/ms"); } let exitTs = Infinity; function setProgramExitTs() { if (waitTimeout) { exitTs = Date.now() + waitTimeoutMs; } } getLineThenRunCommand(); if (wait) { let waitChanged2 = function() { return Promise.race( [emitter.once(["default", "error"]), waitTimeoutMs ? delay(waitTimeoutMs + 1e3) : void 0].filter(Boolean) ); }, printNoNewItems2 = function() { console.log(); console.info(`${chalk2.green(`[${lognsp}:wait]`)} no new items, waiting for changes ...`); console.log(); }; var waitChanged = waitChanged2, printNoNewItems = printNoNewItems2; const emitter = new Emittery(); const subscription = await subscribe(path2.dirname(txtFile), (error, events) => { if (error) { return emitter.emit("error", error); } if (events.some((item) => item.path === txtFile)) { return emitter.emit("default", events); } }); const unsubscribe = once(() => subscription.unsubscribe()); process.on("SIGINT", unsubscribe); process.on("SIGTERM", unsubscribe); process.on("exit", unsubscribe); let prevWorked = true; while (Date.now() <= exitTs) { if (prevWorked) { const hasNewLine = !!getTxtNextLine(); if (!hasNewLine) printNoNewItems2(); } await waitChanged2(); prevWorked = getLineThenRunCommand(); } unsubscribe(); } } export { BaseCommand, getFilenameTokens, renderFilenameTokens, printFilenameTokens, TxtCommand, SessionControl, defaultTxtCommandArgs, startTxtCommand };