@companyhelm/cli 0.1.2 → 0.1.5

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -33
  3. package/dist/cli.js +11 -1
  4. package/dist/commands/dependencies.d.ts +22 -3
  5. package/dist/commands/dependencies.js +218 -23
  6. package/dist/commands/interactive.d.ts +6 -0
  7. package/dist/commands/interactive.js +22 -0
  8. package/dist/commands/logs.js +6 -1
  9. package/dist/commands/register-commands.js +4 -0
  10. package/dist/commands/reset.d.ts +4 -0
  11. package/dist/commands/reset.js +43 -4
  12. package/dist/commands/set-image-version.d.ts +31 -0
  13. package/dist/commands/set-image-version.js +87 -0
  14. package/dist/commands/setup-github-app.d.ts +10 -0
  15. package/dist/commands/setup-github-app.js +211 -0
  16. package/dist/commands/status.js +3 -1
  17. package/dist/commands/up.js +36 -2
  18. package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +7 -2
  19. package/dist/core/bootstrap/DeploymentBootstrapper.js +13 -11
  20. package/dist/core/bootstrap/SeedSqlRenderer.js +23 -5
  21. package/dist/core/config/ApiEnvFileWriter.d.ts +6 -0
  22. package/dist/core/config/ApiEnvFileWriter.js +26 -0
  23. package/dist/core/config/GithubAppConfig.d.ts +6 -0
  24. package/dist/core/config/GithubAppConfig.js +26 -0
  25. package/dist/core/config/GithubAppConfigStore.d.ts +11 -0
  26. package/dist/core/config/GithubAppConfigStore.js +65 -0
  27. package/dist/core/docker/ComposeTemplateRenderer.d.ts +9 -1
  28. package/dist/core/docker/ComposeTemplateRenderer.js +48 -5
  29. package/dist/core/docker/DockerStackManager.d.ts +18 -3
  30. package/dist/core/docker/DockerStackManager.js +70 -8
  31. package/dist/core/local/ApiLocalService.d.ts +22 -0
  32. package/dist/core/local/ApiLocalService.js +65 -0
  33. package/dist/core/local/LocalRepoSourceResolver.d.ts +24 -0
  34. package/dist/core/local/LocalRepoSourceResolver.js +33 -0
  35. package/dist/core/local/LocalServiceProcessManager.d.ts +18 -0
  36. package/dist/core/local/LocalServiceProcessManager.js +83 -0
  37. package/dist/core/local/WebLocalService.d.ts +23 -0
  38. package/dist/core/local/WebLocalService.js +101 -0
  39. package/dist/core/process/CommandRunner.d.ts +2 -2
  40. package/dist/core/process/CommandRunner.js +10 -2
  41. package/dist/core/runner/RunnerSupervisor.d.ts +6 -0
  42. package/dist/core/runner/RunnerSupervisor.js +31 -3
  43. package/dist/core/runtime/ImageCatalog.js +5 -2
  44. package/dist/core/runtime/LocalConfigStore.d.ts +16 -0
  45. package/dist/core/runtime/LocalConfigStore.js +59 -0
  46. package/dist/core/runtime/ManagedImages.d.ts +10 -0
  47. package/dist/core/runtime/ManagedImages.js +27 -0
  48. package/dist/core/runtime/ProjectPaths.d.ts +7 -0
  49. package/dist/core/runtime/ProjectPaths.js +16 -0
  50. package/dist/core/runtime/PublicImageTagRegistry.d.ts +16 -0
  51. package/dist/core/runtime/PublicImageTagRegistry.js +148 -0
  52. package/dist/core/runtime/RuntimePaths.d.ts +2 -0
  53. package/dist/core/runtime/RuntimePaths.js +7 -1
  54. package/dist/core/runtime/RuntimeState.d.ts +15 -1
  55. package/dist/core/runtime/RuntimeStateStore.d.ts +2 -0
  56. package/dist/core/runtime/RuntimeStateStore.js +33 -4
  57. package/dist/core/runtime/VersionCatalog.d.ts +10 -0
  58. package/dist/core/runtime/VersionCatalog.js +21 -0
  59. package/dist/core/status/StatusService.d.ts +8 -1
  60. package/dist/core/status/StatusService.js +16 -4
  61. package/dist/core/ui/TerminalRenderer.d.ts +10 -0
  62. package/dist/core/ui/TerminalRenderer.js +48 -0
  63. package/dist/templates/docker-compose.yaml.tpl +3 -27
  64. package/dist/templates/seed.sql.tpl +32 -13
  65. package/package.json +7 -3
  66. package/src/templates/docker-compose.yaml.tpl +3 -27
  67. package/src/templates/seed.sql.tpl +32 -13
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CompanyHelm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,54 +1,61 @@
1
- # CompanyHelm CLI
1
+ # CompanyHelm - Distributed AI Agent Orchestration
2
2
 
3
- Bootstrap and manage a local CompanyHelm deployment.
3
+ CompanyHelm is an open-source control plane for running AI-agent companies in your own infrastructure.
4
+ It gives teams a way to organize agents by role, keep humans in the loop for approvals and clarification, and run agent workloads in isolated environments instead of opaque hosted black boxes. Each agent can run its own app infrastructure for testing and create PRs autonomously. Spin up container-isolated agent threads with a click.
4
5
 
5
- ## Commands
6
+ [Website](https://www.companyhelm.com/)
6
7
 
7
- Start or reconcile the local deployment:
8
+ ## Quick start
9
+
10
+ Dependecies:
11
+ - Docker
12
+ - Node.js `>=24`
13
+ - Codex subscription or api key
14
+ - Github account
8
15
 
9
16
  ```bash
10
17
  npx @companyhelm/cli up
11
18
  ```
12
19
 
13
- Stop the local deployment:
20
+ After startup, the CLI prints:
14
21
 
15
- ```bash
16
- npx @companyhelm/cli down
17
- ```
22
+ - the local dashboard UI URL
23
+ - the generated username and password
18
24
 
19
- Show the current service status:
25
+ ## What CompanyHelm is
20
26
 
21
- ```bash
22
- npx @companyhelm/cli status
23
- ```
24
-
25
- Show logs for one managed service:
27
+ From the product perspective, CompanyHelm is built around a few core ideas:
26
28
 
27
- ```bash
28
- npx @companyhelm/cli logs <service>
29
- ```
29
+ - Your infrastructure, not a vendor-controlled runtime
30
+ - Model-agnostic agent execution through open runners and protocols
31
+ - Easy agent customization: add skills, MCP servers, and custom instructions to agents from the UI
32
+ - Human-in-the-loop workflows: tasks can be steered at any moment through the built-in chat; approvals and questions are still a work in progress
33
+ - Isolated execution so agents can work in parallel with minimal interference. Each runner can spin up the full app infrastructure within Docker and test in isolation
34
+ - YOLO mode by default: agents run commands without pausing for trivial confirmations
35
+ - Remote repository as the source of truth: agents clone the repo and submit PRs automatically
36
+ - Parallel task execution: multiple agents can execute tasks independently in isolated environments
30
37
 
31
- Supported services:
38
+ ## What the CLI boots locally
32
39
 
33
- - `postgres`
34
- - `api`
35
- - `frontend`
36
- - `runner`
40
+ `npx @companyhelm/cli up` brings up a local CompanyHelm stack with:
37
41
 
38
- Destroy the local deployment and generated state:
42
+ - CompanyHelm API
43
+ - Postgres
44
+ - CompanyHelm frontend
45
+ - CompanyHelm agent runner
39
46
 
40
- ```bash
41
- npx @companyhelm/cli reset --force
42
- ```
43
47
 
44
- ## Authentication
48
+ ## Command reference
45
49
 
46
- On first `npx @companyhelm/cli up`, the CLI generates a local `admin` account and a random password. The password is printed at startup and persisted in the local runtime state until `npx @companyhelm/cli reset --force` is run.
50
+ For the full CLI help:
47
51
 
48
- ## Image Overrides
52
+ ```bash
53
+ npx @companyhelm/cli --help
54
+ ```
49
55
 
50
- The packaged stack can be overridden with environment variables:
56
+ Common commands:
51
57
 
52
- - `COMPANYHELM_API_IMAGE`
53
- - `COMPANYHELM_WEB_IMAGE`
54
- - `COMPANYHELM_POSTGRES_IMAGE`
58
+ ```bash
59
+ npx @companyhelm/cli logs {service]
60
+ npx @companyhelm/cli reset
61
+ ```
package/dist/cli.js CHANGED
@@ -1,7 +1,17 @@
1
1
  import { buildProgram } from "./commands/register-commands.js";
2
+ import { InteractiveCommandCancelledError } from "./commands/interactive.js";
2
3
  export async function main(argv = process.argv) {
3
4
  const program = buildProgram();
4
- await program.parseAsync(argv);
5
+ try {
6
+ await program.parseAsync(argv);
7
+ }
8
+ catch (error) {
9
+ if (error instanceof InteractiveCommandCancelledError) {
10
+ process.exitCode = 1;
11
+ return;
12
+ }
13
+ throw error;
14
+ }
5
15
  }
6
16
  if (import.meta.url === `file://${process.argv[1]}`) {
7
17
  void main();
@@ -1,9 +1,28 @@
1
+ import { type RuntimeVersions } from "../core/runtime/VersionCatalog.js";
1
2
  import { type StatusSnapshot } from "../core/status/StatusService.js";
3
+ export type LogLevel = "debug" | "info" | "warn" | "error";
4
+ export type LocalRepoOptionValue = string | true | undefined;
5
+ export interface UpOptions {
6
+ logLevel?: LogLevel;
7
+ useHostDockerRuntime?: boolean;
8
+ apiRepoPath?: LocalRepoOptionValue;
9
+ webRepoPath?: LocalRepoOptionValue;
10
+ }
11
+ export interface ResetOptions {
12
+ removeGithubAppConfig?: boolean;
13
+ }
14
+ export interface StatusReport {
15
+ services: StatusSnapshot;
16
+ apiUrl?: string;
17
+ uiUrl?: string;
18
+ username?: string;
19
+ versions?: RuntimeVersions;
20
+ }
2
21
  export interface CommandDependencies {
3
- up(): Promise<void>;
22
+ up(options?: UpOptions): Promise<void>;
4
23
  down(): Promise<void>;
5
- status(): Promise<StatusSnapshot>;
24
+ status(): Promise<StatusReport>;
6
25
  logs(service: string): Promise<void>;
7
- reset(): Promise<void>;
26
+ reset(options?: ResetOptions): Promise<void>;
8
27
  }
9
28
  export declare function createDefaultDependencies(): CommandDependencies;
@@ -2,15 +2,24 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { DeploymentBootstrapper } from "../core/bootstrap/DeploymentBootstrapper.js";
5
+ import { ApiEnvFileWriter } from "../core/config/ApiEnvFileWriter.js";
6
+ import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
5
7
  import { DockerStackManager } from "../core/docker/DockerStackManager.js";
8
+ import { ApiLocalService } from "../core/local/ApiLocalService.js";
9
+ import { LocalRepoSourceResolver } from "../core/local/LocalRepoSourceResolver.js";
10
+ import { LocalServiceProcessManager } from "../core/local/LocalServiceProcessManager.js";
11
+ import { WebLocalService } from "../core/local/WebLocalService.js";
6
12
  import { LogsService } from "../core/logs/LogsService.js";
7
13
  import { CommandRunner } from "../core/process/CommandRunner.js";
14
+ import { ProjectPaths } from "../core/runtime/ProjectPaths.js";
8
15
  import { RunnerSupervisor } from "../core/runner/RunnerSupervisor.js";
9
16
  import { createPasswordHash } from "../core/runtime/Secrets.js";
10
17
  import { RuntimePaths } from "../core/runtime/RuntimePaths.js";
11
18
  import { RuntimeStateStore } from "../core/runtime/RuntimeStateStore.js";
19
+ import { VersionCatalog } from "../core/runtime/VersionCatalog.js";
12
20
  import { StatusService } from "../core/status/StatusService.js";
13
21
  import { TerminalRenderer } from "../core/ui/TerminalRenderer.js";
22
+ import { ensureGithubAppConfig } from "./setup-github-app.js";
14
23
  function runtimeRoot() {
15
24
  return process.env.COMPANYHELM_HOME || path.join(os.homedir(), ".companyhelm");
16
25
  }
@@ -23,7 +32,38 @@ export function createDefaultDependencies() {
23
32
  const dockerStackManager = new DockerStackManager(root, commandRunner);
24
33
  const runnerSupervisor = new RunnerSupervisor(runtimePaths.runnerConfigPath());
25
34
  const bootstrapper = new DeploymentBootstrapper();
26
- const statusService = new StatusService(() => dockerStackManager.runningServices());
35
+ const githubAppConfigStore = new GithubAppConfigStore();
36
+ const apiEnvFileWriter = new ApiEnvFileWriter(process.cwd());
37
+ const projectPaths = new ProjectPaths(process.cwd());
38
+ const localRepoSourceResolver = new LocalRepoSourceResolver(process.cwd());
39
+ const localServiceProcessManager = new LocalServiceProcessManager();
40
+ const apiLocalService = new ApiLocalService(localServiceProcessManager, commandRunner);
41
+ const webLocalService = new WebLocalService(localServiceProcessManager, commandRunner);
42
+ const versionCatalog = new VersionCatalog();
43
+ const statusService = new StatusService(() => dockerStackManager.runningServices(), {
44
+ api: () => {
45
+ const state = stateStore.load();
46
+ return state?.services.api.source === "local"
47
+ ? localServiceProcessManager.isRunning(state.services.api)
48
+ : undefined;
49
+ },
50
+ frontend: () => {
51
+ const state = stateStore.load();
52
+ return state?.services.frontend.source === "local"
53
+ ? localServiceProcessManager.isRunning(state.services.frontend)
54
+ : undefined;
55
+ },
56
+ runner: async () => {
57
+ try {
58
+ const statusCommand = runnerSupervisor.buildStatusArgs();
59
+ const output = await commandRunner.capture(statusCommand.command, statusCommand.args);
60
+ return output.includes("Daemon: running");
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ });
27
67
  const logsService = new LogsService(async (service) => {
28
68
  if (service === "runner") {
29
69
  if (fs.existsSync(runtimePaths.runnerLogPath())) {
@@ -31,34 +71,140 @@ export function createDefaultDependencies() {
31
71
  }
32
72
  return;
33
73
  }
74
+ const state = stateStore.load();
75
+ if (service === "api" && state?.services.api.source === "local") {
76
+ localServiceProcessManager.printLogs(state.services.api);
77
+ return;
78
+ }
79
+ if (service === "frontend" && state?.services.frontend.source === "local") {
80
+ localServiceProcessManager.printLogs(state.services.frontend);
81
+ return;
82
+ }
34
83
  await dockerStackManager.logs(service);
35
84
  });
36
85
  return {
37
- async up() {
86
+ async up(options = {}) {
87
+ const logLevel = options.logLevel ?? "info";
88
+ const useHostDockerRuntime = options.useHostDockerRuntime ?? false;
89
+ const githubAppConfig = await ensureGithubAppConfig(githubAppConfigStore, process.stdin, process.stdout);
38
90
  const state = stateStore.initialize();
91
+ process.stdout.write(`${renderer.renderBanner()}\n`);
92
+ const runnerAlreadyRunning = await isRunnerRunning(commandRunner, runnerSupervisor);
93
+ const desiredSources = localRepoSourceResolver.resolve(options);
94
+ const versions = versionCatalog.resolve();
39
95
  const passwordRecord = createPasswordHash(state.auth.password);
96
+ const startedLocalServices = [];
97
+ let runnerStarted = false;
40
98
  fs.mkdirSync(root, { recursive: true });
99
+ fs.mkdirSync(runtimePaths.serviceRuntimePath(), { recursive: true });
100
+ apiEnvFileWriter.write(githubAppConfig);
41
101
  bootstrapper.writeSeedSql(root, state, passwordRecord.passwordHash, passwordRecord.passwordSalt);
42
- bootstrapper.writeApiConfig(root, state);
43
- bootstrapper.writeFrontendConfig(root, state);
44
- process.stdout.write(`${renderer.renderBanner()}\n`);
45
- await dockerStackManager.up(state);
46
- await dockerStackManager.applySeedSql();
47
- const startCommand = runnerSupervisor.buildStartArgs({
48
- serverUrl: `127.0.0.1:${state.ports.runnerGrpc}`,
49
- agentApiUrl: `127.0.0.1:${state.ports.agentCliGrpc}`,
50
- logPath: runtimePaths.runnerLogPath(),
51
- secret: state.runner.secret
102
+ bootstrapper.writeApiConfig(root, state, logLevel, {
103
+ databaseHost: desiredSources.api.source === "local" ? "127.0.0.1" : "postgres"
52
104
  });
53
- await commandRunner.run(startCommand.command, startCommand.args);
54
- process.stdout.write(`${renderer.success(`API: http://127.0.0.1:${state.ports.apiHttp}/graphql`)}\n`);
55
- process.stdout.write(`${renderer.success(`UI: http://127.0.0.1:${state.ports.ui}`)}\n`);
56
- process.stdout.write(`admin password: ${state.auth.password}\n`);
105
+ bootstrapper.writeFrontendConfig(root, state);
106
+ await stopLocalServicesFromState(stateStore.load(), localServiceProcessManager);
107
+ try {
108
+ await dockerStackManager.up(state, {
109
+ frontendLogLevel: logLevel,
110
+ includeApi: desiredSources.api.source === "docker",
111
+ includeFrontend: desiredSources.frontend.source === "docker",
112
+ exposePostgresPort: desiredSources.api.source === "local"
113
+ });
114
+ const apiUrl = `http://127.0.0.1:${state.ports.apiHttp}/graphql`;
115
+ const uiUrl = `http://127.0.0.1:${state.ports.ui}`;
116
+ if (desiredSources.api.source === "local") {
117
+ process.stdout.write(`${renderer.progress(`Starting companyhelm-api from ${desiredSources.api.repoPath}...`)}\n`);
118
+ state.services.api = await apiLocalService.start({
119
+ repoPath: desiredSources.api.repoPath,
120
+ configPath: runtimePaths.apiConfigPath(),
121
+ graphqlUrl: apiUrl,
122
+ logPath: runtimePaths.serviceLogPath("api"),
123
+ githubAppConfig,
124
+ state,
125
+ logLevel
126
+ });
127
+ startedLocalServices.push("api");
128
+ }
129
+ else {
130
+ state.services.api = { source: "docker" };
131
+ }
132
+ process.stdout.write(`${renderer.progress("Initializing the database...")}\n`);
133
+ process.stdout.write(`${renderer.progress("Waiting for database migrations...")}\n`);
134
+ await dockerStackManager.applySeedSql(state.auth.username);
135
+ if (runnerAlreadyRunning) {
136
+ process.stdout.write(`${renderer.progress("Runner already running; skipping startup.")}\n`);
137
+ }
138
+ else {
139
+ const configureSdkCommand = runnerSupervisor.buildUseHostAuthArgs();
140
+ process.stdout.write(`${renderer.progress("Configuring runner authentication...")}\n`);
141
+ await commandRunner.run(configureSdkCommand.command, configureSdkCommand.args);
142
+ const startCommand = runnerSupervisor.buildStartArgs({
143
+ serverUrl: `127.0.0.1:${state.ports.runnerGrpc}`,
144
+ agentApiUrl: `127.0.0.1:${state.ports.agentCliGrpc}`,
145
+ logPath: runtimePaths.runnerLogPath(),
146
+ secret: state.runner.secret,
147
+ logLevel,
148
+ useHostDockerRuntime
149
+ });
150
+ process.stdout.write(`${renderer.progress("Starting the runner...")}\n`);
151
+ await commandRunner.run(startCommand.command, startCommand.args);
152
+ runnerStarted = true;
153
+ }
154
+ if (desiredSources.frontend.source === "local") {
155
+ process.stdout.write(`${renderer.progress(`Starting companyhelm-web from ${desiredSources.frontend.repoPath}...`)}\n`);
156
+ state.services.frontend = await webLocalService.start({
157
+ repoPath: desiredSources.frontend.repoPath,
158
+ configPath: runtimePaths.frontendConfigPath(),
159
+ url: uiUrl,
160
+ uiPort: state.ports.ui,
161
+ logPath: runtimePaths.serviceLogPath("frontend"),
162
+ logLevel
163
+ });
164
+ startedLocalServices.push("frontend");
165
+ }
166
+ else {
167
+ state.services.frontend = { source: "docker" };
168
+ }
169
+ stateStore.persist(state);
170
+ process.stdout.write(`${renderer.success(`API ready: ${apiUrl}`)}\n`);
171
+ process.stdout.write(`CompanyHelm CLI: ${versions.cliPackage}\n`);
172
+ process.stdout.write(`Runner package: ${versions.runnerPackage}\n`);
173
+ process.stdout.write(desiredSources.api.source === "local"
174
+ ? `API repo: ${desiredSources.api.repoPath}\n`
175
+ : `API image: ${versions.images.api}\n`);
176
+ process.stdout.write(desiredSources.frontend.source === "local"
177
+ ? `companyhelm-web repo: ${desiredSources.frontend.repoPath}\n`
178
+ : `companyhelm-web image: ${versions.images.frontend}\n`);
179
+ process.stdout.write(`Postgres image: ${versions.images.postgres}\n`);
180
+ process.stdout.write(`\n${renderer.success("CompanyHelm started successfully.")}\n`);
181
+ process.stdout.write(`${renderer.successHighlight("UI URL")}\n`);
182
+ process.stdout.write(`${renderer.clickableUrl(uiUrl)}\n`);
183
+ process.stdout.write(`${renderer.successHighlight("Login credentials")}\n`);
184
+ process.stdout.write(`username: ${state.auth.username}\n`);
185
+ process.stdout.write(`password: ${state.auth.password}\n`);
186
+ }
187
+ catch (error) {
188
+ if (runnerStarted) {
189
+ const stopCommand = runnerSupervisor.buildStopArgs();
190
+ try {
191
+ await commandRunner.run(stopCommand.command, stopCommand.args);
192
+ }
193
+ catch {
194
+ // Ignore runner stop failures during cleanup.
195
+ }
196
+ }
197
+ for (const service of startedLocalServices.reverse()) {
198
+ const runtime = service === "api" ? state.services.api : state.services.frontend;
199
+ if (runtime.source === "local") {
200
+ await localServiceProcessManager.stop(runtime);
201
+ }
202
+ }
203
+ await dockerStackManager.down();
204
+ throw error;
205
+ }
57
206
  },
58
207
  async down() {
59
- if (!stateStore.load()) {
60
- return;
61
- }
62
208
  const stopCommand = runnerSupervisor.buildStopArgs();
63
209
  try {
64
210
  await commandRunner.run(stopCommand.command, stopCommand.args);
@@ -66,17 +212,66 @@ export function createDefaultDependencies() {
66
212
  catch {
67
213
  // Ignore runner stop failures during teardown so container cleanup still happens.
68
214
  }
215
+ const state = stateStore.load();
216
+ if (!state) {
217
+ return;
218
+ }
219
+ await stopLocalServicesFromState(state, localServiceProcessManager);
69
220
  await dockerStackManager.down();
70
221
  },
71
- status() {
72
- return statusService.read();
222
+ async status() {
223
+ const services = await statusService.read();
224
+ const state = stateStore.load();
225
+ return {
226
+ services,
227
+ apiUrl: state ? `http://127.0.0.1:${state.ports.apiHttp}/graphql` : undefined,
228
+ uiUrl: state ? `http://127.0.0.1:${state.ports.ui}` : undefined,
229
+ username: state?.auth.username,
230
+ versions: versionCatalog.resolve()
231
+ };
73
232
  },
74
233
  logs(service) {
75
234
  return logsService.stream(service);
76
235
  },
77
- async reset() {
78
- await this.down();
236
+ async reset(options = {}) {
237
+ const state = stateStore.load();
238
+ if (state) {
239
+ const stopCommand = runnerSupervisor.buildStopArgs();
240
+ try {
241
+ await commandRunner.run(stopCommand.command, stopCommand.args);
242
+ }
243
+ catch {
244
+ // Ignore runner stop failures during teardown so docker cleanup still runs.
245
+ }
246
+ await stopLocalServicesFromState(state, localServiceProcessManager);
247
+ }
248
+ await dockerStackManager.down({ removeVolumes: true });
249
+ fs.rmSync(projectPaths.apiEnvPath(), { force: true });
79
250
  fs.rmSync(root, { recursive: true, force: true });
251
+ if (options.removeGithubAppConfig) {
252
+ githubAppConfigStore.delete();
253
+ }
80
254
  }
81
255
  };
82
256
  }
257
+ async function stopLocalServicesFromState(state, processManager) {
258
+ if (!state) {
259
+ return;
260
+ }
261
+ if (state.services.api.source === "local") {
262
+ await processManager.stop(state.services.api);
263
+ }
264
+ if (state.services.frontend.source === "local") {
265
+ await processManager.stop(state.services.frontend);
266
+ }
267
+ }
268
+ async function isRunnerRunning(commandRunner, runnerSupervisor) {
269
+ try {
270
+ const statusCommand = runnerSupervisor.buildStatusArgs();
271
+ const output = await commandRunner.capture(statusCommand.command, statusCommand.args);
272
+ return output.includes("Daemon: running");
273
+ }
274
+ catch (error) {
275
+ return false;
276
+ }
277
+ }
@@ -0,0 +1,6 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ export declare class InteractiveCommandCancelledError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function requireInteractiveTerminal(input: Readable, output: Writable, message: string): void;
6
+ export declare function unwrapPromptResult<T>(value: T | symbol, message: string, output: Writable): T;
@@ -0,0 +1,22 @@
1
+ import * as clack from "@clack/prompts";
2
+ export class InteractiveCommandCancelledError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "InteractiveCommandCancelledError";
6
+ }
7
+ }
8
+ function isReadableTty(input) {
9
+ return "isTTY" in input && Boolean(input.isTTY);
10
+ }
11
+ export function requireInteractiveTerminal(input, output, message) {
12
+ if (!isReadableTty(input) || !clack.isTTY(output)) {
13
+ throw new Error(message);
14
+ }
15
+ }
16
+ export function unwrapPromptResult(value, message, output) {
17
+ if (clack.isCancel(value)) {
18
+ clack.cancel(message, { output });
19
+ throw new InteractiveCommandCancelledError(message);
20
+ }
21
+ return value;
22
+ }
@@ -1,9 +1,14 @@
1
+ const AVAILABLE_LOG_SERVICES = ["postgres", "api", "frontend", "runner"];
1
2
  export function registerLogsCommand(program, dependencies) {
2
3
  program
3
4
  .command("logs")
4
5
  .description("Show logs for a managed service.")
5
- .argument("<service>")
6
+ .argument("[service]")
6
7
  .action(async (service) => {
8
+ if (!service) {
9
+ process.stdout.write(`Available services:\n${AVAILABLE_LOG_SERVICES.map((name) => `- ${name}`).join("\n")}\n`);
10
+ return;
11
+ }
7
12
  await dependencies.logs(service);
8
13
  });
9
14
  }
@@ -3,14 +3,18 @@ import { createDefaultDependencies } from "./dependencies.js";
3
3
  import { registerDownCommand } from "./down.js";
4
4
  import { registerLogsCommand } from "./logs.js";
5
5
  import { registerResetCommand } from "./reset.js";
6
+ import { registerSetupGithubAppCommand } from "./setup-github-app.js";
7
+ import { registerSetImageVersionCommand } from "./set-image-version.js";
6
8
  import { registerStatusCommand } from "./status.js";
7
9
  import { registerUpCommand } from "./up.js";
8
10
  export function buildProgram(dependencies = createDefaultDependencies()) {
9
11
  const program = new Command().name("companyhelm");
12
+ registerSetupGithubAppCommand(program);
10
13
  registerUpCommand(program, dependencies);
11
14
  registerDownCommand(program, dependencies);
12
15
  registerStatusCommand(program, dependencies);
13
16
  registerLogsCommand(program, dependencies);
17
+ registerSetImageVersionCommand(program);
14
18
  registerResetCommand(program, dependencies);
15
19
  return program;
16
20
  }
@@ -1,3 +1,7 @@
1
+ import type { Readable, Writable } from "node:stream";
1
2
  import type { Command } from "commander";
3
+ import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
2
4
  import type { CommandDependencies } from "./dependencies.js";
5
+ export declare function confirmReset(input?: Readable, output?: Writable): Promise<boolean>;
6
+ export declare function confirmRemoveGithubAppConfig(store?: GithubAppConfigStore, input?: Readable, output?: Writable): Promise<boolean>;
3
7
  export declare function registerResetCommand(program: Command, dependencies: CommandDependencies): void;
@@ -1,12 +1,51 @@
1
+ import * as clack from "@clack/prompts";
2
+ import { GithubAppConfigStore } from "../core/config/GithubAppConfigStore.js";
3
+ import { requireInteractiveTerminal, unwrapPromptResult } from "./interactive.js";
4
+ export async function confirmReset(input = process.stdin, output = process.stdout) {
5
+ requireInteractiveTerminal(input, output, "reset requires confirmation from a TTY. Re-run with --yes to skip the prompt.");
6
+ const confirmed = await clack.confirm({
7
+ message: "This will remove CompanyHelm containers, Postgres data, local runtime state, and generated .companyhelm/api/.env. Continue?",
8
+ active: "Yes",
9
+ inactive: "No",
10
+ initialValue: false,
11
+ input,
12
+ output
13
+ });
14
+ return unwrapPromptResult(confirmed, "Reset cancelled.", output);
15
+ }
16
+ export async function confirmRemoveGithubAppConfig(store = new GithubAppConfigStore(), input = process.stdin, output = process.stdout) {
17
+ if (!store.hasConfig()) {
18
+ return false;
19
+ }
20
+ requireInteractiveTerminal(input, output, "reset requires confirmation from a TTY. Re-run with --yes to skip the prompt.");
21
+ const confirmed = await clack.confirm({
22
+ message: `Also remove the machine GitHub App config at ${store.configPath()}?`,
23
+ active: "Remove it",
24
+ inactive: "Keep it",
25
+ initialValue: false,
26
+ input,
27
+ output
28
+ });
29
+ return unwrapPromptResult(confirmed, "Reset cancelled.", output);
30
+ }
1
31
  export function registerResetCommand(program, dependencies) {
2
32
  program
3
33
  .command("reset")
4
34
  .description("Destroy the local deployment state.")
5
- .option("--force")
35
+ .option("-y, --yes", "Skip the confirmation prompt.")
36
+ .option("--remove-github-app-config", "Also remove the machine GitHub App config.")
6
37
  .action(async (options) => {
7
- if (!options.force) {
8
- throw new Error("reset requires --force");
38
+ let removeGithubAppConfig = Boolean(options.removeGithubAppConfig);
39
+ if (!options.yes) {
40
+ const confirmed = await confirmReset();
41
+ if (!confirmed) {
42
+ clack.cancel("Reset cancelled.");
43
+ return;
44
+ }
45
+ if (!removeGithubAppConfig) {
46
+ removeGithubAppConfig = await confirmRemoveGithubAppConfig();
47
+ }
9
48
  }
10
- await dependencies.reset();
49
+ await dependencies.reset({ removeGithubAppConfig });
11
50
  });
12
51
  }
@@ -0,0 +1,31 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import type { Command } from "commander";
3
+ import { type ManagedImageService } from "../core/runtime/ManagedImages.js";
4
+ export interface SetImageVersionOptions {
5
+ service?: string;
6
+ limit: number;
7
+ }
8
+ export interface AvailableImageTag {
9
+ tag: string;
10
+ createdAt?: string;
11
+ }
12
+ export interface InteractiveImageSelector {
13
+ listAvailableTags(service: ManagedImageService, limit: number): Promise<AvailableImageTag[]>;
14
+ buildImageReference(service: ManagedImageService, tag: string): string;
15
+ }
16
+ export interface ImageConfigStore {
17
+ load(): {
18
+ images: Partial<Record<ManagedImageService, string>>;
19
+ };
20
+ setImage(service: ManagedImageService, image: string): {
21
+ configPath: string;
22
+ image: string;
23
+ };
24
+ }
25
+ export declare function runSetImageVersion(options: SetImageVersionOptions, dependencies?: {
26
+ input?: Readable;
27
+ output?: Writable;
28
+ registry?: InteractiveImageSelector;
29
+ configStore?: ImageConfigStore;
30
+ }): Promise<void>;
31
+ export declare function registerSetImageVersionCommand(program: Command): void;