mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:20:51 +08:00
feat(cli): cover OpenAPI with a remote npm CLI and internal panel tools (#3074)
* feat(cli): add unified Commander CLI for QingLong 2.x * fix(cli): publish via npm and address security review feedback * ci(cli): package npm artifacts and remove evaluation collateral * test(cli): use a fixed shell fixture for log retention * refactor(cli): separate remote npm client from panel tools * feat(cli): cover active panel OpenAPI resources * docs(cli): unify authentication and skill guidance * refactor(cli): isolate internal commands and generate Commander help * refactor(cli): organize remote and internal modules by responsibility * ci(cli): publish verified npm archives from master * fix(cli): publish under the whyour npm scope * ci: use npm trusted publishing for both packages * docs: introduce the published CLI on the project homepage * fix(cli): preserve server log truncation and correct login hints * fix(cli): accept dashboard record request bodies * fix(cli): preserve stdin for local task execution * fix(cli): resolve task executables after changing directory * fix(cli): preserve shell function tasks and sanitize test failures * fix(cli): preserve shell hook state and resolve workdir after hooks * fix(cli): preserve cleanup across shared shell task timeouts * fix(cli): isolate shell control descriptors and reap timed-out descendants
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env node
|
||||
import { translate } from '../shared/i18n/index';
|
||||
import { standaloneHelp } from '../internal/help/standalone';
|
||||
import {
|
||||
withCommandCancellation,
|
||||
interruptedCode,
|
||||
} from '../internal/runtime/cancellation';
|
||||
import { CliError, fail } from '../shared/errors';
|
||||
|
||||
export interface CompatibilityRoute {
|
||||
surface: 'local' | 'subscription';
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export function compatibilityRoute(input: string[]): CompatibilityRoute {
|
||||
const args = input[0] === '-l' ? input.slice(1) : [...input];
|
||||
const [action, ...values] = args;
|
||||
if (action === 'repo' || action === 'raw')
|
||||
return { surface: 'subscription', args };
|
||||
if (action === 'update') {
|
||||
if (
|
||||
values.length > 1 ||
|
||||
(values.length && !['true', 'false'].includes(values[0]!))
|
||||
)
|
||||
fail(translate(process.env, '用法:ql-compat update [true|false]'), 2);
|
||||
return {
|
||||
surface: 'local',
|
||||
args: ['update', ...(values[0] === 'false' ? ['--download-only'] : [])],
|
||||
};
|
||||
}
|
||||
if (action === 'reload') {
|
||||
if (
|
||||
values.length > 1 ||
|
||||
(values.length && !['services', 'system', 'data'].includes(values[0]!))
|
||||
)
|
||||
fail(
|
||||
translate(process.env, '用法:ql-compat reload [services|system|data]'),
|
||||
2,
|
||||
);
|
||||
return {
|
||||
surface: 'local',
|
||||
args: ['reload', '--target', values[0] || 'services'],
|
||||
};
|
||||
}
|
||||
if (
|
||||
action &&
|
||||
[
|
||||
'rmlog',
|
||||
'extra',
|
||||
'bot',
|
||||
'check',
|
||||
'resetlet',
|
||||
'resettfa',
|
||||
'resetpwd',
|
||||
'resetname',
|
||||
].includes(action)
|
||||
) {
|
||||
// A legacy positional value beginning with '-' is data, never a CLI option.
|
||||
return { surface: 'local', args: [action, '--', ...values] };
|
||||
}
|
||||
fail(translate(process.env, '未知旧命令,请运行 ql-compat --help。'), 2);
|
||||
}
|
||||
|
||||
export async function compatibilityMain(
|
||||
args = process.argv.slice(2),
|
||||
): Promise<number> {
|
||||
return withCommandCancellation(async (signal) => {
|
||||
try {
|
||||
if (
|
||||
!args.length ||
|
||||
(args.length === 1 && ['--help', '-h'].includes(args[0]!))
|
||||
) {
|
||||
process.stdout.write(standaloneHelp('compat'));
|
||||
return 0;
|
||||
}
|
||||
const route = compatibilityRoute(args);
|
||||
if (route.surface === 'subscription') {
|
||||
const { subscriptionWorker } = await import(
|
||||
'../internal/subscription/worker'
|
||||
);
|
||||
return subscriptionWorker(route.args);
|
||||
}
|
||||
const { parse } = await import('../shared/cli/arguments');
|
||||
const { localContext } = await import('../internal/runtime/context');
|
||||
const { maintenance } = await import(
|
||||
'../internal/maintenance/maintenance'
|
||||
);
|
||||
const { loggedOperation } = await import(
|
||||
'../internal/runtime/commandLog'
|
||||
);
|
||||
const { registry } = await import('../internal/commands/registry');
|
||||
const command = parse(route.args, registry);
|
||||
const context = await localContext(command, { signal });
|
||||
const { result, logPath } = await loggedOperation(
|
||||
context,
|
||||
route.args[0]!,
|
||||
() => maintenance(command, context),
|
||||
signal,
|
||||
);
|
||||
process.stdout.write(
|
||||
JSON.stringify({ code: 200, data: result, logPath }) + '\n',
|
||||
);
|
||||
return 0;
|
||||
} catch (error) {
|
||||
const code =
|
||||
interruptedCode(signal) ??
|
||||
(error instanceof CliError ? error.exitCode : 1);
|
||||
process.stderr.write(
|
||||
JSON.stringify({
|
||||
code,
|
||||
message: signal.aborted
|
||||
? translate(process.env, '命令已中断。')
|
||||
: error instanceof CliError
|
||||
? error.message
|
||||
: translate(process.env, '旧命令适配器执行失败。'),
|
||||
}) + '\n',
|
||||
);
|
||||
return code;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module)
|
||||
void compatibilityMain().then((code) => {
|
||||
process.exitCode = code;
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { CommandSurface } from '../shared/cli/registry';
|
||||
// Compatibility for existing programmatic callers; product entries select one surface.
|
||||
export async function main(
|
||||
args = process.argv.slice(2),
|
||||
surface: CommandSurface = 'public',
|
||||
signal?: AbortSignal,
|
||||
): Promise<number> {
|
||||
return surface === 'public'
|
||||
? (await import('../entrypoints/remote')).remoteMain(args)
|
||||
: (await import('../internal/commands/main')).internalMain(args, signal);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
import { standaloneHelp } from '../internal/help/standalone';
|
||||
import {
|
||||
withCommandCancellation,
|
||||
cancellableOperation,
|
||||
interruptedCode,
|
||||
} from '../internal/runtime/cancellation';
|
||||
|
||||
// The legacy qinglong command starts the host installation; reload is its only
|
||||
// positional mode. All remaining validation belongs to the shared registry.
|
||||
export function startupArguments(args: string[]): string[] {
|
||||
return args[0] === 'reload'
|
||||
? ['start', '--reload', ...args.slice(1)]
|
||||
: ['start', ...args];
|
||||
}
|
||||
|
||||
export async function startupMain(
|
||||
args = process.argv.slice(2),
|
||||
): Promise<number> {
|
||||
if (
|
||||
args.length &&
|
||||
args.every((arg) => ['--help', '-h', '--json'].includes(arg)) &&
|
||||
args.some((arg) => arg === '--help' || arg === '-h')
|
||||
) {
|
||||
const help = standaloneHelp('startup');
|
||||
process.stdout.write(
|
||||
(args.includes('--json')
|
||||
? JSON.stringify({ code: 200, data: { help } })
|
||||
: help) + '\n',
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
return withCommandCancellation(async (signal) => {
|
||||
try {
|
||||
const { main } = await import('./main');
|
||||
return await cancellableOperation(signal, () =>
|
||||
main(startupArguments(args), 'local', signal),
|
||||
);
|
||||
} catch (error) {
|
||||
const code = interruptedCode(signal);
|
||||
if (code !== undefined) return code;
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module)
|
||||
void startupMain().then((code) => {
|
||||
process.exitCode = code;
|
||||
});
|
||||
Reference in New Issue
Block a user