@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.
- package/LICENSE +21 -0
- package/README.md +40 -33
- package/dist/cli.js +11 -1
- package/dist/commands/dependencies.d.ts +18 -3
- package/dist/commands/dependencies.js +76 -13
- package/dist/commands/interactive.d.ts +6 -0
- package/dist/commands/interactive.js +22 -0
- package/dist/commands/logs.js +6 -1
- package/dist/commands/register-commands.js +4 -0
- package/dist/commands/reset.d.ts +4 -0
- package/dist/commands/reset.js +43 -4
- package/dist/commands/set-image-version.d.ts +31 -0
- package/dist/commands/set-image-version.js +87 -0
- package/dist/commands/setup-github-app.d.ts +10 -0
- package/dist/commands/setup-github-app.js +211 -0
- package/dist/commands/status.js +3 -1
- package/dist/commands/up.js +11 -2
- package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +2 -2
- package/dist/core/bootstrap/DeploymentBootstrapper.js +5 -7
- package/dist/core/bootstrap/SeedSqlRenderer.js +23 -5
- package/dist/core/config/ApiEnvFileWriter.d.ts +6 -0
- package/dist/core/config/ApiEnvFileWriter.js +26 -0
- package/dist/core/config/GithubAppConfig.d.ts +6 -0
- package/dist/core/config/GithubAppConfig.js +26 -0
- package/dist/core/config/GithubAppConfigStore.d.ts +11 -0
- package/dist/core/config/GithubAppConfigStore.js +65 -0
- package/dist/core/docker/ComposeTemplateRenderer.d.ts +6 -1
- package/dist/core/docker/ComposeTemplateRenderer.js +22 -4
- package/dist/core/docker/DockerStackManager.d.ts +15 -3
- package/dist/core/docker/DockerStackManager.js +67 -8
- package/dist/core/runner/RunnerSupervisor.d.ts +4 -0
- package/dist/core/runner/RunnerSupervisor.js +19 -3
- package/dist/core/runtime/ImageCatalog.js +5 -2
- package/dist/core/runtime/LocalConfigStore.d.ts +16 -0
- package/dist/core/runtime/LocalConfigStore.js +59 -0
- package/dist/core/runtime/ManagedImages.d.ts +10 -0
- package/dist/core/runtime/ManagedImages.js +27 -0
- package/dist/core/runtime/ProjectPaths.d.ts +7 -0
- package/dist/core/runtime/ProjectPaths.js +16 -0
- package/dist/core/runtime/PublicImageTagRegistry.d.ts +16 -0
- package/dist/core/runtime/PublicImageTagRegistry.js +148 -0
- package/dist/core/runtime/RuntimeState.d.ts +1 -1
- package/dist/core/runtime/RuntimeStateStore.d.ts +1 -0
- package/dist/core/runtime/RuntimeStateStore.js +8 -2
- package/dist/core/runtime/VersionCatalog.d.ts +10 -0
- package/dist/core/runtime/VersionCatalog.js +21 -0
- package/dist/core/status/StatusService.d.ts +5 -1
- package/dist/core/status/StatusService.js +5 -2
- package/dist/core/ui/TerminalRenderer.d.ts +10 -0
- package/dist/core/ui/TerminalRenderer.js +48 -0
- package/dist/templates/docker-compose.yaml.tpl +4 -13
- package/dist/templates/seed.sql.tpl +32 -13
- package/package.json +7 -3
- package/src/templates/docker-compose.yaml.tpl +4 -13
- 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
|
|
1
|
+
# CompanyHelm - Distributed AI Agent Orchestration
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
+
[Website](https://www.companyhelm.com/)
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
20
|
+
After startup, the CLI prints:
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```
|
|
22
|
+
- the local dashboard UI URL
|
|
23
|
+
- the generated username and password
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
## What CompanyHelm is
|
|
20
26
|
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
38
|
+
## What the CLI boots locally
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
- `api`
|
|
35
|
-
- `frontend`
|
|
36
|
-
- `runner`
|
|
40
|
+
`npx @companyhelm/cli up` brings up a local CompanyHelm stack with:
|
|
37
41
|
|
|
38
|
-
|
|
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
|
-
##
|
|
48
|
+
## Command reference
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
For the full CLI help:
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
```bash
|
|
53
|
+
npx @companyhelm/cli --help
|
|
54
|
+
```
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
Common commands:
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
process.stdout.write(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -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("
|
|
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
|
}
|
package/dist/commands/reset.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/reset.js
CHANGED
|
@@ -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("--
|
|
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
|
-
|
|
8
|
-
|
|
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 {};
|