@companyhelm/cli 0.2.0 → 0.4.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 (62) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +12 -1
  3. package/dist/commands/dependencies.js +29 -14
  4. package/dist/commands/interactive.d.ts +1 -0
  5. package/dist/commands/interactive.js +4 -1
  6. package/dist/commands/logs.js +2 -2
  7. package/dist/commands/register-commands.js +4 -1
  8. package/dist/commands/reset.js +1 -1
  9. package/dist/commands/set-image-version.js +3 -3
  10. package/dist/commands/setup-github-app.d.ts +4 -1
  11. package/dist/commands/setup-github-app.js +30 -8
  12. package/dist/commands/startup-preferences.d.ts +3 -0
  13. package/dist/commands/startup-preferences.js +39 -0
  14. package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +2 -1
  15. package/dist/core/bootstrap/DeploymentBootstrapper.js +23 -8
  16. package/dist/core/config/ApiEnvFileWriter.d.ts +5 -3
  17. package/dist/core/config/ApiEnvFileWriter.js +20 -13
  18. package/dist/core/config/GithubAppConfigStore.js +2 -10
  19. package/dist/core/docker/ComposeTemplateRenderer.js +0 -1
  20. package/dist/core/docker/DockerStackManager.js +1 -2
  21. package/dist/core/local/ApiLocalService.d.ts +1 -1
  22. package/dist/core/local/ApiLocalService.js +3 -3
  23. package/dist/core/logs/LogsService.d.ts +2 -1
  24. package/dist/core/logs/LogsService.js +5 -4
  25. package/dist/core/runner/RunnerSupervisor.d.ts +6 -0
  26. package/dist/core/runner/RunnerSupervisor.js +20 -3
  27. package/dist/core/runner/runner-bootstrap.d.ts +2 -0
  28. package/dist/core/runner/runner-bootstrap.js +48 -0
  29. package/dist/core/runtime/CliPackageMetadata.d.ts +3 -0
  30. package/dist/core/runtime/CliPackageMetadata.js +8 -0
  31. package/dist/core/runtime/CliRoot.d.ts +2 -0
  32. package/dist/core/runtime/CliRoot.js +23 -0
  33. package/dist/core/runtime/ImageCatalog.js +2 -2
  34. package/dist/core/runtime/LocalConfigStore.d.ts +4 -4
  35. package/dist/core/runtime/LocalConfigStore.js +18 -27
  36. package/dist/core/runtime/PublicImageTagRegistry.d.ts +1 -0
  37. package/dist/core/runtime/PublicImageTagRegistry.js +34 -14
  38. package/dist/core/runtime/RepoConfigStore.d.ts +16 -0
  39. package/dist/core/runtime/RepoConfigStore.js +63 -0
  40. package/dist/core/runtime/RuntimePaths.d.ts +2 -0
  41. package/dist/core/runtime/RuntimePaths.js +6 -0
  42. package/dist/core/services/ManagedServiceNames.d.ts +5 -0
  43. package/dist/core/services/ManagedServiceNames.js +12 -0
  44. package/dist/preflight/ApiPortPreflightCheck.d.ts +6 -0
  45. package/dist/preflight/ApiPortPreflightCheck.js +10 -0
  46. package/dist/preflight/DockerInstalledPreflightCheck.d.ts +7 -0
  47. package/dist/preflight/DockerInstalledPreflightCheck.js +15 -0
  48. package/dist/preflight/PortAvailabilityPreflightCheck.d.ts +7 -0
  49. package/dist/preflight/PortAvailabilityPreflightCheck.js +31 -0
  50. package/dist/preflight/PostgresPortPreflightCheck.d.ts +6 -0
  51. package/dist/preflight/PostgresPortPreflightCheck.js +10 -0
  52. package/dist/preflight/PreflightCheck.d.ts +3 -0
  53. package/dist/preflight/PreflightCheck.js +1 -0
  54. package/dist/preflight/WebPortPreflightCheck.d.ts +6 -0
  55. package/dist/preflight/WebPortPreflightCheck.js +10 -0
  56. package/dist/preflight/runStartupPreflightChecks.d.ts +18 -0
  57. package/dist/preflight/runStartupPreflightChecks.js +42 -0
  58. package/dist/templates/api.env.tpl +3 -0
  59. package/package.json +2 -2
  60. package/src/templates/api.env.tpl +3 -0
  61. package/dist/core/runtime/ProjectPaths.d.ts +0 -7
  62. package/dist/core/runtime/ProjectPaths.js +0 -16
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.js CHANGED
@@ -3,7 +3,16 @@ import { realpathSync } from "node:fs";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { buildProgram } from "./commands/register-commands.js";
5
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
+ }
6
11
  export async function main(argv = process.argv) {
12
+ if (shouldPrintVersion(argv)) {
13
+ process.stdout.write(`${new CliPackageMetadata().version()}\n`);
14
+ return;
15
+ }
7
16
  const program = buildProgram();
8
17
  try {
9
18
  await program.parseAsync(argv);
@@ -13,7 +22,9 @@ export async function main(argv = process.argv) {
13
22
  process.exitCode = 1;
14
23
  return;
15
24
  }
16
- throw error;
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ process.stderr.write(`${message}\n`);
27
+ process.exitCode = 1;
17
28
  }
18
29
  }
19
30
  function isCliEntrypoint(argv = process.argv) {
@@ -1,6 +1,4 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
2
  import { DeploymentBootstrapper } from "../core/bootstrap/DeploymentBootstrapper.js";
5
3
  import { ApiEnvFileWriter } from "../core/config/ApiEnvFileWriter.js";
6
4
  import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
@@ -11,17 +9,21 @@ import { LocalServiceProcessManager } from "../core/local/LocalServiceProcessMan
11
9
  import { WebLocalService } from "../core/local/WebLocalService.js";
12
10
  import { LogsService } from "../core/logs/LogsService.js";
13
11
  import { CommandRunner } from "../core/process/CommandRunner.js";
14
- import { ProjectPaths } from "../core/runtime/ProjectPaths.js";
15
12
  import { RunnerSupervisor } from "../core/runner/RunnerSupervisor.js";
13
+ import { defaultCliRoot } from "../core/runtime/CliRoot.js";
16
14
  import { createPasswordHash } from "../core/runtime/Secrets.js";
17
15
  import { RuntimePaths } from "../core/runtime/RuntimePaths.js";
18
16
  import { RuntimeStateStore } from "../core/runtime/RuntimeStateStore.js";
19
17
  import { VersionCatalog } from "../core/runtime/VersionCatalog.js";
20
18
  import { StatusService } from "../core/status/StatusService.js";
21
19
  import { TerminalRenderer } from "../core/ui/TerminalRenderer.js";
20
+ import { LocalConfigStore } from "../core/runtime/LocalConfigStore.js";
21
+ import { PortAllocator } from "../core/runtime/PortAllocator.js";
22
+ import { runStartupPreflightChecks } from "../preflight/runStartupPreflightChecks.js";
22
23
  import { ensureGithubAppConfig } from "./setup-github-app.js";
24
+ import { ensureAgentWorkspaceMode } from "./startup-preferences.js";
23
25
  function runtimeRoot() {
24
- return process.env.COMPANYHELM_HOME || path.join(os.homedir(), ".companyhelm");
26
+ return defaultCliRoot();
25
27
  }
26
28
  export function createDefaultDependencies() {
27
29
  const root = runtimeRoot();
@@ -33,9 +35,9 @@ export function createDefaultDependencies() {
33
35
  const runnerSupervisor = new RunnerSupervisor(runtimePaths.runnerConfigPath());
34
36
  const bootstrapper = new DeploymentBootstrapper();
35
37
  const githubAppConfigStore = new GithubAppConfigStore();
36
- const apiEnvFileWriter = new ApiEnvFileWriter(process.cwd());
37
- const projectPaths = new ProjectPaths(process.cwd());
38
+ const apiEnvFileWriter = new ApiEnvFileWriter(root);
38
39
  const localRepoSourceResolver = new LocalRepoSourceResolver(process.cwd());
40
+ const localConfigStore = new LocalConfigStore();
39
41
  const localServiceProcessManager = new LocalServiceProcessManager();
40
42
  const apiLocalService = new ApiLocalService(localServiceProcessManager, commandRunner);
41
43
  const webLocalService = new WebLocalService(localServiceProcessManager, commandRunner);
@@ -86,11 +88,21 @@ export function createDefaultDependencies() {
86
88
  async up(options = {}) {
87
89
  const logLevel = options.logLevel ?? "info";
88
90
  const useHostDockerRuntime = options.useHostDockerRuntime ?? false;
89
- const githubAppConfig = await ensureGithubAppConfig(githubAppConfigStore, process.stdin, process.stdout);
90
- const state = stateStore.initialize();
91
91
  process.stdout.write(`${renderer.renderBanner()}\n`);
92
- const runnerAlreadyRunning = await isRunnerRunning(commandRunner, runnerSupervisor);
93
92
  const desiredSources = localRepoSourceResolver.resolve(options);
93
+ const currentState = stateStore.load();
94
+ const allocatedPorts = currentState?.ports ?? new PortAllocator().allocate();
95
+ await runStartupPreflightChecks({
96
+ commandRunner,
97
+ currentState,
98
+ desiredSources,
99
+ ports: allocatedPorts,
100
+ readStatus: () => statusService.read()
101
+ });
102
+ const workspaceMode = await ensureAgentWorkspaceMode(localConfigStore, process.stdin, process.stdout);
103
+ const githubAppConfig = await ensureGithubAppConfig(githubAppConfigStore, process.stdin, process.stdout, { workspaceMode });
104
+ const state = currentState ?? stateStore.initialize();
105
+ const runnerAlreadyRunning = await isRunnerRunning(commandRunner, runnerSupervisor);
94
106
  const versions = versionCatalog.resolve();
95
107
  const passwordRecord = createPasswordHash(state.auth.password);
96
108
  const startedLocalServices = [];
@@ -100,7 +112,8 @@ export function createDefaultDependencies() {
100
112
  apiEnvFileWriter.write(githubAppConfig);
101
113
  bootstrapper.writeSeedSql(root, state, passwordRecord.passwordHash, passwordRecord.passwordSalt);
102
114
  bootstrapper.writeApiConfig(root, state, logLevel, {
103
- databaseHost: desiredSources.api.source === "local" ? "127.0.0.1" : "postgres"
115
+ databaseHost: desiredSources.api.source === "local" ? "127.0.0.1" : "postgres",
116
+ githubAppConfig
104
117
  });
105
118
  bootstrapper.writeFrontendConfig(root, state);
106
119
  await stopLocalServicesFromState(stateStore.load(), localServiceProcessManager);
@@ -141,14 +154,16 @@ export function createDefaultDependencies() {
141
154
  await commandRunner.run(configureSdkCommand.command, configureSdkCommand.args);
142
155
  const startCommand = runnerSupervisor.buildStartArgs({
143
156
  serverUrl: `127.0.0.1:${state.ports.runnerGrpc}`,
144
- agentApiUrl: `127.0.0.1:${state.ports.agentCliGrpc}`,
157
+ agentApiUrl: `http://127.0.0.1:${state.ports.apiHttp}/agent/v1`,
145
158
  logPath: runtimePaths.runnerLogPath(),
146
159
  secret: state.runner.secret,
147
160
  logLevel,
148
- useHostDockerRuntime
161
+ useHostDockerRuntime,
162
+ workspaceMode,
163
+ projectRoot: process.cwd()
149
164
  });
150
165
  process.stdout.write(`${renderer.progress("Starting the runner...")}\n`);
151
- await commandRunner.run(startCommand.command, startCommand.args);
166
+ await commandRunner.run(startCommand.command, startCommand.args, undefined, startCommand.env);
152
167
  runnerStarted = true;
153
168
  }
154
169
  if (desiredSources.frontend.source === "local") {
@@ -246,7 +261,7 @@ export function createDefaultDependencies() {
246
261
  await stopLocalServicesFromState(state, localServiceProcessManager);
247
262
  }
248
263
  await dockerStackManager.down({ removeVolumes: true });
249
- fs.rmSync(projectPaths.apiEnvPath(), { force: true });
264
+ fs.rmSync(localConfigStore.configPath(), { force: true });
250
265
  fs.rmSync(root, { recursive: true, force: true });
251
266
  if (options.removeGithubAppConfig) {
252
267
  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, generated runtime files under ~/.companyhelm/cli/runtime, and the CLI workspace config under ~/.companyhelm/cli/config.yaml. Continue?",
8
8
  active: "Yes",
9
9
  inactive: "No",
10
10
  initialValue: false,
@@ -1,7 +1,7 @@
1
1
  import * as clack from "@clack/prompts";
2
- import { LocalConfigStore } from "../core/runtime/LocalConfigStore.js";
3
2
  import { MANAGED_IMAGE_SERVICES, requireManagedImageService } from "../core/runtime/ManagedImages.js";
4
3
  import { PublicImageTagRegistry } from "../core/runtime/PublicImageTagRegistry.js";
4
+ import { RepoConfigStore } from "../core/runtime/RepoConfigStore.js";
5
5
  import { requireInteractiveTerminal, unwrapPromptResult } from "./interactive.js";
6
6
  function parsePositiveInteger(value) {
7
7
  const parsed = Number.parseInt(value, 10);
@@ -59,7 +59,7 @@ export async function runSetImageVersion(options, dependencies = {}) {
59
59
  const input = dependencies.input ?? process.stdin;
60
60
  const output = dependencies.output ?? process.stdout;
61
61
  const registry = dependencies.registry ?? new PublicImageTagRegistry();
62
- const configStore = dependencies.configStore ?? new LocalConfigStore();
62
+ const configStore = dependencies.configStore ?? new RepoConfigStore();
63
63
  clack.intro("CompanyHelm image selection", { output });
64
64
  const selectedService = options.service
65
65
  ? requireManagedImageService(options.service)
@@ -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 project 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;
@@ -7,7 +8,7 @@ export declare class DeploymentBootstrapper {
7
8
  databaseHost?: string;
8
9
  appPort?: number;
9
10
  runnerGrpcPort?: number;
10
- agentGrpcPort?: number;
11
+ githubAppConfig?: GithubAppConfig | null;
11
12
  }): string;
12
13
  writeFrontendConfig(root: string, state: RuntimeState): string;
13
14
  private indentBlock;
@@ -23,8 +23,18 @@ export class DeploymentBootstrapper {
23
23
  const outputPath = runtimePaths.apiConfigPath();
24
24
  const appPort = options.appPort ?? state.ports.apiHttp;
25
25
  const runnerGrpcPort = options.runnerGrpcPort ?? state.ports.runnerGrpc;
26
- const agentGrpcPort = options.agentGrpcPort ?? state.ports.agentCliGrpc;
27
26
  const databaseHost = options.databaseHost ?? "postgres";
27
+ const githubConfigLines = options.githubAppConfig
28
+ ? [
29
+ ' app_client_id: "${GITHUB_APP_CLIENT_ID}"',
30
+ ' app_private_key_pem: "${GITHUB_APP_PRIVATE_KEY_PEM}"',
31
+ ' app_link: "${GITHUB_APP_URL}"'
32
+ ]
33
+ : [
34
+ ' app_client_id: "companyhelm-local-github-disabled"',
35
+ ' app_private_key_pem: "companyhelm-local-github-disabled"',
36
+ ' app_link: "https://github.com/apps/companyhelm-local-disabled"'
37
+ ];
28
38
  const yaml = [
29
39
  "app:",
30
40
  ' host: "0.0.0.0"',
@@ -37,10 +47,17 @@ export class DeploymentBootstrapper {
37
47
  " heartbeat:",
38
48
  " intervalMs: 20000",
39
49
  " jitterMs: 10000",
40
- "agent:",
41
- " grpc:",
42
- ' host: "0.0.0.0"',
43
- ` port: ${agentGrpcPort}`,
50
+ " workers:",
51
+ " agentHeartbeats:",
52
+ " intervalSeconds: 60",
53
+ " jitterSeconds: 60",
54
+ " batchSize: 10",
55
+ " leaseSeconds: 120",
56
+ " taskWorker:",
57
+ " intervalSeconds: 60",
58
+ " jitterSeconds: 60",
59
+ " batchSize: 10",
60
+ " leaseSeconds: 120",
44
61
  "database:",
45
62
  ' name: "companyhelm"',
46
63
  ` host: "${databaseHost}"`,
@@ -53,9 +70,7 @@ export class DeploymentBootstrapper {
53
70
  ' username: "postgres"',
54
71
  ' password: "postgres"',
55
72
  "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}"',
73
+ ...githubConfigLines,
59
74
  'authProvider: "companyhelm"',
60
75
  "auth:",
61
76
  " companyhelm:",
@@ -1,6 +1,8 @@
1
1
  import type { GithubAppConfig } from "./GithubAppConfig.js";
2
2
  export declare class ApiEnvFileWriter {
3
- private readonly projectPaths;
4
- constructor(projectRoot?: string);
5
- write(config: GithubAppConfig): string;
3
+ private readonly runtimePaths;
4
+ private readonly templatePath;
5
+ constructor(root: string);
6
+ write(config: GithubAppConfig | null): string;
7
+ private render;
6
8
  }
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs";
2
- import { ProjectPaths } from "../runtime/ProjectPaths.js";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { RuntimePaths } from "../runtime/RuntimePaths.js";
3
5
  function escapeEnvValue(value) {
4
6
  return String(value || "")
5
7
  .replace(/\\/g, "\\\\")
@@ -8,19 +10,24 @@ function escapeEnvValue(value) {
8
10
  .replace(/\n/g, "\\n");
9
11
  }
10
12
  export class ApiEnvFileWriter {
11
- projectPaths;
12
- constructor(projectRoot = process.cwd()) {
13
- this.projectPaths = new ProjectPaths(projectRoot);
13
+ runtimePaths;
14
+ templatePath;
15
+ constructor(root) {
16
+ this.runtimePaths = new RuntimePaths(root);
17
+ this.templatePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../templates/api.env.tpl");
14
18
  }
15
19
  write(config) {
16
- fs.mkdirSync(this.projectPaths.apiDirectoryPath(), { recursive: true });
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)}`,
21
- "",
22
- ].join("\n");
23
- fs.writeFileSync(this.projectPaths.apiEnvPath(), contents, "utf8");
24
- return this.projectPaths.apiEnvPath();
20
+ fs.mkdirSync(this.runtimePaths.apiDirectoryPath(), { recursive: true });
21
+ const contents = this.render(config);
22
+ fs.writeFileSync(this.runtimePaths.apiEnvPath(), contents, "utf8");
23
+ return this.runtimePaths.apiEnvPath();
24
+ }
25
+ render(config) {
26
+ const template = fs.readFileSync(this.templatePath, "utf8");
27
+ const rendered = template
28
+ .replace("{{GITHUB_APP_URL}}", escapeEnvValue(config?.appUrl ?? ""))
29
+ .replace("{{GITHUB_APP_CLIENT_ID}}", escapeEnvValue(config?.appClientId ?? ""))
30
+ .replace("{{GITHUB_APP_PRIVATE_KEY_PEM}}", escapeEnvValue(config?.appPrivateKeyPem ?? ""));
31
+ return rendered.endsWith("\n") ? rendered : `${rendered}\n`;
25
32
  }
26
33
  }
@@ -1,18 +1,10 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import { parse, stringify } from "yaml";
5
4
  import { normalizeGithubAppConfig } from "./GithubAppConfig.js";
5
+ import { defaultCliConfigRoot } from "../runtime/CliRoot.js";
6
6
  function defaultConfigRoot() {
7
- const explicitRoot = String(process.env.COMPANYHELM_CONFIG_HOME || "").trim();
8
- if (explicitRoot) {
9
- return path.resolve(explicitRoot);
10
- }
11
- const xdgRoot = String(process.env.XDG_CONFIG_HOME || "").trim();
12
- if (xdgRoot) {
13
- return path.resolve(xdgRoot, "companyhelm");
14
- }
15
- return path.join(os.homedir(), ".config", "companyhelm");
7
+ return defaultCliConfigRoot();
16
8
  }
17
9
  export class GithubAppConfigStore {
18
10
  configRoot;
@@ -29,7 +29,6 @@ export class ComposeTemplateRenderer {
29
29
  " ports:",
30
30
  ` - "${ports.apiHttpPort}:4000"`,
31
31
  ` - "${ports.runnerGrpcPort}:${ports.runnerGrpcPort}"`,
32
- ` - "${ports.agentCliGrpcPort}:${ports.agentCliGrpcPort}"`,
33
32
  " volumes:",
34
33
  ` - "${paths.apiConfigPath}:/run/companyhelm/config.yaml:ro"`,
35
34
  " networks:",
@@ -1,6 +1,5 @@
1
1
  import fs from "node:fs";
2
2
  import { CommandRunner } from "../process/CommandRunner.js";
3
- import { ProjectPaths } from "../runtime/ProjectPaths.js";
4
3
  import { RuntimePaths } from "../runtime/RuntimePaths.js";
5
4
  import { ComposeTemplateRenderer } from "./ComposeTemplateRenderer.js";
6
5
  export class DockerStackManager {
@@ -23,7 +22,7 @@ export class DockerStackManager {
23
22
  agentCliGrpcPort: state.ports.agentCliGrpc
24
23
  }, {
25
24
  apiConfigPath: this.runtimePaths.apiConfigPath(),
26
- apiEnvPath: new ProjectPaths(process.cwd()).apiEnvPath(),
25
+ apiEnvPath: this.runtimePaths.apiEnvPath(),
27
26
  frontendConfigPath: this.runtimePaths.frontendConfigPath(),
28
27
  seedFilePath: this.runtimePaths.seedFilePath()
29
28
  }, {
@@ -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
  }