feat: add appstore install CLI
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user