@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.
Files changed (47) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +23 -2
  4. package/dist/commands/dependencies.js +25 -6
  5. package/dist/commands/interactive.d.ts +1 -0
  6. package/dist/commands/interactive.js +4 -1
  7. package/dist/commands/logs.js +2 -2
  8. package/dist/commands/register-commands.js +4 -1
  9. package/dist/commands/reset.js +1 -1
  10. package/dist/commands/set-image-version.js +1 -1
  11. package/dist/commands/setup-github-app.d.ts +4 -1
  12. package/dist/commands/setup-github-app.js +30 -8
  13. package/dist/commands/startup-preferences.d.ts +3 -0
  14. package/dist/commands/startup-preferences.js +39 -0
  15. package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +2 -0
  16. package/dist/core/bootstrap/DeploymentBootstrapper.js +23 -3
  17. package/dist/core/config/ApiEnvFileWriter.d.ts +1 -1
  18. package/dist/core/config/ApiEnvFileWriter.js +3 -3
  19. package/dist/core/local/ApiLocalService.d.ts +1 -1
  20. package/dist/core/local/ApiLocalService.js +3 -3
  21. package/dist/core/logs/LogsService.d.ts +2 -1
  22. package/dist/core/logs/LogsService.js +5 -4
  23. package/dist/core/runner/RunnerSupervisor.d.ts +6 -0
  24. package/dist/core/runner/RunnerSupervisor.js +20 -3
  25. package/dist/core/runner/runner-bootstrap.d.ts +2 -0
  26. package/dist/core/runner/runner-bootstrap.js +48 -0
  27. package/dist/core/runtime/CliPackageMetadata.d.ts +3 -0
  28. package/dist/core/runtime/CliPackageMetadata.js +8 -0
  29. package/dist/core/runtime/LocalConfigStore.d.ts +6 -0
  30. package/dist/core/runtime/LocalConfigStore.js +27 -3
  31. package/dist/core/services/ManagedServiceNames.d.ts +5 -0
  32. package/dist/core/services/ManagedServiceNames.js +12 -0
  33. package/dist/preflight/ApiPortPreflightCheck.d.ts +6 -0
  34. package/dist/preflight/ApiPortPreflightCheck.js +10 -0
  35. package/dist/preflight/DockerInstalledPreflightCheck.d.ts +7 -0
  36. package/dist/preflight/DockerInstalledPreflightCheck.js +15 -0
  37. package/dist/preflight/PortAvailabilityPreflightCheck.d.ts +7 -0
  38. package/dist/preflight/PortAvailabilityPreflightCheck.js +31 -0
  39. package/dist/preflight/PostgresPortPreflightCheck.d.ts +6 -0
  40. package/dist/preflight/PostgresPortPreflightCheck.js +10 -0
  41. package/dist/preflight/PreflightCheck.d.ts +3 -0
  42. package/dist/preflight/PreflightCheck.js +1 -0
  43. package/dist/preflight/WebPortPreflightCheck.d.ts +6 -0
  44. package/dist/preflight/WebPortPreflightCheck.js +10 -0
  45. package/dist/preflight/runStartupPreflightChecks.d.ts +18 -0
  46. package/dist/preflight/runStartupPreflightChecks.js +42 -0
  47. 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
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export declare function main(argv?: string[]): Promise<void>;
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
- throw error;
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 (import.meta.url === `file://${process.argv[1]}`) {
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 (!isReadableTty(input) || !clack.isTTY(output)) {
15
+ if (!hasInteractiveTerminal(input, output)) {
13
16
  throw new Error(message);
14
17
  }
15
18
  }
@@ -1,4 +1,4 @@
1
- const AVAILABLE_LOG_SERVICES = ["postgres", "api", "frontend", "runner"];
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${AVAILABLE_LOG_SERVICES.map((name) => `- ${name}`).join("\n")}\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().name("companyhelm");
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);
@@ -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, and generated .companyhelm/api/.env. Continue?",
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 local config.yaml.")
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): Promise<GithubAppConfig>;
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
- requireInteractiveTerminal(input, output, `GitHub App config is not set up. Run \`companyhelm setup-github-app\` or rerun \`companyhelm up\` in a TTY.`);
185
- clack.intro("CompanyHelm GitHub App setup", { output });
186
- clack.note([
187
- "No machine GitHub App config was found.",
188
- "CompanyHelm will collect it now and then continue startup.",
189
- ].join("\n"), "Setup required", { output });
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
- ' app_client_id: "${GITHUB_APP_CLIENT_ID}"',
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:",
@@ -2,5 +2,5 @@ import type { GithubAppConfig } from "./GithubAppConfig.js";
2
2
  export declare class ApiEnvFileWriter {
3
3
  private readonly projectPaths;
4
4
  constructor(projectRoot?: string);
5
- write(config: GithubAppConfig): string;
5
+ write(config: GithubAppConfig | null): string;
6
6
  }
@@ -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.appUrl)}`,
19
- `GITHUB_APP_CLIENT_ID=${escapeEnvValue(config.appClientId)}`,
20
- `GITHUB_APP_PRIVATE_KEY_PEM=${escapeEnvValue(config.appPrivateKeyPem)}`,
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");
@@ -8,7 +8,7 @@ export interface ApiLocalServiceStartInput {
8
8
  configPath: string;
9
9
  graphqlUrl: string;
10
10
  logPath: string;
11
- githubAppConfig: GithubAppConfig;
11
+ githubAppConfig: GithubAppConfig | null;
12
12
  state: RuntimeState;
13
13
  logLevel: LogLevel;
14
14
  }
@@ -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.appClientId,
28
- GITHUB_APP_URL: input.githubAppConfig.appUrl,
29
- GITHUB_APP_PRIVATE_KEY_PEM: input.githubAppConfig.appPrivateKeyPem,
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: "postgres" | "api" | "frontend" | "runner") => Promise<void>);
4
+ constructor(streamServiceLogs: (service: ManagedServiceKey) => Promise<void>);
4
5
  stream(service: string): Promise<void>;
5
6
  }
@@ -1,13 +1,14 @@
1
- const MANAGED_SERVICES = new Set(["postgres", "api", "frontend", "runner"]);
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
- if (!MANAGED_SERVICES.has(service)) {
9
- throw new Error(`Unknown service '${service}'. Expected one of: ${Array.from(MANAGED_SERVICES).join(", ")}`);
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(service);
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
- runnerCliPath,
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,2 @@
1
+ export declare function resolveRunnerCliEntrypointArg(argv: string[]): string | null;
2
+ export declare function runRunnerBootstrap(): Promise<void>;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export declare class CliPackageMetadata {
2
+ version(): string;
3
+ }
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs";
2
+ export class CliPackageMetadata {
3
+ version() {
4
+ const manifestPath = new URL("../../../package.json", import.meta.url);
5
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
6
+ return manifest.version;
7
+ }
8
+ }
@@ -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 = process.cwd()) {
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 = ["images:"];
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 { images };
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,6 @@
1
+ import type { PreflightCheck } from "./PreflightCheck.js";
2
+ export declare class ApiPortPreflightCheck implements PreflightCheck {
3
+ private readonly delegate;
4
+ constructor(port: number);
5
+ run(): Promise<void>;
6
+ }
@@ -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,6 @@
1
+ import type { PreflightCheck } from "./PreflightCheck.js";
2
+ export declare class PostgresPortPreflightCheck implements PreflightCheck {
3
+ private readonly delegate;
4
+ constructor(port?: number);
5
+ run(): Promise<void>;
6
+ }
@@ -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,3 @@
1
+ export interface PreflightCheck {
2
+ run(): Promise<void>;
3
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { PreflightCheck } from "./PreflightCheck.js";
2
+ export declare class WebPortPreflightCheck implements PreflightCheck {
3
+ private readonly delegate;
4
+ constructor(port: number);
5
+ run(): Promise<void>;
6
+ }
@@ -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.1.5",
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.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",