Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/hydra/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { AbstractCLI, ConfigManager } from "@r5n/cli-core";
import { version } from "../package.json";
import { CreateCommand, InitCommand, ProfileCommand, RemoveCommand, StartCommand, StatusCommand, StopCommand } from "./commands";
import {
CreateCommand,
InitCommand,
ProfileCommand,
RemoveCommand,
StartCommand,
StatusCommand,
StopCommand,
UpdateCommand,
} from "./commands";
import { CLI_BIN, DEFAULT_CONFIG, HYDRA_CONFIG_FILE } from "./constants";
import type { HydraConfig } from "./types";

Expand All @@ -25,6 +34,7 @@ class HydraCLI extends AbstractCLI {
new StopCommand(),
new StatusCommand(),
new RemoveCommand(),
new UpdateCommand(),
new ProfileCommand(),
]);
}
Expand Down
10 changes: 7 additions & 3 deletions packages/hydra/src/commands/create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { join } from "node:path";
import { Exit, color, log, positionals, spinner, text } from "@r5n/cli-core";
import { color, Exit, log, positionals, spinner, text } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../base-command";
import { createProvider } from "../providers";
import type { RunnerEntry } from "../types";
Expand All @@ -24,7 +24,9 @@ export class CreateCommand extends BaseCommand {

const count = ctx.interactive
? await this.promptCount(profile.numberOfMachines)
: (ctx.positionals.count ? Number.parseInt(ctx.positionals.count, 10) : profile.numberOfMachines);
: ctx.positionals.count
? Number.parseInt(ctx.positionals.count, 10)
: profile.numberOfMachines;

if (!count || count < 1) {
throw new Exit("Runner count must be at least 1");
Expand Down Expand Up @@ -73,7 +75,9 @@ export class CreateCommand extends BaseCommand {

const isDefault = profileName === ctx.config.get("defaultProfile");
const profileSuffix = isDefault ? "" : ` ${profileName}`;
log.info(`${color.green("Created")} ${toCreate} runner(s) for "${profileName}". Run ${color.green(`hydra start${profileSuffix}`)} to start them.`);
log.info(
`${color.green("Created")} ${toCreate} runner(s) for "${profileName}". Run ${color.green(`hydra start${profileSuffix}`)} to start them.`,
);
}

private async promptCount(defaultCount: number): Promise<number> {
Expand Down
3 changes: 2 additions & 1 deletion packages/hydra/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export { CreateCommand } from "./create";
export { InitCommand } from "./init";
export { StatusCommand } from "./status";
export { ProfileCommand } from "./profile";
export { RemoveCommand } from "./remove";
export { StartCommand } from "./start";
export { StatusCommand } from "./status";
export { StopCommand } from "./stop";
export { UpdateCommand } from "./update";
38 changes: 15 additions & 23 deletions packages/hydra/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { existsSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { Cancel, Exit, args, color, confirm, group, note, positionals, text } from "@r5n/cli-core";
import { args, Cancel, color, confirm, Exit, group, note, positionals, text } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../base-command";
import { CLI_BIN, DEFAULT_PROFILE, RUNNERS_DIR, SHARED_DIR } from "../constants";
import type { Profile } from "../types";

const initPositionals = positionals({
url: { description: "GitHub repository or organization URL" },
});
const initPositionals = positionals({ url: { description: "GitHub repository or organization URL" } });

const initArgs = args({
force: { alias: "f", default: false, description: "Overwrite existing config", type: "boolean" },
Expand All @@ -27,9 +25,7 @@ export class InitCommand extends BaseCommand {
prompts = true;

async execute(ctx: InitCtx) {
const profileName = ctx.interactive
? await this.promptProfileName(ctx)
: (ctx.args.profile ?? DEFAULT_PROFILE);
const profileName = ctx.interactive ? await this.promptProfileName(ctx) : (ctx.args.profile ?? DEFAULT_PROFILE);

const isNewInit = !ctx.config.exists();

Expand All @@ -38,7 +34,10 @@ export class InitCommand extends BaseCommand {
if (!ctx.interactive) {
throw new Exit(`Profile "${profileName}" already exists. Use --force to overwrite.`);
}
const overwrite = await confirm({ initialValue: false, message: `Profile "${profileName}" already exists. Overwrite?` });
const overwrite = await confirm({
initialValue: false,
message: `Profile "${profileName}" already exists. Overwrite?`,
});
if (!overwrite) return;
}

Expand Down Expand Up @@ -70,10 +69,7 @@ export class InitCommand extends BaseCommand {
}

private async promptProfileName(ctx: InitCtx): Promise<string> {
return text({
initialValue: ctx.args.profile ?? DEFAULT_PROFILE,
message: "Profile name",
});
return text({ initialValue: ctx.args.profile ?? DEFAULT_PROFILE, message: "Profile name" });
}

private async ensureDirectories() {
Expand Down Expand Up @@ -102,12 +98,7 @@ export class InitCommand extends BaseCommand {
return undefined;
},
}),
name: () =>
text({
initialValue: "runner",
message: "Base name for runners",
placeholder: "runner",
}),
name: () => text({ initialValue: "runner", message: "Base name for runners", placeholder: "runner" }),
runners: () =>
text({
initialValue: "1",
Expand All @@ -121,12 +112,13 @@ export class InitCommand extends BaseCommand {
},
}),
labels: () =>
text({
message: "Additional labels (comma-separated, optional)",
placeholder: "self-hosted,macOS,ARM64",
}),
text({ message: "Additional labels (comma-separated, optional)", placeholder: "self-hosted,macOS,ARM64" }),
},
{
onCancel: () => {
throw new Cancel();
},
},
{ onCancel: () => { throw new Cancel(); } },
);

return {
Expand Down
10 changes: 3 additions & 7 deletions packages/hydra/src/commands/profile/default.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Exit, color, log, positionals } from "@r5n/cli-core";
import { color, Exit, log, positionals } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../../base-command";
import { resolveProfile, selectProfile } from "../../utils";

const defaultPositionals = positionals({
name: { description: "Profile name to set as default" },
});
const defaultPositionals = positionals({ name: { description: "Profile name to set as default" } });

type DefaultCtx = Ctx<Record<string, never>, typeof defaultPositionals>;

Expand All @@ -14,9 +12,7 @@ export class ProfileDefaultCommand extends BaseCommand {
positionals = defaultPositionals;

async execute(ctx: DefaultCtx) {
const profileName = ctx.interactive
? await selectProfile(ctx.config)
: ctx.positionals.name;
const profileName = ctx.interactive ? await selectProfile(ctx.config) : ctx.positionals.name;

if (!profileName) {
throw new Exit("Profile name is required", "Usage: hydra profile default <name>");
Expand Down
2 changes: 1 addition & 1 deletion packages/hydra/src/commands/profile/list.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Exit, color, log, note } from "@r5n/cli-core";
import { color, Exit, log, note } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../../base-command";
import { createProvider } from "../../providers";
import type { Profile } from "../../types";
Expand Down
15 changes: 7 additions & 8 deletions packages/hydra/src/commands/profile/remove.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { Exit, color, confirm, log, positionals, spinner } from "@r5n/cli-core";
import { color, confirm, Exit, log, positionals, spinner } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../../base-command";
import { createProvider } from "../../providers";
import type { Profile } from "../../types";
import { resolveProfile, selectProfile } from "../../utils";

const removePositionals = positionals({
name: { description: "Profile name to remove" },
});
const removePositionals = positionals({ name: { description: "Profile name to remove" } });

type RemoveCtx = Ctx<Record<string, never>, typeof removePositionals>;

Expand All @@ -18,9 +16,7 @@ export class ProfileRemoveCommand extends BaseCommand {
positionals = removePositionals;

async execute(ctx: RemoveCtx) {
const profileName = ctx.interactive
? await selectProfile(ctx.config)
: ctx.positionals.name;
const profileName = ctx.interactive ? await selectProfile(ctx.config) : ctx.positionals.name;

if (!profileName) {
throw new Exit("Profile name is required", "Usage: hydra profile remove <name>");
Expand All @@ -38,7 +34,10 @@ export class ProfileRemoveCommand extends BaseCommand {
});
if (!proceed) return;

await this.removeRunners(profile, profileRunners.map((e) => e.id));
await this.removeRunners(
profile,
profileRunners.map((e) => e.id),
);
const remainingEntries = entries.filter((e) => e.profile !== profileName);
ctx.config.set("runners", remainingEntries);
}
Expand Down
7 changes: 5 additions & 2 deletions packages/hydra/src/commands/remove.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { Exit, color, log, multiselect, positionals, spinner } from "@r5n/cli-core";
import { color, Exit, log, multiselect, positionals, spinner } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../base-command";
import { createProvider } from "../providers";
import { resolveProfile, resolveRunnerIds, selectProfile } from "../utils";
Expand Down Expand Up @@ -30,7 +30,10 @@ export class RemoveCommand extends BaseCommand {

const ids = ctx.interactive
? await this.promptRunnerSelection(profileEntries.map((e) => e.id))
: resolveRunnerIds(ctx.positionals.ids ?? [], profileEntries.map((e) => e.id));
: resolveRunnerIds(
ctx.positionals.ids ?? [],
profileEntries.map((e) => e.id),
);

const provider = createProvider(profile);
const s = spinner();
Expand Down
7 changes: 5 additions & 2 deletions packages/hydra/src/commands/start.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Exit, color, log, multiselect, positionals, spinner } from "@r5n/cli-core";
import { color, Exit, log, multiselect, positionals, spinner } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../base-command";
import { createProvider } from "../providers";
import { resolveProfile, resolveRunnerIds, selectProfile } from "../utils";
Expand Down Expand Up @@ -39,7 +39,10 @@ export class StartCommand extends BaseCommand {

const ids = ctx.interactive
? await this.promptRunnerSelection(stoppedIds)
: resolveRunnerIds(ctx.positionals.ids ?? [], profileEntries.map((e) => e.id));
: resolveRunnerIds(
ctx.positionals.ids ?? [],
profileEntries.map((e) => e.id),
);

let started = 0;
for (const id of ids) {
Expand Down
4 changes: 2 additions & 2 deletions packages/hydra/src/commands/status.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Exit, color, log } from "@r5n/cli-core";
import { color, Exit, log } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../base-command";
import { createProvider } from "../providers";
import type { RunnerInfo } from "../providers";
import { createProvider } from "../providers";
import type { Profile } from "../types";

type ListCtx = Ctx;
Expand Down
7 changes: 5 additions & 2 deletions packages/hydra/src/commands/stop.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Exit, color, log, multiselect, positionals, spinner } from "@r5n/cli-core";
import { color, Exit, log, multiselect, positionals, spinner } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../base-command";
import { createProvider } from "../providers";
import { resolveProfile, resolveRunnerIds, selectProfile } from "../utils";
Expand Down Expand Up @@ -39,7 +39,10 @@ export class StopCommand extends BaseCommand {

const ids = ctx.interactive
? await this.promptRunnerSelection(activeIds)
: resolveRunnerIds(ctx.positionals.ids ?? [], profileEntries.map((e) => e.id));
: resolveRunnerIds(
ctx.positionals.ids ?? [],
profileEntries.map((e) => e.id),
);

let stopped = 0;
for (const id of ids) {
Expand Down
66 changes: 66 additions & 0 deletions packages/hydra/src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { color, Exit, log, spinner } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../base-command";
import { createProvider } from "../providers";
import type { Profile } from "../types";

type UpdateCtx = Ctx;

export class UpdateCommand extends BaseCommand {
name = "update";
description = "Update runners to the latest version";

async execute(ctx: UpdateCtx) {
const profiles = ctx.config.get("profiles");
const entries = ctx.config.get("runners") ?? [];

if (entries.length === 0) {
throw new Exit("No runners found", "Run hydra create to provision runners");
}

const s = spinner();
const byProfile = Map.groupBy(entries, (e) => e.profile);
let currentVersion: string | null = null;
let latestVersion = "";
let versionChecked = false;

for (const [profileName, profileEntries] of byProfile) {
const profile = profiles[profileName] as Profile | undefined;
const firstRunner = profileEntries[0];
if (!profile || !firstRunner) continue;

const provider = createProvider(profile);

if (!versionChecked) {
versionChecked = true;
s.start("Checking for updates...");
const result = await provider.download();
latestVersion = result.version;
currentVersion = await provider.currentVersion(firstRunner.id);
s.stop(`Current: v${currentVersion ?? "unknown"} | Latest: v${latestVersion}`);

if (currentVersion === latestVersion) {
log.info(`Already on latest version ${color.dim(`(v${latestVersion})`)}`);
return;
}
}

const statuses = await provider.list();
const runningIds = new Set(statuses.filter((r) => r.status === "running").map((r) => r.id));

for (const entry of profileEntries) {
const wasRunning = runningIds.has(entry.id);

s.start(`Updating ${entry.id}...`);

if (wasRunning) await provider.stop([entry.id]);
await provider.update([entry.id]);
if (wasRunning) await provider.start([entry.id]);

const suffix = wasRunning ? ` ${color.dim("(restarted)")}` : "";
s.stop(`${color.green("✓")} ${entry.id}${suffix}`);
}
}

log.info(`${color.green("Updated")} ${entries.length} runner(s) from v${currentVersion} to v${latestVersion}`);
}
}
4 changes: 1 addition & 3 deletions packages/hydra/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,4 @@ export const RUNNERS_DIR = `${HYDRA_DIR}/runners`;

export const DEFAULT_PROFILE = "default";

export const DEFAULT_CONFIG: HydraConfig = {
profiles: {},
};
export const DEFAULT_CONFIG: HydraConfig = { profiles: {} };
Loading
Loading