mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { constants } from 'node:os';
|
||||
|
||||
import { resolveQingLong3ProductCommand } from './productCommand';
|
||||
|
||||
const FORWARDED_SIGNALS = Object.freeze([
|
||||
'SIGINT',
|
||||
'SIGTERM',
|
||||
'SIGHUP',
|
||||
] as const);
|
||||
|
||||
export interface QingLong3ProductSignalChild {
|
||||
readonly exitCode: number | null;
|
||||
readonly signalCode: NodeJS.Signals | null;
|
||||
kill(signal: NodeJS.Signals): boolean;
|
||||
}
|
||||
|
||||
export interface QingLong3ProductSignalHost {
|
||||
on(signal: NodeJS.Signals, handler: () => void): unknown;
|
||||
off(signal: NodeJS.Signals, handler: () => void): unknown;
|
||||
}
|
||||
|
||||
export function signalExitCode(signal: NodeJS.Signals | null): number {
|
||||
if (signal === null) return 1;
|
||||
const number = constants.signals[signal];
|
||||
return typeof number === 'number' ? 128 + number : 1;
|
||||
}
|
||||
|
||||
function lowSensitivityFailure(
|
||||
code: string,
|
||||
message: string,
|
||||
): Readonly<Record<string, string | number>> {
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-product-cli',
|
||||
code,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
export function forwardSignals(
|
||||
child: QingLong3ProductSignalChild,
|
||||
signalHost: QingLong3ProductSignalHost = process,
|
||||
): () => void {
|
||||
const handlers = FORWARDED_SIGNALS.map((signal) => {
|
||||
const handler = (): void => {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill(signal);
|
||||
}
|
||||
};
|
||||
signalHost.on(signal, handler);
|
||||
return Object.freeze({ signal, handler });
|
||||
});
|
||||
return () => {
|
||||
for (const { signal, handler } of handlers) {
|
||||
signalHost.off(signal, handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function invoke(targetFilePath: string, argv: readonly string[]): void {
|
||||
let child: ChildProcess;
|
||||
try {
|
||||
child = spawn(process.execPath, [targetFilePath, ...argv], {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
});
|
||||
} catch {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify(
|
||||
lowSensitivityFailure(
|
||||
'QL3_PRODUCT_COMMAND_START_FAILED',
|
||||
'QingLong 3.0 product command could not start',
|
||||
),
|
||||
)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const removeSignalHandlers = forwardSignals(child);
|
||||
let settled = false;
|
||||
const settle = (exitCode: number): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
removeSignalHandlers();
|
||||
process.exitCode = exitCode;
|
||||
};
|
||||
child.once('error', () => {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify(
|
||||
lowSensitivityFailure(
|
||||
'QL3_PRODUCT_COMMAND_START_FAILED',
|
||||
'QingLong 3.0 product command could not start',
|
||||
),
|
||||
)}\n`,
|
||||
);
|
||||
settle(1);
|
||||
});
|
||||
child.once('close', (code, signal) => {
|
||||
settle(code ?? signalExitCode(signal));
|
||||
});
|
||||
}
|
||||
|
||||
function main(argv: readonly string[]): void {
|
||||
try {
|
||||
const resolution = resolveQingLong3ProductCommand(argv, __dirname);
|
||||
if (resolution.kind === 'help' || resolution.kind === 'version') {
|
||||
process.stdout.write(`${resolution.output}\n`);
|
||||
return;
|
||||
}
|
||||
if (resolution.kind === 'invalid') {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify(
|
||||
lowSensitivityFailure(resolution.code, resolution.message),
|
||||
)}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
invoke(resolution.targetFilePath, resolution.argv);
|
||||
} catch {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify(
|
||||
lowSensitivityFailure(
|
||||
'QL3_PRODUCT_CLI_INSTALLATION_INVALID',
|
||||
'QingLong 3.0 product command installation is invalid',
|
||||
),
|
||||
)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
||||
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
||||
|
||||
export interface QingLong3ProductCommandDefinition {
|
||||
readonly name: string;
|
||||
readonly binary: string;
|
||||
readonly target: string;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
export type QingLong3ProductCommandResolution =
|
||||
| Readonly<{ kind: 'help'; output: string }>
|
||||
| Readonly<{ kind: 'version'; output: string }>
|
||||
| Readonly<{
|
||||
kind: 'invoke';
|
||||
command: QingLong3ProductCommandDefinition;
|
||||
targetFilePath: string;
|
||||
argv: readonly string[];
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: 'invalid';
|
||||
code: 'QL3_PRODUCT_CLI_USAGE_INVALID';
|
||||
message: string;
|
||||
}>;
|
||||
|
||||
const PACKAGE_NAME = '@qinglong/local-owner-cli';
|
||||
const MAXIMUM_PACKAGE_MANIFEST_BYTES = 64 * 1024;
|
||||
const SEMVER_PATTERN =
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||
|
||||
export const QINGLONG3_PRODUCT_COMMANDS: readonly QingLong3ProductCommandDefinition[] =
|
||||
Object.freeze([
|
||||
Object.freeze({
|
||||
name: 'setup',
|
||||
binary: 'ql3-local-setup',
|
||||
target: 'lifecycle/localSetupCli.js',
|
||||
description: 'prepare Local storage and owner material',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'readiness',
|
||||
binary: 'ql3-local-readiness',
|
||||
target: 'lifecycle/localReadinessCli.js',
|
||||
description: 'inspect Local schema and runtime readiness',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'deploy',
|
||||
binary: 'ql3-local-deploy',
|
||||
target: 'deployment/localDeploymentCli.js',
|
||||
description: 'run the bounded Local deployment ceremony',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'owner',
|
||||
binary: 'ql3-owner',
|
||||
target: 'cli.js',
|
||||
description: 'manage the Local Owner ceremony',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'identity',
|
||||
binary: 'ql3-identity',
|
||||
target: 'security-management/identityCredentialCli.js',
|
||||
description: 'manage identities and API credentials',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'policy',
|
||||
binary: 'ql3-policy',
|
||||
target: 'security-management/projectPolicyCli.js',
|
||||
description: 'manage Project policy and lifecycle',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'audit',
|
||||
binary: 'ql3-audit',
|
||||
target: 'security-management/securityAuditQueryCli.js',
|
||||
description: 'query bounded security audit facts',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'secret',
|
||||
binary: 'ql3-secret',
|
||||
target: 'security-management/secretCli.js',
|
||||
description: 'manage Local Secret references',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'task',
|
||||
binary: 'ql3-task',
|
||||
target: 'automation-management/taskDefinitionCli.js',
|
||||
description: 'manage Task definitions',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'trigger',
|
||||
binary: 'ql3-trigger',
|
||||
target: 'automation-management/triggerCli.js',
|
||||
description: 'manage Trigger definitions',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'workflow',
|
||||
binary: 'ql3-workflow',
|
||||
target: 'plugin-package/pluginPackageWorkflowCli.js',
|
||||
description: 'inspect, start, and control Package workflows',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'approval',
|
||||
binary: 'ql3-approval',
|
||||
target: 'approval-management/approvalCli.js',
|
||||
description: 'inspect and decide Approved Actions',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'package',
|
||||
binary: 'ql3-package',
|
||||
target: 'plugin-package/pluginPackageCli.js',
|
||||
description: 'manage Plugin Package lifecycle',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'package-catalog',
|
||||
binary: 'ql3-package-catalog',
|
||||
target: 'plugin-package/pluginPackageCatalogCli.js',
|
||||
description: 'inspect the installed Package catalog',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'package-trust',
|
||||
binary: 'ql3-package-trust',
|
||||
target: 'plugin-package/pluginPackagePublisherTrustCli.js',
|
||||
description: 'manage trusted Package publishers',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'ai-feature',
|
||||
binary: 'ql3-ai-feature',
|
||||
target: 'ai-management/aiFeatureCli.js',
|
||||
description: 'activate or deactivate optional Local AI',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'model-price',
|
||||
binary: 'ql3-model-price',
|
||||
target: 'ai-management/modelPriceCatalogCli.js',
|
||||
description: 'manage the model price catalog',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'model-credential',
|
||||
binary: 'ql3-model-credential',
|
||||
target: 'ai-management/modelProviderCredentialCli.js',
|
||||
description: 'manage model provider credentials',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'prompt',
|
||||
binary: 'ql3-prompt',
|
||||
target: 'plugin-package/pluginPackagePromptCli.js',
|
||||
description: 'inspect and execute Package prompts',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'adoption',
|
||||
binary: 'ql3-adoption',
|
||||
target: 'lifecycle/adoptionCli.js',
|
||||
description: 'review and commit legacy task adoption',
|
||||
}),
|
||||
]);
|
||||
|
||||
function installationPaths(moduleDirectory: string): Readonly<{
|
||||
distRoot: string;
|
||||
packageManifestPath: string;
|
||||
}> {
|
||||
const distRoot = resolve(moduleDirectory, '..');
|
||||
return Object.freeze({
|
||||
distRoot,
|
||||
packageManifestPath: resolve(distRoot, '..', 'package.json'),
|
||||
});
|
||||
}
|
||||
|
||||
function isInside(parent: string, candidate: string): boolean {
|
||||
const pathFromParent = relative(parent, candidate);
|
||||
return (
|
||||
pathFromParent !== '' &&
|
||||
pathFromParent !== '..' &&
|
||||
!pathFromParent.startsWith(`..${sep}`) &&
|
||||
!isAbsolute(pathFromParent)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveInstalledTarget(
|
||||
distRoot: string,
|
||||
definition: QingLong3ProductCommandDefinition,
|
||||
): string {
|
||||
const lexicalTarget = resolve(distRoot, definition.target);
|
||||
if (!isInside(distRoot, lexicalTarget)) {
|
||||
throw new Error('product command target escapes package dist root');
|
||||
}
|
||||
const targetStatus = statSync(lexicalTarget, { throwIfNoEntry: false });
|
||||
if (targetStatus === undefined || !targetStatus.isFile()) {
|
||||
throw new Error('product command target is unavailable');
|
||||
}
|
||||
const canonicalDistRoot = realpathSync(distRoot);
|
||||
const canonicalTarget = realpathSync(lexicalTarget);
|
||||
if (!isInside(canonicalDistRoot, canonicalTarget)) {
|
||||
throw new Error('product command target escapes canonical package root');
|
||||
}
|
||||
return lexicalTarget;
|
||||
}
|
||||
|
||||
export function loadQingLong3ProductVersion(moduleDirectory: string): string {
|
||||
const { packageManifestPath } = installationPaths(moduleDirectory);
|
||||
const status = statSync(packageManifestPath, { throwIfNoEntry: false });
|
||||
if (
|
||||
status === undefined ||
|
||||
!status.isFile() ||
|
||||
status.size <= 0 ||
|
||||
status.size > MAXIMUM_PACKAGE_MANIFEST_BYTES
|
||||
) {
|
||||
throw new Error('product package manifest is unavailable');
|
||||
}
|
||||
const manifest = JSON.parse(readFileSync(packageManifestPath, 'utf8')) as {
|
||||
readonly name?: unknown;
|
||||
readonly version?: unknown;
|
||||
};
|
||||
if (
|
||||
manifest.name !== PACKAGE_NAME ||
|
||||
typeof manifest.version !== 'string' ||
|
||||
!SEMVER_PATTERN.test(manifest.version)
|
||||
) {
|
||||
throw new Error('product package identity is invalid');
|
||||
}
|
||||
return manifest.version;
|
||||
}
|
||||
|
||||
export function qingLong3ProductHelp(): string {
|
||||
const longestName = Math.max(
|
||||
...QINGLONG3_PRODUCT_COMMANDS.map(({ name }) => name.length),
|
||||
);
|
||||
const commands = QINGLONG3_PRODUCT_COMMANDS.map(
|
||||
({ name, description }) => ` ${name.padEnd(longestName)} ${description}`,
|
||||
).join('\n');
|
||||
return [
|
||||
'Usage: ql3 <command> [arguments]',
|
||||
'',
|
||||
'Commands:',
|
||||
commands,
|
||||
'',
|
||||
'Use `ql3 <command> --help` for command-specific usage.',
|
||||
'Root service mutation remains isolated in `ql3-service-bridge`.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function resolveQingLong3ProductCommand(
|
||||
argv: readonly string[],
|
||||
moduleDirectory: string,
|
||||
): QingLong3ProductCommandResolution {
|
||||
if (
|
||||
argv.length === 0 ||
|
||||
(argv.length === 1 &&
|
||||
(argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help'))
|
||||
) {
|
||||
return Object.freeze({ kind: 'help', output: qingLong3ProductHelp() });
|
||||
}
|
||||
if (
|
||||
argv.length === 1 &&
|
||||
(argv[0] === '--version' || argv[0] === '-V' || argv[0] === 'version')
|
||||
) {
|
||||
return Object.freeze({
|
||||
kind: 'version',
|
||||
output: loadQingLong3ProductVersion(moduleDirectory),
|
||||
});
|
||||
}
|
||||
const commandName = argv[0];
|
||||
const command = QINGLONG3_PRODUCT_COMMANDS.find(
|
||||
(candidate) => candidate.name === commandName,
|
||||
);
|
||||
if (command === undefined) {
|
||||
return Object.freeze({
|
||||
kind: 'invalid',
|
||||
code: 'QL3_PRODUCT_CLI_USAGE_INVALID',
|
||||
message: 'unknown QingLong 3.0 product command',
|
||||
});
|
||||
}
|
||||
const { distRoot } = installationPaths(moduleDirectory);
|
||||
return Object.freeze({
|
||||
kind: 'invoke',
|
||||
command,
|
||||
targetFilePath: resolveInstalledTarget(distRoot, command),
|
||||
argv: Object.freeze(argv.slice(1)),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user