From 72fbe2bb2d783c66b446a9d579896faafb7f9bda Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 23 Jun 2026 07:07:19 +0000 Subject: [PATCH] core: cli: allow interactive mode --- packages/hydrooj/package.json | 2 ++ packages/hydrooj/src/entry/cli.ts | 43 ++++++++++++++++++++++++++++--- packages/hydrooj/src/loader.ts | 2 +- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index a28a1fd8d..666a4637f 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -49,6 +49,7 @@ "sanitize-filename": "^1.6.4", "schemastery": "^3.18.0", "semver": "^7.8.5", + "shell-quote": "^1.8.4", "shorty.js": "^1.0.1", "superagent": "^10.3.0", "superagent-proxy": "^3.0.0", @@ -66,6 +67,7 @@ "@types/mongodb-uri": "^0.9.4", "@types/nodemailer": "^8.0.1", "@types/semver": "^7.7.1", + "@types/shell-quote": "^1.7.5", "@types/superagent": "^8.1.10", "@types/superagent-proxy": "^3.0.4", "@types/ua-parser-js": "^0.7.39", diff --git a/packages/hydrooj/src/entry/cli.ts b/packages/hydrooj/src/entry/cli.ts index a13c6201f..69998fd5e 100644 --- a/packages/hydrooj/src/entry/cli.ts +++ b/packages/hydrooj/src/entry/cli.ts @@ -1,8 +1,10 @@ import os from 'os'; import path from 'path'; +import readline from 'readline/promises'; import cac from 'cac'; import fs from 'fs-extra'; import { ObjectId } from 'mongodb'; +import { parse, type ParseEntry } from 'shell-quote'; import { Context } from '../context'; import SystemModel from '../model/system'; import { load as loadOptions } from '../options'; @@ -32,8 +34,20 @@ async function runScript(name: string, arg: any) { return await s.run(arg, console.info); } -async function cli() { - const [, modelName, func, ...args] = argv.args as [string, string, string, ...any[]]; +function checkStringArray(args: ParseEntry[]): args is string[] { + return args.every((arg) => typeof arg === 'string'); +} + +function parseInteractiveArgv(line: string) { + const args = parse(line); + if (!checkStringArray(args)) throw new Error('Shell operators are not supported in interactive mode.'); + if (args[0] === 'hydrooj') args.shift(); + if (args[0] === 'cli') args.shift(); + return cac().parse(['node', 'hydrooj', 'cli', ...args], { run: false }); +} + +async function cli(commandArgv = argv) { + const [, modelName, func, ...args] = commandArgv.args as [string, string, string, ...any[]]; if (modelName === 'execute') { try { // eslint-disable-next-line no-eval @@ -84,7 +98,7 @@ async function cli() { } else if ((+args[i]).toString() === args[i]) { args[i] = +args[i]; } else if (args[i].startsWith('~')) { - args[i] = argv.options[args[i].substr(1)]; + args[i] = commandArgv.options[args[i].substr(1)]; } else if ((args[i].startsWith('[') && args[i].endsWith(']')) || (args[i].startsWith('{') && args[i].endsWith('}'))) { try { args[i] = JSON.parse(args[i]); @@ -137,5 +151,26 @@ export async function load(ctx: Context) { const scriptDir = path.resolve(__dirname, '..', 'script'); await Promise.all((await fs.readdir(scriptDir)) .map((h) => ctx.loader.reloadPlugin(path.resolve(scriptDir, h), ''))); - await cli(); + if (argv.args.length > 1) { + await cli(); + return; + } + const rl = readline.createInterface(process.stdin, process.stdout); + console.log('Hydro CLI interactive mode. Type `exit` or press Ctrl+D to exit.'); + try { + while (true) { + const line = await rl.question('hydrooj> '); + if (!line.trim()) continue; + if (['exit', 'quit'].includes(line.trim())) break; + try { + await cli(parseInteractiveArgv(line)); + } catch (e) { + console.error(e.message); + } + } + } catch (e) { + if (e?.code !== 'ERR_USE_AFTER_CLOSE') console.error(e); + } finally { + rl.close(); + } } diff --git a/packages/hydrooj/src/loader.ts b/packages/hydrooj/src/loader.ts index 49d56b043..091427a33 100644 --- a/packages/hydrooj/src/loader.ts +++ b/packages/hydrooj/src/loader.ts @@ -7,7 +7,6 @@ import child from 'child_process'; // eslint-disable-next-line import/no-duplicates import './utils'; import cac from 'cac'; -import './ui'; import { I18nService } from './lib/i18n'; import { Logger } from './logger'; @@ -205,6 +204,7 @@ async function preload() { } export async function load() { + require('./ui'); await preload(); Error.stackTraceLimit = 50; try {