@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
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function defaultCliRoot(): string;
2
+ export declare function defaultCliConfigRoot(): string;
@@ -0,0 +1,23 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ function defaultCliBaseRoot() {
4
+ return path.join(os.homedir(), ".companyhelm", "cli");
5
+ }
6
+ export function defaultCliRoot() {
7
+ const explicitRoot = String(process.env.COMPANYHELM_HOME || "").trim();
8
+ if (explicitRoot) {
9
+ return path.resolve(explicitRoot);
10
+ }
11
+ return path.join(defaultCliBaseRoot(), "runtime");
12
+ }
13
+ export function defaultCliConfigRoot() {
14
+ const explicitRoot = String(process.env.COMPANYHELM_CONFIG_HOME || "").trim();
15
+ if (explicitRoot) {
16
+ return path.resolve(explicitRoot);
17
+ }
18
+ const explicitRuntimeRoot = String(process.env.COMPANYHELM_HOME || "").trim();
19
+ if (explicitRuntimeRoot) {
20
+ return path.resolve(explicitRuntimeRoot);
21
+ }
22
+ return defaultCliBaseRoot();
23
+ }
@@ -1,8 +1,8 @@
1
- import { LocalConfigStore } from "./LocalConfigStore.js";
2
1
  import { defaultManagedImageReference } from "./ManagedImages.js";
2
+ import { RepoConfigStore } from "./RepoConfigStore.js";
3
3
  export class ImageCatalog {
4
4
  resolve() {
5
- const configuredImages = new LocalConfigStore().load().images;
5
+ const configuredImages = new RepoConfigStore().load().images;
6
6
  return {
7
7
  api: configuredImages.api || process.env.COMPANYHELM_API_IMAGE || defaultManagedImageReference("api"),
8
8
  frontend: configuredImages.frontend || process.env.COMPANYHELM_WEB_IMAGE || defaultManagedImageReference("frontend"),
@@ -1,15 +1,15 @@
1
- import type { ManagedImageService } from "./ManagedImages.js";
1
+ export type AgentWorkspaceMode = "dedicated" | "current-working-directory";
2
2
  export interface LocalConfig {
3
- images: Partial<Record<ManagedImageService, string>>;
3
+ agentWorkspaceMode?: AgentWorkspaceMode;
4
4
  }
5
5
  export declare class LocalConfigStore {
6
6
  private readonly root;
7
7
  constructor(root?: string);
8
8
  configPath(): string;
9
9
  load(): LocalConfig;
10
- setImage(service: ManagedImageService, image: string): {
10
+ setAgentWorkspaceMode(agentWorkspaceMode: AgentWorkspaceMode): {
11
11
  configPath: string;
12
- image: string;
12
+ agentWorkspaceMode: AgentWorkspaceMode;
13
13
  };
14
14
  save(config: LocalConfig): void;
15
15
  private parse;
@@ -1,8 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { defaultCliConfigRoot } from "./CliRoot.js";
4
+ function defaultLocalConfigRoot() {
5
+ return defaultCliConfigRoot();
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() {
@@ -11,49 +15,36 @@ export class LocalConfigStore {
11
15
  load() {
12
16
  const configPath = this.configPath();
13
17
  if (!fs.existsSync(configPath)) {
14
- return { images: {} };
18
+ return {};
15
19
  }
16
20
  return this.parse(fs.readFileSync(configPath, "utf8"));
17
21
  }
18
- setImage(service, image) {
22
+ setAgentWorkspaceMode(agentWorkspaceMode) {
19
23
  const nextConfig = this.load();
20
- nextConfig.images[service] = image;
24
+ nextConfig.agentWorkspaceMode = agentWorkspaceMode;
21
25
  this.save(nextConfig);
22
- return { configPath: this.configPath(), image };
26
+ return { configPath: this.configPath(), agentWorkspaceMode };
23
27
  }
24
28
  save(config) {
25
- const lines = ["images:"];
26
- if (config.images.api) {
27
- lines.push(` api: ${config.images.api}`);
28
- }
29
- if (config.images.frontend) {
30
- lines.push(` frontend: ${config.images.frontend}`);
29
+ const lines = [];
30
+ if (config.agentWorkspaceMode) {
31
+ lines.push(`agent_workspace_mode: ${config.agentWorkspaceMode}`);
31
32
  }
33
+ fs.mkdirSync(path.dirname(this.configPath()), { recursive: true });
32
34
  fs.writeFileSync(this.configPath(), `${lines.join("\n")}\n`, "utf8");
33
35
  }
34
36
  parse(content) {
35
- const images = {};
36
- let inImagesSection = false;
37
+ let agentWorkspaceMode;
37
38
  for (const rawLine of content.split(/\r?\n/)) {
38
39
  const line = rawLine.trimEnd();
39
40
  if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
40
41
  continue;
41
42
  }
42
- if (line === "images:") {
43
- inImagesSection = true;
44
- continue;
45
- }
46
- if (inImagesSection && /^[^\s]/.test(line)) {
47
- inImagesSection = false;
48
- }
49
- if (!inImagesSection) {
50
- continue;
51
- }
52
- const match = line.match(/^ (api|frontend):\s*(.+)$/);
53
- if (match) {
54
- images[match[1]] = match[2];
43
+ const workspaceModeMatch = line.match(/^agent_workspace_mode:\s*(dedicated|current-working-directory)$/);
44
+ if (workspaceModeMatch) {
45
+ agentWorkspaceMode = workspaceModeMatch[1];
55
46
  }
56
47
  }
57
- return { images };
48
+ return { agentWorkspaceMode };
58
49
  }
59
50
  }
@@ -12,5 +12,6 @@ export declare class PublicImageTagRegistry {
12
12
  private fetchJson;
13
13
  private parseRetryAfterMs;
14
14
  private sleep;
15
+ private mapWithConcurrency;
15
16
  }
16
17
  export {};
@@ -2,6 +2,7 @@ import { buildManagedImageReference, getManagedImageDefinition } from "./Managed
2
2
  const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
3
3
  const MAX_FETCH_ATTEMPTS = 3;
4
4
  const DEFAULT_RETRY_DELAY_MS = 250;
5
+ const METADATA_FETCH_CONCURRENCY = 6;
5
6
  class PublicRegistryRequestError extends Error {
6
7
  status;
7
8
  retryAfterMs;
@@ -31,15 +32,13 @@ export class PublicImageTagRegistry {
31
32
  throw new Error(payload.errors[0]?.message || `Unable to load ${service} image tags.`);
32
33
  }
33
34
  const uniqueTags = [...new Set(payload.tags ?? [])].map((tag, position) => ({ tag, position }));
35
+ const candidateTags = uniqueTags.slice(0, limit);
34
36
  const createdAtByDigest = new Map();
35
- const tagsWithMetadata = [];
36
- for (const { tag, position } of uniqueTags) {
37
- tagsWithMetadata.push({
38
- tag,
39
- position,
40
- createdAt: await this.fetchCreatedAt(repositoryPath, token, tag, createdAtByDigest)
41
- });
42
- }
37
+ const tagsWithMetadata = await this.mapWithConcurrency(candidateTags, METADATA_FETCH_CONCURRENCY, async ({ tag, position }) => ({
38
+ tag,
39
+ position,
40
+ createdAt: await this.fetchCreatedAt(repositoryPath, token, tag, createdAtByDigest)
41
+ }));
43
42
  return tagsWithMetadata
44
43
  .sort((left, right) => {
45
44
  const leftTimestamp = left.createdAt ? Date.parse(left.createdAt) : Number.NEGATIVE_INFINITY;
@@ -49,7 +48,6 @@ export class PublicImageTagRegistry {
49
48
  }
50
49
  return left.position - right.position;
51
50
  })
52
- .slice(0, limit)
53
51
  .map(({ tag, createdAt }) => ({ tag, createdAt }));
54
52
  }
55
53
  buildImageReference(service, tag) {
@@ -89,14 +87,20 @@ export class PublicImageTagRegistry {
89
87
  if (!configDigest) {
90
88
  return undefined;
91
89
  }
92
- if (createdAtByDigest.has(configDigest)) {
93
- return createdAtByDigest.get(configDigest);
90
+ const existingCreatedAt = createdAtByDigest.get(configDigest);
91
+ if (existingCreatedAt) {
92
+ return await existingCreatedAt;
94
93
  }
95
- const config = await this.fetchJson(`https://public.ecr.aws/v2/${repositoryPath}/blobs/${configDigest}`, {
94
+ const createdAtPromise = this.fetchJson(`https://public.ecr.aws/v2/${repositoryPath}/blobs/${configDigest}`, {
96
95
  Authorization: `Bearer ${token}`
96
+ })
97
+ .then((config) => config.created)
98
+ .catch((error) => {
99
+ createdAtByDigest.delete(configDigest);
100
+ throw error;
97
101
  });
98
- createdAtByDigest.set(configDigest, config.created);
99
- return config.created;
102
+ createdAtByDigest.set(configDigest, createdAtPromise);
103
+ return await createdAtPromise;
100
104
  }
101
105
  catch (error) {
102
106
  if (error instanceof PublicRegistryRequestError) {
@@ -145,4 +149,20 @@ export class PublicImageTagRegistry {
145
149
  async sleep(delayMs) {
146
150
  await new Promise((resolve) => setTimeout(resolve, delayMs));
147
151
  }
152
+ async mapWithConcurrency(items, concurrency, mapper) {
153
+ if (items.length === 0) {
154
+ return [];
155
+ }
156
+ const results = new Array(items.length);
157
+ let nextIndex = 0;
158
+ const workerCount = Math.min(concurrency, items.length);
159
+ await Promise.all(Array.from({ length: workerCount }, async () => {
160
+ while (nextIndex < items.length) {
161
+ const currentIndex = nextIndex;
162
+ nextIndex += 1;
163
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
164
+ }
165
+ }));
166
+ return results;
167
+ }
148
168
  }
@@ -0,0 +1,16 @@
1
+ import type { ManagedImageService } from "./ManagedImages.js";
2
+ export interface RepoConfig {
3
+ images: Partial<Record<ManagedImageService, string>>;
4
+ }
5
+ export declare class RepoConfigStore {
6
+ private readonly root;
7
+ constructor(root?: string);
8
+ configPath(): string;
9
+ load(): RepoConfig;
10
+ setImage(service: ManagedImageService, image: string): {
11
+ configPath: string;
12
+ image: string;
13
+ };
14
+ save(config: RepoConfig): void;
15
+ private parse;
16
+ }
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function defaultRepoConfigRoot() {
4
+ return process.cwd();
5
+ }
6
+ export class RepoConfigStore {
7
+ root;
8
+ constructor(root = defaultRepoConfigRoot()) {
9
+ this.root = root;
10
+ }
11
+ configPath() {
12
+ return path.join(this.root, "config.yaml");
13
+ }
14
+ load() {
15
+ const configPath = this.configPath();
16
+ if (!fs.existsSync(configPath)) {
17
+ return { images: {} };
18
+ }
19
+ return this.parse(fs.readFileSync(configPath, "utf8"));
20
+ }
21
+ setImage(service, image) {
22
+ const nextConfig = this.load();
23
+ nextConfig.images[service] = image;
24
+ this.save(nextConfig);
25
+ return { configPath: this.configPath(), image };
26
+ }
27
+ save(config) {
28
+ const lines = ["images:"];
29
+ if (config.images.api) {
30
+ lines.push(` api: ${config.images.api}`);
31
+ }
32
+ if (config.images.frontend) {
33
+ lines.push(` frontend: ${config.images.frontend}`);
34
+ }
35
+ fs.mkdirSync(path.dirname(this.configPath()), { recursive: true });
36
+ fs.writeFileSync(this.configPath(), `${lines.join("\n")}\n`, "utf8");
37
+ }
38
+ parse(content) {
39
+ const images = {};
40
+ let inImagesSection = false;
41
+ for (const rawLine of content.split(/\r?\n/)) {
42
+ const line = rawLine.trimEnd();
43
+ if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
44
+ continue;
45
+ }
46
+ if (line === "images:") {
47
+ inImagesSection = true;
48
+ continue;
49
+ }
50
+ if (inImagesSection && /^[^\s]/.test(line)) {
51
+ inImagesSection = false;
52
+ }
53
+ if (!inImagesSection) {
54
+ continue;
55
+ }
56
+ const match = line.match(/^ (api|frontend):\s*(.+)$/);
57
+ if (match) {
58
+ images[match[1]] = match[2];
59
+ }
60
+ }
61
+ return { images };
62
+ }
63
+ }
@@ -3,6 +3,8 @@ export declare class RuntimePaths {
3
3
  constructor(root: string);
4
4
  stateFilePath(): string;
5
5
  composeFilePath(): string;
6
+ apiDirectoryPath(): string;
7
+ apiEnvPath(): string;
6
8
  apiConfigPath(): string;
7
9
  frontendConfigPath(): string;
8
10
  seedFilePath(): string;
@@ -10,6 +10,12 @@ export class RuntimePaths {
10
10
  composeFilePath() {
11
11
  return path.join(this.root, "docker-compose.yaml");
12
12
  }
13
+ apiDirectoryPath() {
14
+ return path.join(this.root, "api");
15
+ }
16
+ apiEnvPath() {
17
+ return path.join(this.apiDirectoryPath(), ".env");
18
+ }
13
19
  apiConfigPath() {
14
20
  return path.join(this.root, "api-config.yaml");
15
21
  }
@@ -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 {};