@companyhelm/cli 0.1.5 → 0.3.0
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/README.md +6 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -2
- package/dist/commands/dependencies.js +25 -6
- package/dist/commands/interactive.d.ts +1 -0
- package/dist/commands/interactive.js +4 -1
- package/dist/commands/logs.js +2 -2
- package/dist/commands/register-commands.js +4 -1
- package/dist/commands/reset.js +1 -1
- package/dist/commands/set-image-version.js +1 -1
- package/dist/commands/setup-github-app.d.ts +4 -1
- package/dist/commands/setup-github-app.js +30 -8
- package/dist/commands/startup-preferences.d.ts +3 -0
- package/dist/commands/startup-preferences.js +39 -0
- package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +2 -0
- package/dist/core/bootstrap/DeploymentBootstrapper.js +23 -3
- package/dist/core/config/ApiEnvFileWriter.d.ts +1 -1
- package/dist/core/config/ApiEnvFileWriter.js +3 -3
- package/dist/core/local/ApiLocalService.d.ts +1 -1
- package/dist/core/local/ApiLocalService.js +3 -3
- package/dist/core/logs/LogsService.d.ts +2 -1
- package/dist/core/logs/LogsService.js +5 -4
- package/dist/core/runner/RunnerSupervisor.d.ts +6 -0
- package/dist/core/runner/RunnerSupervisor.js +20 -3
- package/dist/core/runner/runner-bootstrap.d.ts +2 -0
- package/dist/core/runner/runner-bootstrap.js +48 -0
- package/dist/core/runtime/CliPackageMetadata.d.ts +3 -0
- package/dist/core/runtime/CliPackageMetadata.js +8 -0
- package/dist/core/runtime/LocalConfigStore.d.ts +6 -0
- package/dist/core/runtime/LocalConfigStore.js +27 -3
- package/dist/core/services/ManagedServiceNames.d.ts +5 -0
- package/dist/core/services/ManagedServiceNames.js +12 -0
- package/dist/preflight/ApiPortPreflightCheck.d.ts +6 -0
- package/dist/preflight/ApiPortPreflightCheck.js +10 -0
- package/dist/preflight/DockerInstalledPreflightCheck.d.ts +7 -0
- package/dist/preflight/DockerInstalledPreflightCheck.js +15 -0
- package/dist/preflight/PortAvailabilityPreflightCheck.d.ts +7 -0
- package/dist/preflight/PortAvailabilityPreflightCheck.js +31 -0
- package/dist/preflight/PostgresPortPreflightCheck.d.ts +6 -0
- package/dist/preflight/PostgresPortPreflightCheck.js +10 -0
- package/dist/preflight/PreflightCheck.d.ts +3 -0
- package/dist/preflight/PreflightCheck.js +1 -0
- package/dist/preflight/WebPortPreflightCheck.d.ts +6 -0
- package/dist/preflight/WebPortPreflightCheck.js +10 -0
- package/dist/preflight/runStartupPreflightChecks.d.ts +18 -0
- package/dist/preflight/runStartupPreflightChecks.js +42 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# CompanyHelm - Distributed AI Agent Orchestration
|
|
2
2
|
|
|
3
|
+
<a href="https://discord.gg/YueY3dQM9Q"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
|
|
4
|
+
|
|
3
5
|
CompanyHelm is an open-source control plane for running AI-agent companies in your own infrastructure.
|
|
4
6
|
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.
|
|
5
7
|
|
|
6
|
-
[Website](https://www.companyhelm.com/)
|
|
8
|
+
[Website](https://www.companyhelm.com/) · [Discord](https://discord.gg/YueY3dQM9Q)
|
|
9
|
+
|
|
10
|
+
<img width="1843" height="973" alt="Task management" src="https://github.com/user-attachments/assets/c0faa139-06ed-4299-a76f-63c186204038" />
|
|
11
|
+
|
|
7
12
|
|
|
8
13
|
## Quick start
|
|
9
14
|
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
1
4
|
import { buildProgram } from "./commands/register-commands.js";
|
|
2
5
|
import { InteractiveCommandCancelledError } from "./commands/interactive.js";
|
|
6
|
+
import { CliPackageMetadata } from "./core/runtime/CliPackageMetadata.js";
|
|
7
|
+
function shouldPrintVersion(argv = process.argv) {
|
|
8
|
+
const args = argv.slice(2);
|
|
9
|
+
return args.length === 1 && (args[0] === "--version" || args[0] === "-V");
|
|
10
|
+
}
|
|
3
11
|
export async function main(argv = process.argv) {
|
|
12
|
+
if (shouldPrintVersion(argv)) {
|
|
13
|
+
process.stdout.write(`${new CliPackageMetadata().version()}\n`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
4
16
|
const program = buildProgram();
|
|
5
17
|
try {
|
|
6
18
|
await program.parseAsync(argv);
|
|
@@ -10,9 +22,18 @@ export async function main(argv = process.argv) {
|
|
|
10
22
|
process.exitCode = 1;
|
|
11
23
|
return;
|
|
12
24
|
}
|
|
13
|
-
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
process.stderr.write(`${message}\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isCliEntrypoint(argv = process.argv) {
|
|
31
|
+
const entrypointPath = argv[1];
|
|
32
|
+
if (!entrypointPath) {
|
|
33
|
+
return false;
|
|
14
34
|
}
|
|
35
|
+
return pathToFileURL(realpathSync(entrypointPath)).href === import.meta.url;
|
|
15
36
|
}
|
|
16
|
-
if (
|
|
37
|
+
if (isCliEntrypoint()) {
|
|
17
38
|
void main();
|
|
18
39
|
}
|
|
@@ -19,7 +19,11 @@ import { RuntimeStateStore } from "../core/runtime/RuntimeStateStore.js";
|
|
|
19
19
|
import { VersionCatalog } from "../core/runtime/VersionCatalog.js";
|
|
20
20
|
import { StatusService } from "../core/status/StatusService.js";
|
|
21
21
|
import { TerminalRenderer } from "../core/ui/TerminalRenderer.js";
|
|
22
|
+
import { LocalConfigStore } from "../core/runtime/LocalConfigStore.js";
|
|
23
|
+
import { PortAllocator } from "../core/runtime/PortAllocator.js";
|
|
24
|
+
import { runStartupPreflightChecks } from "../preflight/runStartupPreflightChecks.js";
|
|
22
25
|
import { ensureGithubAppConfig } from "./setup-github-app.js";
|
|
26
|
+
import { ensureAgentWorkspaceMode } from "./startup-preferences.js";
|
|
23
27
|
function runtimeRoot() {
|
|
24
28
|
return process.env.COMPANYHELM_HOME || path.join(os.homedir(), ".companyhelm");
|
|
25
29
|
}
|
|
@@ -36,6 +40,7 @@ export function createDefaultDependencies() {
|
|
|
36
40
|
const apiEnvFileWriter = new ApiEnvFileWriter(process.cwd());
|
|
37
41
|
const projectPaths = new ProjectPaths(process.cwd());
|
|
38
42
|
const localRepoSourceResolver = new LocalRepoSourceResolver(process.cwd());
|
|
43
|
+
const localConfigStore = new LocalConfigStore(root);
|
|
39
44
|
const localServiceProcessManager = new LocalServiceProcessManager();
|
|
40
45
|
const apiLocalService = new ApiLocalService(localServiceProcessManager, commandRunner);
|
|
41
46
|
const webLocalService = new WebLocalService(localServiceProcessManager, commandRunner);
|
|
@@ -86,11 +91,21 @@ export function createDefaultDependencies() {
|
|
|
86
91
|
async up(options = {}) {
|
|
87
92
|
const logLevel = options.logLevel ?? "info";
|
|
88
93
|
const useHostDockerRuntime = options.useHostDockerRuntime ?? false;
|
|
89
|
-
const githubAppConfig = await ensureGithubAppConfig(githubAppConfigStore, process.stdin, process.stdout);
|
|
90
|
-
const state = stateStore.initialize();
|
|
91
94
|
process.stdout.write(`${renderer.renderBanner()}\n`);
|
|
92
|
-
const runnerAlreadyRunning = await isRunnerRunning(commandRunner, runnerSupervisor);
|
|
93
95
|
const desiredSources = localRepoSourceResolver.resolve(options);
|
|
96
|
+
const currentState = stateStore.load();
|
|
97
|
+
const allocatedPorts = currentState?.ports ?? new PortAllocator().allocate();
|
|
98
|
+
await runStartupPreflightChecks({
|
|
99
|
+
commandRunner,
|
|
100
|
+
currentState,
|
|
101
|
+
desiredSources,
|
|
102
|
+
ports: allocatedPorts,
|
|
103
|
+
readStatus: () => statusService.read()
|
|
104
|
+
});
|
|
105
|
+
const workspaceMode = await ensureAgentWorkspaceMode(localConfigStore, process.stdin, process.stdout);
|
|
106
|
+
const githubAppConfig = await ensureGithubAppConfig(githubAppConfigStore, process.stdin, process.stdout, { workspaceMode });
|
|
107
|
+
const state = currentState ?? stateStore.initialize();
|
|
108
|
+
const runnerAlreadyRunning = await isRunnerRunning(commandRunner, runnerSupervisor);
|
|
94
109
|
const versions = versionCatalog.resolve();
|
|
95
110
|
const passwordRecord = createPasswordHash(state.auth.password);
|
|
96
111
|
const startedLocalServices = [];
|
|
@@ -100,7 +115,8 @@ export function createDefaultDependencies() {
|
|
|
100
115
|
apiEnvFileWriter.write(githubAppConfig);
|
|
101
116
|
bootstrapper.writeSeedSql(root, state, passwordRecord.passwordHash, passwordRecord.passwordSalt);
|
|
102
117
|
bootstrapper.writeApiConfig(root, state, logLevel, {
|
|
103
|
-
databaseHost: desiredSources.api.source === "local" ? "127.0.0.1" : "postgres"
|
|
118
|
+
databaseHost: desiredSources.api.source === "local" ? "127.0.0.1" : "postgres",
|
|
119
|
+
githubAppConfig
|
|
104
120
|
});
|
|
105
121
|
bootstrapper.writeFrontendConfig(root, state);
|
|
106
122
|
await stopLocalServicesFromState(stateStore.load(), localServiceProcessManager);
|
|
@@ -145,10 +161,12 @@ export function createDefaultDependencies() {
|
|
|
145
161
|
logPath: runtimePaths.runnerLogPath(),
|
|
146
162
|
secret: state.runner.secret,
|
|
147
163
|
logLevel,
|
|
148
|
-
useHostDockerRuntime
|
|
164
|
+
useHostDockerRuntime,
|
|
165
|
+
workspaceMode,
|
|
166
|
+
projectRoot: process.cwd()
|
|
149
167
|
});
|
|
150
168
|
process.stdout.write(`${renderer.progress("Starting the runner...")}\n`);
|
|
151
|
-
await commandRunner.run(startCommand.command, startCommand.args);
|
|
169
|
+
await commandRunner.run(startCommand.command, startCommand.args, undefined, startCommand.env);
|
|
152
170
|
runnerStarted = true;
|
|
153
171
|
}
|
|
154
172
|
if (desiredSources.frontend.source === "local") {
|
|
@@ -247,6 +265,7 @@ export function createDefaultDependencies() {
|
|
|
247
265
|
}
|
|
248
266
|
await dockerStackManager.down({ removeVolumes: true });
|
|
249
267
|
fs.rmSync(projectPaths.apiEnvPath(), { force: true });
|
|
268
|
+
fs.rmSync(localConfigStore.configPath(), { force: true });
|
|
250
269
|
fs.rmSync(root, { recursive: true, force: true });
|
|
251
270
|
if (options.removeGithubAppConfig) {
|
|
252
271
|
githubAppConfigStore.delete();
|
|
@@ -2,5 +2,6 @@ import type { Readable, Writable } from "node:stream";
|
|
|
2
2
|
export declare class InteractiveCommandCancelledError extends Error {
|
|
3
3
|
constructor(message: string);
|
|
4
4
|
}
|
|
5
|
+
export declare function hasInteractiveTerminal(input: Readable, output: Writable): boolean;
|
|
5
6
|
export declare function requireInteractiveTerminal(input: Readable, output: Writable, message: string): void;
|
|
6
7
|
export declare function unwrapPromptResult<T>(value: T | symbol, message: string, output: Writable): T;
|
|
@@ -8,8 +8,11 @@ export class InteractiveCommandCancelledError extends Error {
|
|
|
8
8
|
function isReadableTty(input) {
|
|
9
9
|
return "isTTY" in input && Boolean(input.isTTY);
|
|
10
10
|
}
|
|
11
|
+
export function hasInteractiveTerminal(input, output) {
|
|
12
|
+
return isReadableTty(input) && clack.isTTY(output);
|
|
13
|
+
}
|
|
11
14
|
export function requireInteractiveTerminal(input, output, message) {
|
|
12
|
-
if (!
|
|
15
|
+
if (!hasInteractiveTerminal(input, output)) {
|
|
13
16
|
throw new Error(message);
|
|
14
17
|
}
|
|
15
18
|
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { AVAILABLE_MANAGED_SERVICE_NAMES } from "../core/services/ManagedServiceNames.js";
|
|
2
2
|
export function registerLogsCommand(program, dependencies) {
|
|
3
3
|
program
|
|
4
4
|
.command("logs")
|
|
@@ -6,7 +6,7 @@ export function registerLogsCommand(program, dependencies) {
|
|
|
6
6
|
.argument("[service]")
|
|
7
7
|
.action(async (service) => {
|
|
8
8
|
if (!service) {
|
|
9
|
-
process.stdout.write(`Available services:\n${
|
|
9
|
+
process.stdout.write(`Available services:\n${AVAILABLE_MANAGED_SERVICE_NAMES.map((name) => `- ${name}`).join("\n")}\n`);
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
await dependencies.logs(service);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { createDefaultDependencies } from "./dependencies.js";
|
|
3
|
+
import { CliPackageMetadata } from "../core/runtime/CliPackageMetadata.js";
|
|
3
4
|
import { registerDownCommand } from "./down.js";
|
|
4
5
|
import { registerLogsCommand } from "./logs.js";
|
|
5
6
|
import { registerResetCommand } from "./reset.js";
|
|
@@ -8,7 +9,9 @@ import { registerSetImageVersionCommand } from "./set-image-version.js";
|
|
|
8
9
|
import { registerStatusCommand } from "./status.js";
|
|
9
10
|
import { registerUpCommand } from "./up.js";
|
|
10
11
|
export function buildProgram(dependencies = createDefaultDependencies()) {
|
|
11
|
-
const program = new Command()
|
|
12
|
+
const program = new Command()
|
|
13
|
+
.name("companyhelm")
|
|
14
|
+
.version(new CliPackageMetadata().version());
|
|
12
15
|
registerSetupGithubAppCommand(program);
|
|
13
16
|
registerUpCommand(program, dependencies);
|
|
14
17
|
registerDownCommand(program, dependencies);
|
package/dist/commands/reset.js
CHANGED
|
@@ -4,7 +4,7 @@ import { requireInteractiveTerminal, unwrapPromptResult } from "./interactive.js
|
|
|
4
4
|
export async function confirmReset(input = process.stdin, output = process.stdout) {
|
|
5
5
|
requireInteractiveTerminal(input, output, "reset requires confirmation from a TTY. Re-run with --yes to skip the prompt.");
|
|
6
6
|
const confirmed = await clack.confirm({
|
|
7
|
-
message: "This will remove CompanyHelm containers, Postgres data, local runtime state,
|
|
7
|
+
message: "This will remove CompanyHelm containers, Postgres data, local runtime state, generated .companyhelm/api/.env, and the CompanyHelm home config. Continue?",
|
|
8
8
|
active: "Yes",
|
|
9
9
|
inactive: "No",
|
|
10
10
|
initialValue: false,
|
|
@@ -78,7 +78,7 @@ export async function runSetImageVersion(options, dependencies = {}) {
|
|
|
78
78
|
export function registerSetImageVersionCommand(program) {
|
|
79
79
|
program
|
|
80
80
|
.command("set-image-version")
|
|
81
|
-
.description("Interactively choose an API or frontend image tag and store it in
|
|
81
|
+
.description("Interactively choose an API or frontend image tag and store it in the CompanyHelm home config.")
|
|
82
82
|
.option("-s, --service <service>", "Prefill the service to update (api or frontend)")
|
|
83
83
|
.option("-l, --limit <count>", "How many image tags to show", parsePositiveInteger, 20)
|
|
84
84
|
.action(async (options) => {
|
|
@@ -2,9 +2,12 @@ import { Writable, type Readable } from "node:stream";
|
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
|
|
4
4
|
import { type GithubAppConfig } from "../core/config/GithubAppConfig.js";
|
|
5
|
+
import type { AgentWorkspaceMode } from "../core/runtime/LocalConfigStore.js";
|
|
5
6
|
type BrowserUrlOpener = (url: string) => Promise<void>;
|
|
6
7
|
export declare function readPemFromTerminal(input?: Readable, output?: Writable): Promise<string>;
|
|
7
8
|
export declare function promptGithubAppConfig(input?: Readable, output?: Writable, openBrowser?: BrowserUrlOpener): Promise<GithubAppConfig>;
|
|
8
|
-
export declare function ensureGithubAppConfig(store?: GithubAppConfigStore, input?: Readable, output?: Writable
|
|
9
|
+
export declare function ensureGithubAppConfig(store?: GithubAppConfigStore, input?: Readable, output?: Writable, options?: {
|
|
10
|
+
workspaceMode?: AgentWorkspaceMode;
|
|
11
|
+
}): Promise<GithubAppConfig | null>;
|
|
9
12
|
export declare function registerSetupGithubAppCommand(program: Command, store?: GithubAppConfigStore): void;
|
|
10
13
|
export {};
|
|
@@ -3,7 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
import { Writable } from "node:stream";
|
|
6
|
-
import { unwrapPromptResult, requireInteractiveTerminal, InteractiveCommandCancelledError } from "./interactive.js";
|
|
6
|
+
import { unwrapPromptResult, requireInteractiveTerminal, InteractiveCommandCancelledError, hasInteractiveTerminal } from "./interactive.js";
|
|
7
7
|
import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
|
|
8
8
|
import { normalizeGithubAppConfig } from "../core/config/GithubAppConfig.js";
|
|
9
9
|
const GITHUB_NEW_APP_URL = "https://github.com/settings/apps/new";
|
|
@@ -176,17 +176,39 @@ export async function promptGithubAppConfig(input = process.stdin, output = proc
|
|
|
176
176
|
appPrivateKeyPem,
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
|
-
export async function ensureGithubAppConfig(store = new GithubAppConfigStore(), input = process.stdin, output = process.stdout) {
|
|
179
|
+
export async function ensureGithubAppConfig(store = new GithubAppConfigStore(), input = process.stdin, output = process.stdout, options = {}) {
|
|
180
180
|
const existingConfig = store.load();
|
|
181
181
|
if (existingConfig) {
|
|
182
182
|
return existingConfig;
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
if (!hasInteractiveTerminal(input, output)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const workspaceMode = options.workspaceMode ?? "dedicated";
|
|
188
|
+
clack.intro("CompanyHelm GitHub auth", { output });
|
|
189
|
+
clack.note(workspaceMode === "dedicated"
|
|
190
|
+
? [
|
|
191
|
+
"No machine GitHub App config was found.",
|
|
192
|
+
"Dedicated workspaces cannot access files from your host system.",
|
|
193
|
+
"GitHub auth is recommended so agents can clone and work in your repositories.",
|
|
194
|
+
].join("\n")
|
|
195
|
+
: [
|
|
196
|
+
"No machine GitHub App config was found.",
|
|
197
|
+
"GitHub auth is optional in current working directory mode.",
|
|
198
|
+
"Set it up now if you want agents to access GitHub directly from this deployment.",
|
|
199
|
+
].join("\n"), "Optional setup", { output });
|
|
200
|
+
const shouldSetup = unwrapPromptResult(await clack.confirm({
|
|
201
|
+
message: "Set up GitHub auth now?",
|
|
202
|
+
active: "Set it up",
|
|
203
|
+
inactive: "Skip for now",
|
|
204
|
+
initialValue: false,
|
|
205
|
+
input,
|
|
206
|
+
output,
|
|
207
|
+
}), "GitHub App setup cancelled.", output);
|
|
208
|
+
if (!shouldSetup) {
|
|
209
|
+
clack.outro("Skipping GitHub App setup. Continuing startup without GitHub access.", { output });
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
190
212
|
const config = await promptGithubAppConfig(input, output);
|
|
191
213
|
const spinner = clack.spinner({ output });
|
|
192
214
|
spinner.start("Saving machine GitHub App config");
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import { LocalConfigStore, type AgentWorkspaceMode } from "../core/runtime/LocalConfigStore.js";
|
|
3
|
+
export declare function ensureAgentWorkspaceMode(store?: LocalConfigStore, input?: Readable, output?: Writable): Promise<AgentWorkspaceMode>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
import { unwrapPromptResult, hasInteractiveTerminal } from "./interactive.js";
|
|
3
|
+
import { LocalConfigStore } from "../core/runtime/LocalConfigStore.js";
|
|
4
|
+
const DEFAULT_WORKSPACE_MODE = "current-working-directory";
|
|
5
|
+
export async function ensureAgentWorkspaceMode(store = new LocalConfigStore(), input = process.stdin, output = process.stdout) {
|
|
6
|
+
const existingMode = store.load().agentWorkspaceMode;
|
|
7
|
+
if (existingMode) {
|
|
8
|
+
return existingMode;
|
|
9
|
+
}
|
|
10
|
+
if (!hasInteractiveTerminal(input, output)) {
|
|
11
|
+
return DEFAULT_WORKSPACE_MODE;
|
|
12
|
+
}
|
|
13
|
+
clack.note([
|
|
14
|
+
"Choose where agent threads should run.",
|
|
15
|
+
"Dedicated workspaces keep agents isolated from your host filesystem.",
|
|
16
|
+
"In dedicated mode agents will not have access to files on your system, so GitHub auth is recommended if you want them to clone your repositories.",
|
|
17
|
+
"Current working directory mode mounts this checkout directly into agent threads.",
|
|
18
|
+
].join("\n"), "Agent workspace", { output });
|
|
19
|
+
const selectedMode = unwrapPromptResult(await clack.select({
|
|
20
|
+
message: "Where should agents operate?",
|
|
21
|
+
options: [
|
|
22
|
+
{
|
|
23
|
+
value: "current-working-directory",
|
|
24
|
+
label: "Current working directory",
|
|
25
|
+
hint: "recommended: agents work directly in this checkout"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: "dedicated",
|
|
29
|
+
label: "Dedicated workspaces directory",
|
|
30
|
+
hint: "isolated thread workspaces"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
initialValue: DEFAULT_WORKSPACE_MODE,
|
|
34
|
+
input,
|
|
35
|
+
output
|
|
36
|
+
}), "Workspace selection cancelled.", output);
|
|
37
|
+
store.setAgentWorkspaceMode(selectedMode);
|
|
38
|
+
return selectedMode;
|
|
39
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LogLevel } from "../../commands/dependencies.js";
|
|
2
|
+
import type { GithubAppConfig } from "../config/GithubAppConfig.js";
|
|
2
3
|
import type { RuntimeState } from "../runtime/RuntimeState.js";
|
|
3
4
|
export declare class DeploymentBootstrapper {
|
|
4
5
|
private readonly renderer;
|
|
@@ -8,6 +9,7 @@ export declare class DeploymentBootstrapper {
|
|
|
8
9
|
appPort?: number;
|
|
9
10
|
runnerGrpcPort?: number;
|
|
10
11
|
agentGrpcPort?: number;
|
|
12
|
+
githubAppConfig?: GithubAppConfig | null;
|
|
11
13
|
}): string;
|
|
12
14
|
writeFrontendConfig(root: string, state: RuntimeState): string;
|
|
13
15
|
private indentBlock;
|
|
@@ -25,6 +25,17 @@ export class DeploymentBootstrapper {
|
|
|
25
25
|
const runnerGrpcPort = options.runnerGrpcPort ?? state.ports.runnerGrpc;
|
|
26
26
|
const agentGrpcPort = options.agentGrpcPort ?? state.ports.agentCliGrpc;
|
|
27
27
|
const databaseHost = options.databaseHost ?? "postgres";
|
|
28
|
+
const githubConfigLines = options.githubAppConfig
|
|
29
|
+
? [
|
|
30
|
+
' app_client_id: "${GITHUB_APP_CLIENT_ID}"',
|
|
31
|
+
' app_private_key_pem: "${GITHUB_APP_PRIVATE_KEY_PEM}"',
|
|
32
|
+
' app_link: "${GITHUB_APP_URL}"'
|
|
33
|
+
]
|
|
34
|
+
: [
|
|
35
|
+
' app_client_id: "companyhelm-local-github-disabled"',
|
|
36
|
+
' app_private_key_pem: "companyhelm-local-github-disabled"',
|
|
37
|
+
' app_link: "https://github.com/apps/companyhelm-local-disabled"'
|
|
38
|
+
];
|
|
28
39
|
const yaml = [
|
|
29
40
|
"app:",
|
|
30
41
|
' host: "0.0.0.0"',
|
|
@@ -37,6 +48,17 @@ export class DeploymentBootstrapper {
|
|
|
37
48
|
" heartbeat:",
|
|
38
49
|
" intervalMs: 20000",
|
|
39
50
|
" jitterMs: 10000",
|
|
51
|
+
" workers:",
|
|
52
|
+
" agentHeartbeats:",
|
|
53
|
+
" intervalSeconds: 60",
|
|
54
|
+
" jitterSeconds: 60",
|
|
55
|
+
" batchSize: 10",
|
|
56
|
+
" leaseSeconds: 120",
|
|
57
|
+
" taskWorker:",
|
|
58
|
+
" intervalSeconds: 60",
|
|
59
|
+
" jitterSeconds: 60",
|
|
60
|
+
" batchSize: 10",
|
|
61
|
+
" leaseSeconds: 120",
|
|
40
62
|
"agent:",
|
|
41
63
|
" grpc:",
|
|
42
64
|
' host: "0.0.0.0"',
|
|
@@ -53,9 +75,7 @@ export class DeploymentBootstrapper {
|
|
|
53
75
|
' username: "postgres"',
|
|
54
76
|
' password: "postgres"',
|
|
55
77
|
"github:",
|
|
56
|
-
|
|
57
|
-
' app_private_key_pem: "${GITHUB_APP_PRIVATE_KEY_PEM}"',
|
|
58
|
-
' app_link: "${GITHUB_APP_URL}"',
|
|
78
|
+
...githubConfigLines,
|
|
59
79
|
'authProvider: "companyhelm"',
|
|
60
80
|
"auth:",
|
|
61
81
|
" companyhelm:",
|
|
@@ -15,9 +15,9 @@ export class ApiEnvFileWriter {
|
|
|
15
15
|
write(config) {
|
|
16
16
|
fs.mkdirSync(this.projectPaths.apiDirectoryPath(), { recursive: true });
|
|
17
17
|
const contents = [
|
|
18
|
-
`GITHUB_APP_URL=${escapeEnvValue(config
|
|
19
|
-
`GITHUB_APP_CLIENT_ID=${escapeEnvValue(config
|
|
20
|
-
`GITHUB_APP_PRIVATE_KEY_PEM=${escapeEnvValue(config
|
|
18
|
+
`GITHUB_APP_URL=${escapeEnvValue(config?.appUrl ?? "")}`,
|
|
19
|
+
`GITHUB_APP_CLIENT_ID=${escapeEnvValue(config?.appClientId ?? "")}`,
|
|
20
|
+
`GITHUB_APP_PRIVATE_KEY_PEM=${escapeEnvValue(config?.appPrivateKeyPem ?? "")}`,
|
|
21
21
|
"",
|
|
22
22
|
].join("\n");
|
|
23
23
|
fs.writeFileSync(this.projectPaths.apiEnvPath(), contents, "utf8");
|
|
@@ -24,9 +24,9 @@ export class ApiLocalService {
|
|
|
24
24
|
logPath: input.logPath,
|
|
25
25
|
env: {
|
|
26
26
|
APP_ENV: "local",
|
|
27
|
-
GITHUB_APP_CLIENT_ID: input.githubAppConfig
|
|
28
|
-
GITHUB_APP_URL: input.githubAppConfig
|
|
29
|
-
GITHUB_APP_PRIVATE_KEY_PEM: input.githubAppConfig
|
|
27
|
+
GITHUB_APP_CLIENT_ID: input.githubAppConfig?.appClientId ?? "",
|
|
28
|
+
GITHUB_APP_URL: input.githubAppConfig?.appUrl ?? "",
|
|
29
|
+
GITHUB_APP_PRIVATE_KEY_PEM: input.githubAppConfig?.appPrivateKeyPem ?? "",
|
|
30
30
|
COMPANYHELM_JWT_PRIVATE_KEY_PEM: input.state.auth.jwtPrivateKeyPem,
|
|
31
31
|
COMPANYHELM_JWT_PUBLIC_KEY_PEM: input.state.auth.jwtPublicKeyPem,
|
|
32
32
|
COMPANYHELM_LOG_LEVEL: input.logLevel
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type ManagedServiceKey } from "../services/ManagedServiceNames.js";
|
|
1
2
|
export declare class LogsService {
|
|
2
3
|
private readonly streamServiceLogs;
|
|
3
|
-
constructor(streamServiceLogs: (service:
|
|
4
|
+
constructor(streamServiceLogs: (service: ManagedServiceKey) => Promise<void>);
|
|
4
5
|
stream(service: string): Promise<void>;
|
|
5
6
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { AVAILABLE_MANAGED_SERVICE_NAMES, resolveManagedServiceKey } from "../services/ManagedServiceNames.js";
|
|
2
2
|
export class LogsService {
|
|
3
3
|
streamServiceLogs;
|
|
4
4
|
constructor(streamServiceLogs) {
|
|
5
5
|
this.streamServiceLogs = streamServiceLogs;
|
|
6
6
|
}
|
|
7
7
|
async stream(service) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const resolvedService = resolveManagedServiceKey(service);
|
|
9
|
+
if (!resolvedService) {
|
|
10
|
+
throw new Error(`Unknown service '${service}'. Expected one of: ${AVAILABLE_MANAGED_SERVICE_NAMES.join(", ")}`);
|
|
10
11
|
}
|
|
11
|
-
await this.streamServiceLogs(
|
|
12
|
+
await this.streamServiceLogs(resolvedService);
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LogLevel } from "../../commands/dependencies.js";
|
|
2
|
+
import type { AgentWorkspaceMode } from "../runtime/LocalConfigStore.js";
|
|
2
3
|
export interface RunnerStartInput {
|
|
3
4
|
serverUrl: string;
|
|
4
5
|
agentApiUrl: string;
|
|
@@ -6,10 +7,13 @@ export interface RunnerStartInput {
|
|
|
6
7
|
secret: string;
|
|
7
8
|
logLevel?: LogLevel;
|
|
8
9
|
useHostDockerRuntime?: boolean;
|
|
10
|
+
workspaceMode?: AgentWorkspaceMode;
|
|
11
|
+
projectRoot?: string;
|
|
9
12
|
}
|
|
10
13
|
export interface RunnerStartCommand {
|
|
11
14
|
command: string;
|
|
12
15
|
args: string[];
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
13
17
|
}
|
|
14
18
|
export declare class RunnerSupervisor {
|
|
15
19
|
private readonly configPath;
|
|
@@ -19,5 +23,7 @@ export declare class RunnerSupervisor {
|
|
|
19
23
|
buildStopArgs(): RunnerStartCommand;
|
|
20
24
|
buildStatusArgs(): RunnerStartCommand;
|
|
21
25
|
private resolveRunnerCliPath;
|
|
26
|
+
private resolveRunnerCliOverridePath;
|
|
27
|
+
private resolveRunnerEntrypointPath;
|
|
22
28
|
private resolveHostDockerPath;
|
|
23
29
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
const require = createRequire(import.meta.url);
|
|
4
5
|
const DEFAULT_HOST_DOCKER_PATH = "unix:///var/run/docker.sock";
|
|
5
6
|
export class RunnerSupervisor {
|
|
@@ -15,15 +16,23 @@ export class RunnerSupervisor {
|
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
buildStartArgs(input) {
|
|
18
|
-
const runnerCliPath = this.resolveRunnerCliPath();
|
|
19
19
|
const logLevel = (input.logLevel ?? "info").toUpperCase();
|
|
20
20
|
const hostDockerArgs = input.useHostDockerRuntime
|
|
21
21
|
? ["--use-host-docker-runtime", "--host-docker-path", this.resolveHostDockerPath()]
|
|
22
22
|
: [];
|
|
23
|
+
const runnerEntrypoint = this.resolveRunnerEntrypointPath();
|
|
24
|
+
const runnerCliOverridePath = this.resolveRunnerCliOverridePath();
|
|
25
|
+
const env = input.workspaceMode === "current-working-directory" && input.projectRoot
|
|
26
|
+
? {
|
|
27
|
+
COMPANYHELM_RUNNER_WORKSPACE_MODE: input.workspaceMode,
|
|
28
|
+
COMPANYHELM_RUNNER_PROJECT_ROOT: input.projectRoot,
|
|
29
|
+
}
|
|
30
|
+
: undefined;
|
|
23
31
|
return {
|
|
24
32
|
command: process.execPath,
|
|
25
33
|
args: [
|
|
26
|
-
|
|
34
|
+
runnerEntrypoint,
|
|
35
|
+
...(runnerCliOverridePath ? [runnerCliOverridePath] : []),
|
|
27
36
|
"--config-path",
|
|
28
37
|
this.configPath,
|
|
29
38
|
"start",
|
|
@@ -39,7 +48,8 @@ export class RunnerSupervisor {
|
|
|
39
48
|
input.secret,
|
|
40
49
|
"--log-level",
|
|
41
50
|
logLevel
|
|
42
|
-
]
|
|
51
|
+
],
|
|
52
|
+
env
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
buildStopArgs() {
|
|
@@ -63,6 +73,13 @@ export class RunnerSupervisor {
|
|
|
63
73
|
const packageJsonPath = require.resolve("@companyhelm/runner/package.json");
|
|
64
74
|
return path.resolve(path.dirname(packageJsonPath), "dist/cli.js");
|
|
65
75
|
}
|
|
76
|
+
resolveRunnerCliOverridePath() {
|
|
77
|
+
const overridePath = String(process.env.COMPANYHELM_RUNNER_CLI_PATH || "").trim();
|
|
78
|
+
return overridePath || null;
|
|
79
|
+
}
|
|
80
|
+
resolveRunnerEntrypointPath() {
|
|
81
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "runner-bootstrap.js");
|
|
82
|
+
}
|
|
66
83
|
resolveHostDockerPath() {
|
|
67
84
|
const dockerHost = String(process.env.DOCKER_HOST || "").trim();
|
|
68
85
|
if (dockerHost) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
function applyCurrentWorkingDirectoryMode(projectRoot) {
|
|
6
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
7
|
+
const fsModule = require("node:fs");
|
|
8
|
+
const originalRmSync = fsModule.rmSync.bind(fsModule);
|
|
9
|
+
fsModule.rmSync = ((target, options) => {
|
|
10
|
+
if (path.resolve(String(target)) === resolvedProjectRoot) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
return originalRmSync(target, options);
|
|
14
|
+
});
|
|
15
|
+
const configModule = require("@companyhelm/runner/dist/config.js");
|
|
16
|
+
const originalParse = configModule.config.parse.bind(configModule.config);
|
|
17
|
+
configModule.config.parse = ((input = {}) => originalParse({
|
|
18
|
+
...input,
|
|
19
|
+
workspaces_directory: resolvedProjectRoot
|
|
20
|
+
}));
|
|
21
|
+
const threadLifecycle = require("@companyhelm/runner/dist/service/thread_lifecycle.js");
|
|
22
|
+
threadLifecycle.resolveThreadDirectory = (() => resolvedProjectRoot);
|
|
23
|
+
}
|
|
24
|
+
export function resolveRunnerCliEntrypointArg(argv) {
|
|
25
|
+
const candidate = String(argv[2] || "").trim();
|
|
26
|
+
if (!candidate || candidate.startsWith("-")) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
const workspaceMode = String(process.env.COMPANYHELM_RUNNER_WORKSPACE_MODE || "").trim();
|
|
32
|
+
const projectRoot = String(process.env.COMPANYHELM_RUNNER_PROJECT_ROOT || "").trim();
|
|
33
|
+
export async function runRunnerBootstrap() {
|
|
34
|
+
if (workspaceMode === "current-working-directory" && projectRoot) {
|
|
35
|
+
applyCurrentWorkingDirectoryMode(projectRoot);
|
|
36
|
+
}
|
|
37
|
+
const runnerEntrypoint = resolveRunnerCliEntrypointArg(process.argv);
|
|
38
|
+
if (runnerEntrypoint) {
|
|
39
|
+
process.argv.splice(2, 1);
|
|
40
|
+
}
|
|
41
|
+
await import(runnerEntrypoint ? pathToFileURL(runnerEntrypoint).href : "@companyhelm/runner/dist/cli.js");
|
|
42
|
+
}
|
|
43
|
+
const invokedAsEntrypoint = process.argv[1]
|
|
44
|
+
? pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url
|
|
45
|
+
: false;
|
|
46
|
+
if (invokedAsEntrypoint) {
|
|
47
|
+
await runRunnerBootstrap();
|
|
48
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ManagedImageService } from "./ManagedImages.js";
|
|
2
|
+
export type AgentWorkspaceMode = "dedicated" | "current-working-directory";
|
|
2
3
|
export interface LocalConfig {
|
|
4
|
+
agentWorkspaceMode?: AgentWorkspaceMode;
|
|
3
5
|
images: Partial<Record<ManagedImageService, string>>;
|
|
4
6
|
}
|
|
5
7
|
export declare class LocalConfigStore {
|
|
@@ -11,6 +13,10 @@ export declare class LocalConfigStore {
|
|
|
11
13
|
configPath: string;
|
|
12
14
|
image: string;
|
|
13
15
|
};
|
|
16
|
+
setAgentWorkspaceMode(agentWorkspaceMode: AgentWorkspaceMode): {
|
|
17
|
+
configPath: string;
|
|
18
|
+
agentWorkspaceMode: AgentWorkspaceMode;
|
|
19
|
+
};
|
|
14
20
|
save(config: LocalConfig): void;
|
|
15
21
|
private parse;
|
|
16
22
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
function defaultLocalConfigRoot() {
|
|
5
|
+
return process.env.COMPANYHELM_HOME || path.join(os.homedir(), ".companyhelm");
|
|
6
|
+
}
|
|
3
7
|
export class LocalConfigStore {
|
|
4
8
|
root;
|
|
5
|
-
constructor(root =
|
|
9
|
+
constructor(root = defaultLocalConfigRoot()) {
|
|
6
10
|
this.root = root;
|
|
7
11
|
}
|
|
8
12
|
configPath() {
|
|
@@ -21,18 +25,30 @@ export class LocalConfigStore {
|
|
|
21
25
|
this.save(nextConfig);
|
|
22
26
|
return { configPath: this.configPath(), image };
|
|
23
27
|
}
|
|
28
|
+
setAgentWorkspaceMode(agentWorkspaceMode) {
|
|
29
|
+
const nextConfig = this.load();
|
|
30
|
+
nextConfig.agentWorkspaceMode = agentWorkspaceMode;
|
|
31
|
+
this.save(nextConfig);
|
|
32
|
+
return { configPath: this.configPath(), agentWorkspaceMode };
|
|
33
|
+
}
|
|
24
34
|
save(config) {
|
|
25
|
-
const lines = [
|
|
35
|
+
const lines = [];
|
|
36
|
+
if (config.agentWorkspaceMode) {
|
|
37
|
+
lines.push(`agent_workspace_mode: ${config.agentWorkspaceMode}`);
|
|
38
|
+
}
|
|
39
|
+
lines.push("images:");
|
|
26
40
|
if (config.images.api) {
|
|
27
41
|
lines.push(` api: ${config.images.api}`);
|
|
28
42
|
}
|
|
29
43
|
if (config.images.frontend) {
|
|
30
44
|
lines.push(` frontend: ${config.images.frontend}`);
|
|
31
45
|
}
|
|
46
|
+
fs.mkdirSync(path.dirname(this.configPath()), { recursive: true });
|
|
32
47
|
fs.writeFileSync(this.configPath(), `${lines.join("\n")}\n`, "utf8");
|
|
33
48
|
}
|
|
34
49
|
parse(content) {
|
|
35
50
|
const images = {};
|
|
51
|
+
let agentWorkspaceMode;
|
|
36
52
|
let inImagesSection = false;
|
|
37
53
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
38
54
|
const line = rawLine.trimEnd();
|
|
@@ -43,6 +59,11 @@ export class LocalConfigStore {
|
|
|
43
59
|
inImagesSection = true;
|
|
44
60
|
continue;
|
|
45
61
|
}
|
|
62
|
+
const workspaceModeMatch = line.match(/^agent_workspace_mode:\s*(dedicated|current-working-directory)$/);
|
|
63
|
+
if (workspaceModeMatch) {
|
|
64
|
+
agentWorkspaceMode = workspaceModeMatch[1];
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
46
67
|
if (inImagesSection && /^[^\s]/.test(line)) {
|
|
47
68
|
inImagesSection = false;
|
|
48
69
|
}
|
|
@@ -54,6 +75,9 @@ export class LocalConfigStore {
|
|
|
54
75
|
images[match[1]] = match[2];
|
|
55
76
|
}
|
|
56
77
|
}
|
|
57
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
agentWorkspaceMode,
|
|
80
|
+
images
|
|
81
|
+
};
|
|
58
82
|
}
|
|
59
83
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const MANAGED_SERVICE_KEYS: readonly ["postgres", "api", "frontend", "runner"];
|
|
2
|
+
export type ManagedServiceKey = (typeof MANAGED_SERVICE_KEYS)[number];
|
|
3
|
+
export declare const MANAGED_SERVICE_NAMES: Record<ManagedServiceKey, string>;
|
|
4
|
+
export declare const AVAILABLE_MANAGED_SERVICE_NAMES: string[];
|
|
5
|
+
export declare function resolveManagedServiceKey(serviceName: string): ManagedServiceKey | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const MANAGED_SERVICE_KEYS = ["postgres", "api", "frontend", "runner"];
|
|
2
|
+
export const MANAGED_SERVICE_NAMES = {
|
|
3
|
+
postgres: "postgres",
|
|
4
|
+
api: "companyhelm-api",
|
|
5
|
+
frontend: "companyhelm-web",
|
|
6
|
+
runner: "companyhelm-runner"
|
|
7
|
+
};
|
|
8
|
+
const SERVICE_NAME_TO_KEY = new Map(Object.entries(MANAGED_SERVICE_NAMES).map(([key, value]) => [value, key]));
|
|
9
|
+
export const AVAILABLE_MANAGED_SERVICE_NAMES = MANAGED_SERVICE_KEYS.map((key) => MANAGED_SERVICE_NAMES[key]);
|
|
10
|
+
export function resolveManagedServiceKey(serviceName) {
|
|
11
|
+
return SERVICE_NAME_TO_KEY.get(serviceName.trim()) ?? null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PortAvailabilityPreflightCheck } from "./PortAvailabilityPreflightCheck.js";
|
|
2
|
+
export class ApiPortPreflightCheck {
|
|
3
|
+
delegate;
|
|
4
|
+
constructor(port) {
|
|
5
|
+
this.delegate = new PortAvailabilityPreflightCheck("companyhelm-api", port);
|
|
6
|
+
}
|
|
7
|
+
run() {
|
|
8
|
+
return this.delegate.run();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CommandRunner } from "../core/process/CommandRunner.js";
|
|
2
|
+
import type { PreflightCheck } from "./PreflightCheck.js";
|
|
3
|
+
export declare class DockerInstalledPreflightCheck implements PreflightCheck {
|
|
4
|
+
private readonly commandRunner;
|
|
5
|
+
constructor(commandRunner?: CommandRunner);
|
|
6
|
+
run(): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CommandRunner } from "../core/process/CommandRunner.js";
|
|
2
|
+
export class DockerInstalledPreflightCheck {
|
|
3
|
+
commandRunner;
|
|
4
|
+
constructor(commandRunner = new CommandRunner()) {
|
|
5
|
+
this.commandRunner = commandRunner;
|
|
6
|
+
}
|
|
7
|
+
async run() {
|
|
8
|
+
try {
|
|
9
|
+
await this.commandRunner.capture("docker", ["--version"]);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("Docker is required for `companyhelm up`, but the `docker` command is unavailable. Install Docker and make sure it is on your PATH.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PreflightCheck } from "./PreflightCheck.js";
|
|
2
|
+
export declare class PortAvailabilityPreflightCheck implements PreflightCheck {
|
|
3
|
+
private readonly serviceName;
|
|
4
|
+
private readonly port;
|
|
5
|
+
constructor(serviceName: string, port: number);
|
|
6
|
+
run(): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
export class PortAvailabilityPreflightCheck {
|
|
3
|
+
serviceName;
|
|
4
|
+
port;
|
|
5
|
+
constructor(serviceName, port) {
|
|
6
|
+
this.serviceName = serviceName;
|
|
7
|
+
this.port = port;
|
|
8
|
+
}
|
|
9
|
+
async run() {
|
|
10
|
+
await new Promise((resolve, reject) => {
|
|
11
|
+
const server = net.createServer();
|
|
12
|
+
server.once("error", (error) => {
|
|
13
|
+
if (error.code === "EADDRINUSE") {
|
|
14
|
+
reject(new Error(`${this.serviceName} cannot start because port ${this.port} is already in use.`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
reject(new Error(`${this.serviceName} cannot verify port ${this.port}: ${error.message}`));
|
|
18
|
+
});
|
|
19
|
+
server.once("listening", () => {
|
|
20
|
+
server.close((closeError) => {
|
|
21
|
+
if (closeError) {
|
|
22
|
+
reject(closeError);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
server.listen(this.port, "0.0.0.0");
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PortAvailabilityPreflightCheck } from "./PortAvailabilityPreflightCheck.js";
|
|
2
|
+
export class PostgresPortPreflightCheck {
|
|
3
|
+
delegate;
|
|
4
|
+
constructor(port = 5432) {
|
|
5
|
+
this.delegate = new PortAvailabilityPreflightCheck("Postgres", port);
|
|
6
|
+
}
|
|
7
|
+
run() {
|
|
8
|
+
return this.delegate.run();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PortAvailabilityPreflightCheck } from "./PortAvailabilityPreflightCheck.js";
|
|
2
|
+
export class WebPortPreflightCheck {
|
|
3
|
+
delegate;
|
|
4
|
+
constructor(port) {
|
|
5
|
+
this.delegate = new PortAvailabilityPreflightCheck("companyhelm-web", port);
|
|
6
|
+
}
|
|
7
|
+
run() {
|
|
8
|
+
return this.delegate.run();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResolvedServiceSources } from "../core/local/LocalRepoSourceResolver.js";
|
|
2
|
+
import type { CommandRunner } from "../core/process/CommandRunner.js";
|
|
3
|
+
import type { RuntimePorts, RuntimeState } from "../core/runtime/RuntimeState.js";
|
|
4
|
+
import type { ManagedServiceStatus } from "../core/status/StatusService.js";
|
|
5
|
+
interface StartupStatusSnapshot {
|
|
6
|
+
postgres: ManagedServiceStatus;
|
|
7
|
+
api: ManagedServiceStatus;
|
|
8
|
+
frontend: ManagedServiceStatus;
|
|
9
|
+
}
|
|
10
|
+
interface StartupPreflightOptions {
|
|
11
|
+
commandRunner: CommandRunner;
|
|
12
|
+
currentState: RuntimeState | null;
|
|
13
|
+
desiredSources: ResolvedServiceSources;
|
|
14
|
+
ports: RuntimePorts;
|
|
15
|
+
readStatus: () => Promise<StartupStatusSnapshot>;
|
|
16
|
+
}
|
|
17
|
+
export declare function runStartupPreflightChecks(options: StartupPreflightOptions): Promise<void>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DockerInstalledPreflightCheck } from "./DockerInstalledPreflightCheck.js";
|
|
2
|
+
import { ApiPortPreflightCheck } from "./ApiPortPreflightCheck.js";
|
|
3
|
+
import { PostgresPortPreflightCheck } from "./PostgresPortPreflightCheck.js";
|
|
4
|
+
import { WebPortPreflightCheck } from "./WebPortPreflightCheck.js";
|
|
5
|
+
export async function runStartupPreflightChecks(options) {
|
|
6
|
+
await new DockerInstalledPreflightCheck(options.commandRunner).run();
|
|
7
|
+
const currentStatus = await options.readStatus();
|
|
8
|
+
if (shouldCheckApiPort(options, currentStatus)) {
|
|
9
|
+
await new ApiPortPreflightCheck(options.ports.apiHttp).run();
|
|
10
|
+
}
|
|
11
|
+
if (shouldCheckWebPort(options, currentStatus)) {
|
|
12
|
+
await new WebPortPreflightCheck(options.ports.ui).run();
|
|
13
|
+
}
|
|
14
|
+
if (shouldCheckPostgresPort(options, currentStatus)) {
|
|
15
|
+
await new PostgresPortPreflightCheck().run();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function shouldCheckApiPort(options, currentStatus) {
|
|
19
|
+
return shouldCheckServicePort("api", options.currentState, currentStatus.api, options.desiredSources.api.source);
|
|
20
|
+
}
|
|
21
|
+
function shouldCheckWebPort(options, currentStatus) {
|
|
22
|
+
return shouldCheckServicePort("frontend", options.currentState, currentStatus.frontend, options.desiredSources.frontend.source);
|
|
23
|
+
}
|
|
24
|
+
function shouldCheckPostgresPort(options, currentStatus) {
|
|
25
|
+
if (options.desiredSources.api.source !== "local") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return currentStatus.postgres !== "running";
|
|
29
|
+
}
|
|
30
|
+
function shouldCheckServicePort(service, currentState, currentStatus, desiredSource) {
|
|
31
|
+
if (currentStatus !== "running") {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const currentService = currentState?.services[service];
|
|
35
|
+
if (!currentService) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (currentService.source === "docker") {
|
|
39
|
+
return desiredSource !== "docker";
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@companyhelm/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Bootstrap and manage a local CompanyHelm deployment.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@clack/prompts": "^1.1.0",
|
|
30
|
-
"@companyhelm/runner": "^0.1.
|
|
30
|
+
"@companyhelm/runner": "^0.1.3",
|
|
31
31
|
"chalk": "^5.6.2",
|
|
32
32
|
"commander": "^14.0.1",
|
|
33
33
|
"dockerode": "^4.0.9",
|