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 { OneboxDaemon } from './classes/daemon.ts';
import { OneboxSystemd } from './classes/systemd.ts';
import type { IAppVersionConfig } from './classes/appstore-types.ts';
export async function runCli(): Promise<void> {
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();
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}`);
return;
}
@@ -70,6 +71,11 @@ export async function runCli(): Promise<void> {
await handleSslCommand(onebox, subcommand, args.slice(2));
break;
case 'appstore':
await handleAppStoreCommand(onebox, subcommand, args.slice(2));
break;
case 'proxy':
case 'nginx':
await handleNginxCommand(onebox, subcommand, args.slice(2));
break;
@@ -104,12 +110,11 @@ async function handleServiceCommand(onebox: Onebox, subcommand: string, args: st
const image = getArg(args, '--image');
const domain = getArg(args, '--domain');
const port = parseInt(getArg(args, '--port') || '80', 10);
const envArgs = args.filter((a) => a.startsWith('--env=')).map((a) => a.slice(6));
const envVars: Record<string, string> = {};
for (const env of envArgs) {
const [key, value] = env.split('=');
envVars[key] = value;
}
const envVars = parseEnvArgs(args);
requireValue(name, 'service name');
requireValue(image, '--image');
assertValidPort(port, '--port');
await onebox.services.deployService({ name, image, port, domain, envVars });
break;
@@ -158,6 +163,7 @@ async function handleRegistryCommand(onebox: Onebox, subcommand: string, args: s
const url = getArg(args, '--url');
const username = getArg(args, '--username');
const password = getArg(args, '--password');
requireValue(url, '--url');
await onebox.registries.addRegistry(url, username, password);
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
async function handleDnsCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
@@ -494,8 +570,106 @@ async function handleUpgradeCommand(): Promise<void> {
// Helpers
function getArg(args: string[], flag: string): string {
const arg = args.find((a) => a.startsWith(`${flag}=`));
return arg ? arg.split('=')[1] : '';
for (let i = 0; i < args.length; i++) {
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 {
@@ -532,9 +706,13 @@ Commands:
ssl list
ssl force-renew <domain>
nginx reload
nginx test
nginx status
appstore list
appstore config <app-id> [--version <version>]
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 disable Stop, disable, and remove systemd service
@@ -568,6 +746,7 @@ Production Workflow:
Examples:
onebox server --ephemeral # Start dev server
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 systemd enable
onebox systemd start