core: cli: allow interactive mode

This commit is contained in:
undefined committed 2026-06-23 07:07:30 +00:00
1 parent e86fab8387
commit 72fbe2bb2d
3 files changed
+42 -5

No files matched your search

+2
View File
@@ -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",
+39 -4
View File
@@ -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();
}
}
+1 -1
View File
@@ -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 {