feat: add appstore install CLI

This commit is contained in:
2026-04-29 01:59:09 +00:00
parent 2b51178016
commit c451d71a97
+192 -13
View File
@@ -8,16 +8,17 @@ import { getErrorMessage } from './utils/error.ts';
import { Onebox } from './classes/onebox.ts'; import { Onebox } from './classes/onebox.ts';
import { OneboxDaemon } from './classes/daemon.ts'; import { OneboxDaemon } from './classes/daemon.ts';
import { OneboxSystemd } from './classes/systemd.ts'; import { OneboxSystemd } from './classes/systemd.ts';
import type { IAppVersionConfig } from './classes/appstore-types.ts';
export async function runCli(): Promise<void> { export async function runCli(): Promise<void> {
const args = Deno.args; const args = Deno.args;
if (args.length === 0 || args.includes('--help') || args.includes('-h')) { if (args.length === 0 || (args.length === 1 && (args[0] === '--help' || args[0] === '-h'))) {
printHelp(); printHelp();
return; return;
} }
if (args.includes('--version') || args.includes('-v')) { if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
console.log(`${projectInfo.name} v${projectInfo.version}`); console.log(`${projectInfo.name} v${projectInfo.version}`);
return; return;
} }
@@ -70,6 +71,11 @@ export async function runCli(): Promise<void> {
await handleSslCommand(onebox, subcommand, args.slice(2)); await handleSslCommand(onebox, subcommand, args.slice(2));
break; break;
case 'appstore':
await handleAppStoreCommand(onebox, subcommand, args.slice(2));
break;
case 'proxy':
case 'nginx': case 'nginx':
await handleNginxCommand(onebox, subcommand, args.slice(2)); await handleNginxCommand(onebox, subcommand, args.slice(2));
break; break;
@@ -104,12 +110,11 @@ async function handleServiceCommand(onebox: Onebox, subcommand: string, args: st
const image = getArg(args, '--image'); const image = getArg(args, '--image');
const domain = getArg(args, '--domain'); const domain = getArg(args, '--domain');
const port = parseInt(getArg(args, '--port') || '80', 10); const port = parseInt(getArg(args, '--port') || '80', 10);
const envArgs = args.filter((a) => a.startsWith('--env=')).map((a) => a.slice(6)); const envVars = parseEnvArgs(args);
const envVars: Record<string, string> = {};
for (const env of envArgs) { requireValue(name, 'service name');
const [key, value] = env.split('='); requireValue(image, '--image');
envVars[key] = value; assertValidPort(port, '--port');
}
await onebox.services.deployService({ name, image, port, domain, envVars }); await onebox.services.deployService({ name, image, port, domain, envVars });
break; break;
@@ -158,6 +163,7 @@ async function handleRegistryCommand(onebox: Onebox, subcommand: string, args: s
const url = getArg(args, '--url'); const url = getArg(args, '--url');
const username = getArg(args, '--username'); const username = getArg(args, '--username');
const password = getArg(args, '--password'); const password = getArg(args, '--password');
requireValue(url, '--url');
await onebox.registries.addRegistry(url, username, password); await onebox.registries.addRegistry(url, username, password);
break; break;
} }
@@ -180,6 +186,76 @@ async function handleRegistryCommand(onebox: Onebox, subcommand: string, args: s
} }
} }
// App Store commands
async function handleAppStoreCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'list': {
const apps = await onebox.appStore.getApps();
logger.table(
['ID', 'Name', 'Category', 'Latest'],
apps.map((app) => [app.id, app.name, app.category, app.latestVersion])
);
break;
}
case 'config': {
const appId = args[0];
requireValue(appId, 'app id');
const appMeta = await onebox.appStore.getAppMeta(appId);
const version = getArg(args, '--version') || appMeta.latestVersion;
const config = await onebox.appStore.getAppVersionConfig(appId, version);
console.log(JSON.stringify({ appMeta, version, config }, null, 2));
break;
}
case 'install': {
const appId = args[0];
requireValue(appId, 'app id');
const appMeta = await onebox.appStore.getAppMeta(appId);
const version = getArg(args, '--version') || appMeta.latestVersion;
const config = await onebox.appStore.getAppVersionConfig(appId, version);
const serviceName = getArg(args, '--name') || appId;
const domain = getArg(args, '--domain');
const port = parseInt(getArg(args, '--port') || String(config.port), 10);
const envVars = getAppStoreEnvVars(config, parseEnvArgs(args));
const autoDNS = getBooleanArg(args, '--auto-dns', true);
requireValue(serviceName, '--name');
assertValidPort(port, '--port');
if (requiresTemplateValue(envVars, 'SERVICE_DOMAIN')) {
requireValue(domain, '--domain');
}
const service = await onebox.services.deployService({
name: serviceName,
image: config.image,
port,
domain,
autoDNS,
envVars,
enableMongoDB: Boolean(config.platformRequirements?.mongodb),
enableS3: Boolean(config.platformRequirements?.s3),
enableClickHouse: Boolean(config.platformRequirements?.clickhouse),
enableRedis: Boolean(config.platformRequirements?.redis),
enableMariaDB: Boolean(config.platformRequirements?.mariadb),
appTemplateId: appId,
appTemplateVersion: version,
});
logger.success(`Installed ${appMeta.name} ${version} as ${service.name}`);
if (service.domain) {
logger.info(`Route: https://${service.domain}`);
}
break;
}
default:
logger.error(`Unknown appstore subcommand: ${subcommand}`);
logger.info('Available: list, config, install');
}
}
// DNS commands // DNS commands
async function handleDnsCommand(onebox: Onebox, subcommand: string, args: string[]) { async function handleDnsCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) { switch (subcommand) {
@@ -494,8 +570,106 @@ async function handleUpgradeCommand(): Promise<void> {
// Helpers // Helpers
function getArg(args: string[], flag: string): string { function getArg(args: string[], flag: string): string {
const arg = args.find((a) => a.startsWith(`${flag}=`)); for (let i = 0; i < args.length; i++) {
return arg ? arg.split('=')[1] : ''; const arg = args[i];
if (arg.startsWith(`${flag}=`)) {
return arg.slice(flag.length + 1);
}
if (arg === flag) {
const value = args[i + 1];
return value && !value.startsWith('--') ? value : '';
}
}
return '';
}
function getRepeatedArgs(args: string[], flag: string): string[] {
const values: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith(`${flag}=`)) {
values.push(arg.slice(flag.length + 1));
continue;
}
if (arg === flag) {
const value = args[i + 1];
if (value && !value.startsWith('--')) {
values.push(value);
i++;
}
}
}
return values;
}
function getBooleanArg(args: string[], flag: string, defaultValue: boolean): boolean {
if (args.includes(`--no-${flag.slice(2)}`)) {
return false;
}
const value = getArg(args, flag);
if (!value) {
return args.includes(flag) ? true : defaultValue;
}
return !['0', 'false', 'no', 'off'].includes(value.toLowerCase());
}
function parseEnvArgs(args: string[]): Record<string, string> {
const envVars: Record<string, string> = {};
for (const envArg of getRepeatedArgs(args, '--env')) {
const separatorIndex = envArg.indexOf('=');
if (separatorIndex === -1) {
throw new Error(`Invalid --env value '${envArg}'. Expected KEY=VALUE.`);
}
const key = envArg.slice(0, separatorIndex);
const value = envArg.slice(separatorIndex + 1);
requireValue(key, '--env key');
envVars[key] = value;
}
return envVars;
}
function getAppStoreEnvVars(
configArg: IAppVersionConfig,
overridesArg: Record<string, string>,
): Record<string, string> {
const envVars: Record<string, string> = {};
const missingRequiredEnvVars: string[] = [];
for (const envVar of configArg.envVars || []) {
const value = overridesArg[envVar.key] ?? envVar.value ?? '';
if (envVar.required && !value) {
missingRequiredEnvVars.push(envVar.key);
}
envVars[envVar.key] = value;
}
for (const [key, value] of Object.entries(overridesArg)) {
envVars[key] = value;
}
if (missingRequiredEnvVars.length > 0) {
throw new Error(
`Missing required app env var(s): ${missingRequiredEnvVars.join(', ')}. Use --env KEY=VALUE.`
);
}
return envVars;
}
function requiresTemplateValue(envVarsArg: Record<string, string>, templateNameArg: string): boolean {
return Object.values(envVarsArg).some((value) => value.includes(`\${${templateNameArg}}`));
}
function requireValue(valueArg: string | undefined, labelArg: string): asserts valueArg is string {
if (!valueArg) {
throw new Error(`Missing required ${labelArg}`);
}
}
function assertValidPort(portArg: number, labelArg: string): void {
if (!Number.isInteger(portArg) || portArg <= 0 || portArg > 65535) {
throw new Error(`Invalid ${labelArg}: ${portArg}`);
}
} }
function printHelp(): void { function printHelp(): void {
@@ -532,9 +706,13 @@ Commands:
ssl list ssl list
ssl force-renew <domain> ssl force-renew <domain>
nginx reload appstore list
nginx test appstore config <app-id> [--version <version>]
nginx status appstore install <app-id> --name <name> [--domain <domain>] [--version <version>] [--env KEY=VALUE]
proxy reload # nginx alias is still supported
proxy test
proxy status
systemd enable Install and enable systemd service systemd enable Install and enable systemd service
systemd disable Stop, disable, and remove systemd service systemd disable Stop, disable, and remove systemd service
@@ -568,6 +746,7 @@ Production Workflow:
Examples: Examples:
onebox server --ephemeral # Start dev server onebox server --ephemeral # Start dev server
onebox service add myapp --image nginx:latest --domain app.example.com --port 80 onebox service add myapp --image nginx:latest --domain app.example.com --port 80
onebox appstore install cloudly --name cloudly --domain cloudly.example.com --env SERVEZONE_ADMINACCOUNT=admin:password
onebox registry add --url registry.example.com --username user --password pass onebox registry add --url registry.example.com --username user --password pass
onebox systemd enable onebox systemd enable
onebox systemd start onebox systemd start