@companyhelm/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -33
  3. package/dist/cli.js +11 -1
  4. package/dist/commands/dependencies.d.ts +18 -3
  5. package/dist/commands/dependencies.js +76 -13
  6. package/dist/commands/interactive.d.ts +6 -0
  7. package/dist/commands/interactive.js +22 -0
  8. package/dist/commands/logs.js +6 -1
  9. package/dist/commands/register-commands.js +4 -0
  10. package/dist/commands/reset.d.ts +4 -0
  11. package/dist/commands/reset.js +43 -4
  12. package/dist/commands/set-image-version.d.ts +31 -0
  13. package/dist/commands/set-image-version.js +87 -0
  14. package/dist/commands/setup-github-app.d.ts +10 -0
  15. package/dist/commands/setup-github-app.js +211 -0
  16. package/dist/commands/status.js +3 -1
  17. package/dist/commands/up.js +11 -2
  18. package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +2 -2
  19. package/dist/core/bootstrap/DeploymentBootstrapper.js +5 -7
  20. package/dist/core/bootstrap/SeedSqlRenderer.js +23 -5
  21. package/dist/core/config/ApiEnvFileWriter.d.ts +6 -0
  22. package/dist/core/config/ApiEnvFileWriter.js +26 -0
  23. package/dist/core/config/GithubAppConfig.d.ts +6 -0
  24. package/dist/core/config/GithubAppConfig.js +26 -0
  25. package/dist/core/config/GithubAppConfigStore.d.ts +11 -0
  26. package/dist/core/config/GithubAppConfigStore.js +65 -0
  27. package/dist/core/docker/ComposeTemplateRenderer.d.ts +6 -1
  28. package/dist/core/docker/ComposeTemplateRenderer.js +22 -4
  29. package/dist/core/docker/DockerStackManager.d.ts +15 -3
  30. package/dist/core/docker/DockerStackManager.js +67 -8
  31. package/dist/core/runner/RunnerSupervisor.d.ts +4 -0
  32. package/dist/core/runner/RunnerSupervisor.js +19 -3
  33. package/dist/core/runtime/ImageCatalog.js +5 -2
  34. package/dist/core/runtime/LocalConfigStore.d.ts +16 -0
  35. package/dist/core/runtime/LocalConfigStore.js +59 -0
  36. package/dist/core/runtime/ManagedImages.d.ts +10 -0
  37. package/dist/core/runtime/ManagedImages.js +27 -0
  38. package/dist/core/runtime/ProjectPaths.d.ts +7 -0
  39. package/dist/core/runtime/ProjectPaths.js +16 -0
  40. package/dist/core/runtime/PublicImageTagRegistry.d.ts +16 -0
  41. package/dist/core/runtime/PublicImageTagRegistry.js +148 -0
  42. package/dist/core/runtime/RuntimeState.d.ts +1 -1
  43. package/dist/core/runtime/RuntimeStateStore.d.ts +1 -0
  44. package/dist/core/runtime/RuntimeStateStore.js +8 -2
  45. package/dist/core/runtime/VersionCatalog.d.ts +10 -0
  46. package/dist/core/runtime/VersionCatalog.js +21 -0
  47. package/dist/core/status/StatusService.d.ts +5 -1
  48. package/dist/core/status/StatusService.js +5 -2
  49. package/dist/core/ui/TerminalRenderer.d.ts +10 -0
  50. package/dist/core/ui/TerminalRenderer.js +48 -0
  51. package/dist/templates/docker-compose.yaml.tpl +4 -13
  52. package/dist/templates/seed.sql.tpl +32 -13
  53. package/package.json +7 -3
  54. package/src/templates/docker-compose.yaml.tpl +4 -13
  55. package/src/templates/seed.sql.tpl +32 -13
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CompanyHelm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,54 +1,61 @@
1
- # CompanyHelm CLI
1
+ # CompanyHelm - Distributed AI Agent Orchestration
2
2
 
3
- Bootstrap and manage a local CompanyHelm deployment.
3
+ CompanyHelm is an open-source control plane for running AI-agent companies in your own infrastructure.
4
+ It gives teams a way to organize agents by role, keep humans in the loop for approvals and clarification, and run agent workloads in isolated environments instead of opaque hosted black boxes. Each agent can run its own app infrastructure for testing and create PRs autonomously. Spin up container-isolated agent threads with a click.
4
5
 
5
- ## Commands
6
+ [Website](https://www.companyhelm.com/)
6
7
 
7
- Start or reconcile the local deployment:
8
+ ## Quick start
9
+
10
+ Dependecies:
11
+ - Docker
12
+ - Node.js `>=24`
13
+ - Codex subscription or api key
14
+ - Github account
8
15
 
9
16
  ```bash
10
17
  npx @companyhelm/cli up
11
18
  ```
12
19
 
13
- Stop the local deployment:
20
+ After startup, the CLI prints:
14
21
 
15
- ```bash
16
- npx @companyhelm/cli down
17
- ```
22
+ - the local dashboard UI URL
23
+ - the generated username and password
18
24
 
19
- Show the current service status:
25
+ ## What CompanyHelm is
20
26
 
21
- ```bash
22
- npx @companyhelm/cli status
23
- ```
24
-
25
- Show logs for one managed service:
27
+ From the product perspective, CompanyHelm is built around a few core ideas:
26
28
 
27
- ```bash
28
- npx @companyhelm/cli logs <service>
29
- ```
29
+ - Your infrastructure, not a vendor-controlled runtime
30
+ - Model-agnostic agent execution through open runners and protocols
31
+ - Easy agent customization: add skills, MCP servers, and custom instructions to agents from the UI
32
+ - Human-in-the-loop workflows: tasks can be steered at any moment through the built-in chat; approvals and questions are still a work in progress
33
+ - Isolated execution so agents can work in parallel with minimal interference. Each runner can spin up the full app infrastructure within Docker and test in isolation
34
+ - YOLO mode by default: agents run commands without pausing for trivial confirmations
35
+ - Remote repository as the source of truth: agents clone the repo and submit PRs automatically
36
+ - Parallel task execution: multiple agents can execute tasks independently in isolated environments
30
37
 
31
- Supported services:
38
+ ## What the CLI boots locally
32
39
 
33
- - `postgres`
34
- - `api`
35
- - `frontend`
36
- - `runner`
40
+ `npx @companyhelm/cli up` brings up a local CompanyHelm stack with:
37
41
 
38
- Destroy the local deployment and generated state:
42
+ - CompanyHelm API
43
+ - Postgres
44
+ - CompanyHelm frontend
45
+ - CompanyHelm agent runner
39
46
 
40
- ```bash
41
- npx @companyhelm/cli reset --force
42
- ```
43
47
 
44
- ## Authentication
48
+ ## Command reference
45
49
 
46
- On first `npx @companyhelm/cli up`, the CLI generates a local `admin` account and a random password. The password is printed at startup and persisted in the local runtime state until `npx @companyhelm/cli reset --force` is run.
50
+ For the full CLI help:
47
51
 
48
- ## Image Overrides
52
+ ```bash
53
+ npx @companyhelm/cli --help
54
+ ```
49
55
 
50
- The packaged stack can be overridden with environment variables:
56
+ Common commands:
51
57
 
52
- - `COMPANYHELM_API_IMAGE`
53
- - `COMPANYHELM_WEB_IMAGE`
54
- - `COMPANYHELM_POSTGRES_IMAGE`
58
+ ```bash
59
+ npx @companyhelm/cli logs {service]
60
+ npx @companyhelm/cli reset
61
+ ```
package/dist/cli.js CHANGED
@@ -1,7 +1,17 @@
1
1
  import { buildProgram } from "./commands/register-commands.js";
2
+ import { InteractiveCommandCancelledError } from "./commands/interactive.js";
2
3
  export async function main(argv = process.argv) {
3
4
  const program = buildProgram();
4
- await program.parseAsync(argv);
5
+ try {
6
+ await program.parseAsync(argv);
7
+ }
8
+ catch (error) {
9
+ if (error instanceof InteractiveCommandCancelledError) {
10
+ process.exitCode = 1;
11
+ return;
12
+ }
13
+ throw error;
14
+ }
5
15
  }
6
16
  if (import.meta.url === `file://${process.argv[1]}`) {
7
17
  void main();
@@ -1,9 +1,24 @@
1
+ import { type RuntimeVersions } from "../core/runtime/VersionCatalog.js";
1
2
  import { type StatusSnapshot } from "../core/status/StatusService.js";
3
+ export type LogLevel = "debug" | "info" | "warn" | "error";
4
+ export interface UpOptions {
5
+ logLevel?: LogLevel;
6
+ }
7
+ export interface ResetOptions {
8
+ removeGithubAppConfig?: boolean;
9
+ }
10
+ export interface StatusReport {
11
+ services: StatusSnapshot;
12
+ apiUrl?: string;
13
+ uiUrl?: string;
14
+ username?: string;
15
+ versions?: RuntimeVersions;
16
+ }
2
17
  export interface CommandDependencies {
3
- up(): Promise<void>;
18
+ up(options?: UpOptions): Promise<void>;
4
19
  down(): Promise<void>;
5
- status(): Promise<StatusSnapshot>;
20
+ status(): Promise<StatusReport>;
6
21
  logs(service: string): Promise<void>;
7
- reset(): Promise<void>;
22
+ reset(options?: ResetOptions): Promise<void>;
8
23
  }
9
24
  export declare function createDefaultDependencies(): CommandDependencies;
@@ -2,15 +2,20 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { DeploymentBootstrapper } from "../core/bootstrap/DeploymentBootstrapper.js";
5
+ import { ApiEnvFileWriter } from "../core/config/ApiEnvFileWriter.js";
6
+ import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
5
7
  import { DockerStackManager } from "../core/docker/DockerStackManager.js";
6
8
  import { LogsService } from "../core/logs/LogsService.js";
7
9
  import { CommandRunner } from "../core/process/CommandRunner.js";
10
+ import { ProjectPaths } from "../core/runtime/ProjectPaths.js";
8
11
  import { RunnerSupervisor } from "../core/runner/RunnerSupervisor.js";
9
12
  import { createPasswordHash } from "../core/runtime/Secrets.js";
10
13
  import { RuntimePaths } from "../core/runtime/RuntimePaths.js";
11
14
  import { RuntimeStateStore } from "../core/runtime/RuntimeStateStore.js";
15
+ import { VersionCatalog } from "../core/runtime/VersionCatalog.js";
12
16
  import { StatusService } from "../core/status/StatusService.js";
13
17
  import { TerminalRenderer } from "../core/ui/TerminalRenderer.js";
18
+ import { ensureGithubAppConfig } from "./setup-github-app.js";
14
19
  function runtimeRoot() {
15
20
  return process.env.COMPANYHELM_HOME || path.join(os.homedir(), ".companyhelm");
16
21
  }
@@ -23,7 +28,22 @@ export function createDefaultDependencies() {
23
28
  const dockerStackManager = new DockerStackManager(root, commandRunner);
24
29
  const runnerSupervisor = new RunnerSupervisor(runtimePaths.runnerConfigPath());
25
30
  const bootstrapper = new DeploymentBootstrapper();
26
- const statusService = new StatusService(() => dockerStackManager.runningServices());
31
+ const githubAppConfigStore = new GithubAppConfigStore();
32
+ const apiEnvFileWriter = new ApiEnvFileWriter(process.cwd());
33
+ const projectPaths = new ProjectPaths(process.cwd());
34
+ const versionCatalog = new VersionCatalog();
35
+ const statusService = new StatusService(() => dockerStackManager.runningServices(), {
36
+ runner: async () => {
37
+ try {
38
+ const statusCommand = runnerSupervisor.buildStatusArgs();
39
+ const output = await commandRunner.capture(statusCommand.command, statusCommand.args);
40
+ return output.includes("Daemon: running");
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ });
27
47
  const logsService = new LogsService(async (service) => {
28
48
  if (service === "runner") {
29
49
  if (fs.existsSync(runtimePaths.runnerLogPath())) {
@@ -34,26 +54,48 @@ export function createDefaultDependencies() {
34
54
  await dockerStackManager.logs(service);
35
55
  });
36
56
  return {
37
- async up() {
57
+ async up(options = {}) {
58
+ const logLevel = options.logLevel ?? "info";
59
+ const githubAppConfig = await ensureGithubAppConfig(githubAppConfigStore, process.stdin, process.stdout);
38
60
  const state = stateStore.initialize();
61
+ const versions = versionCatalog.resolve();
39
62
  const passwordRecord = createPasswordHash(state.auth.password);
40
63
  fs.mkdirSync(root, { recursive: true });
64
+ apiEnvFileWriter.write(githubAppConfig);
41
65
  bootstrapper.writeSeedSql(root, state, passwordRecord.passwordHash, passwordRecord.passwordSalt);
42
- bootstrapper.writeApiConfig(root, state);
66
+ bootstrapper.writeApiConfig(root, state, logLevel);
43
67
  bootstrapper.writeFrontendConfig(root, state);
44
68
  process.stdout.write(`${renderer.renderBanner()}\n`);
45
- await dockerStackManager.up(state);
46
- await dockerStackManager.applySeedSql();
69
+ await dockerStackManager.up(state, { frontendLogLevel: logLevel });
70
+ process.stdout.write(`${renderer.progress("Initializing the database...")}\n`);
71
+ process.stdout.write(`${renderer.progress("Waiting for database migrations...")}\n`);
72
+ await dockerStackManager.applySeedSql(state.auth.username);
73
+ const configureSdkCommand = runnerSupervisor.buildUseHostAuthArgs();
74
+ process.stdout.write(`${renderer.progress("Configuring runner authentication...")}\n`);
75
+ await commandRunner.run(configureSdkCommand.command, configureSdkCommand.args);
47
76
  const startCommand = runnerSupervisor.buildStartArgs({
48
77
  serverUrl: `127.0.0.1:${state.ports.runnerGrpc}`,
49
78
  agentApiUrl: `127.0.0.1:${state.ports.agentCliGrpc}`,
50
79
  logPath: runtimePaths.runnerLogPath(),
51
- secret: state.runner.secret
80
+ secret: state.runner.secret,
81
+ logLevel
52
82
  });
83
+ process.stdout.write(`${renderer.progress("Starting the runner...")}\n`);
53
84
  await commandRunner.run(startCommand.command, startCommand.args);
54
- process.stdout.write(`${renderer.success(`API: http://127.0.0.1:${state.ports.apiHttp}/graphql`)}\n`);
55
- process.stdout.write(`${renderer.success(`UI: http://127.0.0.1:${state.ports.ui}`)}\n`);
56
- process.stdout.write(`admin password: ${state.auth.password}\n`);
85
+ const apiUrl = `http://127.0.0.1:${state.ports.apiHttp}/graphql`;
86
+ const uiUrl = `http://127.0.0.1:${state.ports.ui}`;
87
+ process.stdout.write(`${renderer.success(`API ready: ${apiUrl}`)}\n`);
88
+ process.stdout.write(`CompanyHelm CLI: ${versions.cliPackage}\n`);
89
+ process.stdout.write(`Runner package: ${versions.runnerPackage}\n`);
90
+ process.stdout.write(`API image: ${versions.images.api}\n`);
91
+ process.stdout.write(`Frontend image: ${versions.images.frontend}\n`);
92
+ process.stdout.write(`Postgres image: ${versions.images.postgres}\n`);
93
+ process.stdout.write(`\n${renderer.success("CompanyHelm started successfully.")}\n`);
94
+ process.stdout.write(`${renderer.successHighlight("UI URL")}\n`);
95
+ process.stdout.write(`${renderer.clickableUrl(uiUrl)}\n`);
96
+ process.stdout.write(`${renderer.successHighlight("Login credentials")}\n`);
97
+ process.stdout.write(`username: ${state.auth.username}\n`);
98
+ process.stdout.write(`password: ${state.auth.password}\n`);
57
99
  },
58
100
  async down() {
59
101
  if (!stateStore.load()) {
@@ -68,15 +110,36 @@ export function createDefaultDependencies() {
68
110
  }
69
111
  await dockerStackManager.down();
70
112
  },
71
- status() {
72
- return statusService.read();
113
+ async status() {
114
+ const services = await statusService.read();
115
+ const state = stateStore.load();
116
+ return {
117
+ services,
118
+ apiUrl: state ? `http://127.0.0.1:${state.ports.apiHttp}/graphql` : undefined,
119
+ uiUrl: state ? `http://127.0.0.1:${state.ports.ui}` : undefined,
120
+ username: state?.auth.username,
121
+ versions: versionCatalog.resolve()
122
+ };
73
123
  },
74
124
  logs(service) {
75
125
  return logsService.stream(service);
76
126
  },
77
- async reset() {
78
- await this.down();
127
+ async reset(options = {}) {
128
+ if (stateStore.load()) {
129
+ const stopCommand = runnerSupervisor.buildStopArgs();
130
+ try {
131
+ await commandRunner.run(stopCommand.command, stopCommand.args);
132
+ }
133
+ catch {
134
+ // Ignore runner stop failures during teardown so docker cleanup still runs.
135
+ }
136
+ }
137
+ await dockerStackManager.down({ removeVolumes: true });
138
+ fs.rmSync(projectPaths.apiEnvPath(), { force: true });
79
139
  fs.rmSync(root, { recursive: true, force: true });
140
+ if (options.removeGithubAppConfig) {
141
+ githubAppConfigStore.delete();
142
+ }
80
143
  }
81
144
  };
82
145
  }
@@ -0,0 +1,6 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ export declare class InteractiveCommandCancelledError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function requireInteractiveTerminal(input: Readable, output: Writable, message: string): void;
6
+ export declare function unwrapPromptResult<T>(value: T | symbol, message: string, output: Writable): T;
@@ -0,0 +1,22 @@
1
+ import * as clack from "@clack/prompts";
2
+ export class InteractiveCommandCancelledError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "InteractiveCommandCancelledError";
6
+ }
7
+ }
8
+ function isReadableTty(input) {
9
+ return "isTTY" in input && Boolean(input.isTTY);
10
+ }
11
+ export function requireInteractiveTerminal(input, output, message) {
12
+ if (!isReadableTty(input) || !clack.isTTY(output)) {
13
+ throw new Error(message);
14
+ }
15
+ }
16
+ export function unwrapPromptResult(value, message, output) {
17
+ if (clack.isCancel(value)) {
18
+ clack.cancel(message, { output });
19
+ throw new InteractiveCommandCancelledError(message);
20
+ }
21
+ return value;
22
+ }
@@ -1,9 +1,14 @@
1
+ const AVAILABLE_LOG_SERVICES = ["postgres", "api", "frontend", "runner"];
1
2
  export function registerLogsCommand(program, dependencies) {
2
3
  program
3
4
  .command("logs")
4
5
  .description("Show logs for a managed service.")
5
- .argument("<service>")
6
+ .argument("[service]")
6
7
  .action(async (service) => {
8
+ if (!service) {
9
+ process.stdout.write(`Available services:\n${AVAILABLE_LOG_SERVICES.map((name) => `- ${name}`).join("\n")}\n`);
10
+ return;
11
+ }
7
12
  await dependencies.logs(service);
8
13
  });
9
14
  }
@@ -3,14 +3,18 @@ import { createDefaultDependencies } from "./dependencies.js";
3
3
  import { registerDownCommand } from "./down.js";
4
4
  import { registerLogsCommand } from "./logs.js";
5
5
  import { registerResetCommand } from "./reset.js";
6
+ import { registerSetupGithubAppCommand } from "./setup-github-app.js";
7
+ import { registerSetImageVersionCommand } from "./set-image-version.js";
6
8
  import { registerStatusCommand } from "./status.js";
7
9
  import { registerUpCommand } from "./up.js";
8
10
  export function buildProgram(dependencies = createDefaultDependencies()) {
9
11
  const program = new Command().name("companyhelm");
12
+ registerSetupGithubAppCommand(program);
10
13
  registerUpCommand(program, dependencies);
11
14
  registerDownCommand(program, dependencies);
12
15
  registerStatusCommand(program, dependencies);
13
16
  registerLogsCommand(program, dependencies);
17
+ registerSetImageVersionCommand(program);
14
18
  registerResetCommand(program, dependencies);
15
19
  return program;
16
20
  }
@@ -1,3 +1,7 @@
1
+ import type { Readable, Writable } from "node:stream";
1
2
  import type { Command } from "commander";
3
+ import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
2
4
  import type { CommandDependencies } from "./dependencies.js";
5
+ export declare function confirmReset(input?: Readable, output?: Writable): Promise<boolean>;
6
+ export declare function confirmRemoveGithubAppConfig(store?: GithubAppConfigStore, input?: Readable, output?: Writable): Promise<boolean>;
3
7
  export declare function registerResetCommand(program: Command, dependencies: CommandDependencies): void;
@@ -1,12 +1,51 @@
1
+ import * as clack from "@clack/prompts";
2
+ import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
3
+ import { requireInteractiveTerminal, unwrapPromptResult } from "./interactive.js";
4
+ export async function confirmReset(input = process.stdin, output = process.stdout) {
5
+ requireInteractiveTerminal(input, output, "reset requires confirmation from a TTY. Re-run with --yes to skip the prompt.");
6
+ const confirmed = await clack.confirm({
7
+ message: "This will remove CompanyHelm containers, Postgres data, local runtime state, and generated .companyhelm/api/.env. Continue?",
8
+ active: "Yes",
9
+ inactive: "No",
10
+ initialValue: false,
11
+ input,
12
+ output
13
+ });
14
+ return unwrapPromptResult(confirmed, "Reset cancelled.", output);
15
+ }
16
+ export async function confirmRemoveGithubAppConfig(store = new GithubAppConfigStore(), input = process.stdin, output = process.stdout) {
17
+ if (!store.hasConfig()) {
18
+ return false;
19
+ }
20
+ requireInteractiveTerminal(input, output, "reset requires confirmation from a TTY. Re-run with --yes to skip the prompt.");
21
+ const confirmed = await clack.confirm({
22
+ message: `Also remove the machine GitHub App config at ${store.configPath()}?`,
23
+ active: "Remove it",
24
+ inactive: "Keep it",
25
+ initialValue: false,
26
+ input,
27
+ output
28
+ });
29
+ return unwrapPromptResult(confirmed, "Reset cancelled.", output);
30
+ }
1
31
  export function registerResetCommand(program, dependencies) {
2
32
  program
3
33
  .command("reset")
4
34
  .description("Destroy the local deployment state.")
5
- .option("--force")
35
+ .option("-y, --yes", "Skip the confirmation prompt.")
36
+ .option("--remove-github-app-config", "Also remove the machine GitHub App config.")
6
37
  .action(async (options) => {
7
- if (!options.force) {
8
- throw new Error("reset requires --force");
38
+ let removeGithubAppConfig = Boolean(options.removeGithubAppConfig);
39
+ if (!options.yes) {
40
+ const confirmed = await confirmReset();
41
+ if (!confirmed) {
42
+ clack.cancel("Reset cancelled.");
43
+ return;
44
+ }
45
+ if (!removeGithubAppConfig) {
46
+ removeGithubAppConfig = await confirmRemoveGithubAppConfig();
47
+ }
9
48
  }
10
- await dependencies.reset();
49
+ await dependencies.reset({ removeGithubAppConfig });
11
50
  });
12
51
  }
@@ -0,0 +1,31 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import type { Command } from "commander";
3
+ import { type ManagedImageService } from "../core/runtime/ManagedImages.js";
4
+ export interface SetImageVersionOptions {
5
+ service?: string;
6
+ limit: number;
7
+ }
8
+ export interface AvailableImageTag {
9
+ tag: string;
10
+ createdAt?: string;
11
+ }
12
+ export interface InteractiveImageSelector {
13
+ listAvailableTags(service: ManagedImageService, limit: number): Promise<AvailableImageTag[]>;
14
+ buildImageReference(service: ManagedImageService, tag: string): string;
15
+ }
16
+ export interface ImageConfigStore {
17
+ load(): {
18
+ images: Partial<Record<ManagedImageService, string>>;
19
+ };
20
+ setImage(service: ManagedImageService, image: string): {
21
+ configPath: string;
22
+ image: string;
23
+ };
24
+ }
25
+ export declare function runSetImageVersion(options: SetImageVersionOptions, dependencies?: {
26
+ input?: Readable;
27
+ output?: Writable;
28
+ registry?: InteractiveImageSelector;
29
+ configStore?: ImageConfigStore;
30
+ }): Promise<void>;
31
+ export declare function registerSetImageVersionCommand(program: Command): void;
@@ -0,0 +1,87 @@
1
+ import * as clack from "@clack/prompts";
2
+ import { LocalConfigStore } from "../core/runtime/LocalConfigStore.js";
3
+ import { MANAGED_IMAGE_SERVICES, requireManagedImageService } from "../core/runtime/ManagedImages.js";
4
+ import { PublicImageTagRegistry } from "../core/runtime/PublicImageTagRegistry.js";
5
+ import { requireInteractiveTerminal, unwrapPromptResult } from "./interactive.js";
6
+ function parsePositiveInteger(value) {
7
+ const parsed = Number.parseInt(value, 10);
8
+ if (!Number.isInteger(parsed) || parsed < 1) {
9
+ throw new Error(`Expected a positive integer, received: ${value}`);
10
+ }
11
+ return parsed;
12
+ }
13
+ async function promptForSelection(message, options, input, output, defaultValue) {
14
+ requireInteractiveTerminal(input, output, "set-image-version requires a TTY so you can choose an image interactively.");
15
+ if (options.length === 0) {
16
+ throw new Error("No selectable options were provided.");
17
+ }
18
+ const selected = await clack.select({
19
+ message,
20
+ options: options.map((option, index) => ({
21
+ ...option,
22
+ hint: option.value === defaultValue ? "current" : undefined
23
+ })),
24
+ initialValue: defaultValue,
25
+ input,
26
+ output
27
+ });
28
+ return unwrapPromptResult(selected, "Image selection cancelled.", output);
29
+ }
30
+ async function loadAvailableTags(registry, service, limit, output) {
31
+ const spinner = clack.spinner({ output });
32
+ spinner.start(`Loading the latest ${limit} image tags for ${service}`);
33
+ let tags;
34
+ try {
35
+ tags = await registry.listAvailableTags(service, limit);
36
+ }
37
+ catch (error) {
38
+ spinner.stop("Unable to load image tags");
39
+ throw error;
40
+ }
41
+ if (tags.length === 0) {
42
+ spinner.stop("No image tags found");
43
+ throw new Error(`No image tags found for ${service}.`);
44
+ }
45
+ spinner.stop(`Loaded ${tags.length} image tag${tags.length === 1 ? "" : "s"}`);
46
+ return tags;
47
+ }
48
+ function formatTagTimestamp(createdAt) {
49
+ if (!createdAt) {
50
+ return "timestamp unavailable";
51
+ }
52
+ const timestamp = new Date(createdAt);
53
+ if (Number.isNaN(timestamp.valueOf())) {
54
+ return "timestamp unavailable";
55
+ }
56
+ return timestamp.toISOString().slice(0, 16).replace("T", " ") + " UTC";
57
+ }
58
+ export async function runSetImageVersion(options, dependencies = {}) {
59
+ const input = dependencies.input ?? process.stdin;
60
+ const output = dependencies.output ?? process.stdout;
61
+ const registry = dependencies.registry ?? new PublicImageTagRegistry();
62
+ const configStore = dependencies.configStore ?? new LocalConfigStore();
63
+ clack.intro("CompanyHelm image selection", { output });
64
+ const selectedService = options.service
65
+ ? requireManagedImageService(options.service)
66
+ : await promptForSelection("Which image do you want to pin?", MANAGED_IMAGE_SERVICES.map((service) => ({ value: service, label: service })), input, output).then((value) => requireManagedImageService(value));
67
+ const currentImage = configStore.load().images[selectedService];
68
+ clack.log.info(`Current configured image for ${selectedService}: ${currentImage ?? "default (latest)"}`, { output });
69
+ const tags = await loadAvailableTags(registry, selectedService, options.limit, output);
70
+ const currentTag = currentImage ? currentImage.slice(currentImage.lastIndexOf(":") + 1) : undefined;
71
+ const selectedTag = await promptForSelection(`Choose the ${selectedService} image tag`, tags.map((tag) => ({
72
+ value: tag.tag,
73
+ label: `${tag.tag} (${formatTagTimestamp(tag.createdAt)})`
74
+ })), input, output, currentTag);
75
+ const result = configStore.setImage(selectedService, registry.buildImageReference(selectedService, selectedTag));
76
+ clack.outro(`Updated ${result.configPath} to ${result.image}`, { output });
77
+ }
78
+ export function registerSetImageVersionCommand(program) {
79
+ program
80
+ .command("set-image-version")
81
+ .description("Interactively choose an API or frontend image tag and store it in local config.yaml.")
82
+ .option("-s, --service <service>", "Prefill the service to update (api or frontend)")
83
+ .option("-l, --limit <count>", "How many image tags to show", parsePositiveInteger, 20)
84
+ .action(async (options) => {
85
+ await runSetImageVersion(options);
86
+ });
87
+ }
@@ -0,0 +1,10 @@
1
+ import { Writable, type Readable } from "node:stream";
2
+ import type { Command } from "commander";
3
+ import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
4
+ import { type GithubAppConfig } from "../core/config/GithubAppConfig.js";
5
+ type BrowserUrlOpener = (url: string) => Promise<void>;
6
+ export declare function readPemFromTerminal(input?: Readable, output?: Writable): Promise<string>;
7
+ export declare function promptGithubAppConfig(input?: Readable, output?: Writable, openBrowser?: BrowserUrlOpener): Promise<GithubAppConfig>;
8
+ export declare function ensureGithubAppConfig(store?: GithubAppConfigStore, input?: Readable, output?: Writable): Promise<GithubAppConfig>;
9
+ export declare function registerSetupGithubAppCommand(program: Command, store?: GithubAppConfigStore): void;
10
+ export {};