@magicdawn/x-args
Version:
play with cli commands like a composer
271 lines (265 loc) • 8.86 kB
JavaScript
// 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 chalk from "chalk";
import path from "path";
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 (let t of tokens) {
const val = options[t.slice(1)];
if (!val) continue;
result = result.replace(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 chalk2 from "chalk";
import { execSync } from "child_process";
import { Command as Command2, Option as Option2 } from "clipanion";
import delay from "delay";
import { isEqual } from "lodash-es";
import CircularBuffer from "mnemonist/circular-buffer.js";
import ms from "ms";
import { escapeShellArg } from "needle-kit";
import path2 from "path";
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/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 = Option2.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() {
let argsSplit = this.argsSplit;
if (argsSplit.startsWith("/") && argsSplit.endsWith("/")) {
argsSplit = new RegExp(argsSplit.slice(1, -1));
}
return startTxtCommand({
...this,
argsSplit,
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 */,
argsSplit: /\s+/
};
var lognsp = "x-args:txt-command";
async function startTxtCommand(args) {
const { txt, command, argsSplit, 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("args split")}: ${chalk2.yellow("`" + argsSplit + "`")}`);
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 */) {
if (default3.existsSync(sessionFile)) {
const content = default3.readFileSync(sessionFile, "utf-8");
if (content) {
let _processed;
try {
const parsed = superjson.parse(content);
_processed = parsed.processed;
} catch (e) {
}
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 line;
while (line = getTxtNextLine()) {
let splitedArgs = line.split(argsSplit);
let cmd = command;
cmd = cmd.replace(/:args?(\d)/gi, (match, index) => {
return splitedArgs[index] ? escapeShellArg(splitedArgs[index]) : "";
});
cmd = cmd.replace(/:line/gi, escapeShellArg(line));
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);
saveProcessed();
setProgramExitTs();
}
}
const waitTimeoutMs = waitTimeout ? ms(waitTimeout) : 0;
if (isNaN(waitTimeoutMs)) {
throw new Error("unrecognized --wait-timeout format, pls check https://npm.im/ms");
}
let exitTs = Infinity;
function setProgramExitTs() {
if (waitTimeout) {
exitTs = Date.now() + waitTimeoutMs;
}
}
getLineThenRunCommand();
if (wait) {
const q = new CircularBuffer(Array, 2);
q.push(true);
while (Date.now() <= exitTs) {
await delay(2e3);
const hasLine = !!getTxtNextLine();
q.push(hasLine);
if (hasLine) {
getLineThenRunCommand();
} else {
const shouldPrint = isEqual(q.toArray(), [true, false]);
if (shouldPrint) {
console.log();
console.info(`${chalk2.green(`[${lognsp}:wait]`)} no new items, waiting for changes ...`);
console.log();
}
}
}
}
}
export {
BaseCommand,
getFilenameTokens,
renderFilenameTokens,
printFilenameTokens,
TxtCommand,
SessionControl,
defaultTxtCommandArgs,
startTxtCommand
};