Files
qinglong/packages/ql3-local-owner-cli/src/product-cli/productCommand.ts
T

284 lines
8.8 KiB
TypeScript

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: 'run',
binary: 'ql3-run',
target: 'run-management/runRetryCli.js',
description: 'retry terminal Runs under strong local authentication',
}),
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)),
});
}