@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.
- package/README.md +6 -1
- package/dist/cli.js +12 -1
- package/dist/commands/dependencies.js +29 -14
- package/dist/commands/interactive.d.ts +1 -0
- package/dist/commands/interactive.js +4 -1
- package/dist/commands/logs.js +2 -2
- package/dist/commands/register-commands.js +4 -1
- package/dist/commands/reset.js +1 -1
- package/dist/commands/set-image-version.js +3 -3
- package/dist/commands/setup-github-app.d.ts +4 -1
- package/dist/commands/setup-github-app.js +30 -8
- package/dist/commands/startup-preferences.d.ts +3 -0
- package/dist/commands/startup-preferences.js +39 -0
- package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +2 -1
- package/dist/core/bootstrap/DeploymentBootstrapper.js +23 -8
- package/dist/core/config/ApiEnvFileWriter.d.ts +5 -3
- package/dist/core/config/ApiEnvFileWriter.js +20 -13
- package/dist/core/config/GithubAppConfigStore.js +2 -10
- package/dist/core/docker/ComposeTemplateRenderer.js +0 -1
- package/dist/core/docker/DockerStackManager.js +1 -2
- package/dist/core/local/ApiLocalService.d.ts +1 -1
- package/dist/core/local/ApiLocalService.js +3 -3
- package/dist/core/logs/LogsService.d.ts +2 -1
- package/dist/core/logs/LogsService.js +5 -4
- package/dist/core/runner/RunnerSupervisor.d.ts +6 -0
- package/dist/core/runner/RunnerSupervisor.js +20 -3
- package/dist/core/runner/runner-bootstrap.d.ts +2 -0
- package/dist/core/runner/runner-bootstrap.js +48 -0
- package/dist/core/runtime/CliPackageMetadata.d.ts +3 -0
- package/dist/core/runtime/CliPackageMetadata.js +8 -0
- package/dist/core/runtime/CliRoot.d.ts +2 -0
- package/dist/core/runtime/CliRoot.js +23 -0
- package/dist/core/runtime/ImageCatalog.js +2 -2
- package/dist/core/runtime/LocalConfigStore.d.ts +4 -4
- package/dist/core/runtime/LocalConfigStore.js +18 -27
- package/dist/core/runtime/PublicImageTagRegistry.d.ts +1 -0
- package/dist/core/runtime/PublicImageTagRegistry.js +34 -14
- package/dist/core/runtime/RepoConfigStore.d.ts +16 -0
- package/dist/core/runtime/RepoConfigStore.js +63 -0
- package/dist/core/runtime/RuntimePaths.d.ts +2 -0
- package/dist/core/runtime/RuntimePaths.js +6 -0
- package/dist/core/services/ManagedServiceNames.d.ts +5 -0
- package/dist/core/services/ManagedServiceNames.js +12 -0
- package/dist/preflight/ApiPortPreflightCheck.d.ts +6 -0
- package/dist/preflight/ApiPortPreflightCheck.js +10 -0
- package/dist/preflight/DockerInstalledPreflightCheck.d.ts +7 -0
- package/dist/preflight/DockerInstalledPreflightCheck.js +15 -0
- package/dist/preflight/PortAvailabilityPreflightCheck.d.ts +7 -0
- package/dist/preflight/PortAvailabilityPreflightCheck.js +31 -0
- package/dist/preflight/PostgresPortPreflightCheck.d.ts +6 -0
- package/dist/preflight/PostgresPortPreflightCheck.js +10 -0
- package/dist/preflight/PreflightCheck.d.ts +3 -0
- package/dist/preflight/PreflightCheck.js +1 -0
- package/dist/preflight/WebPortPreflightCheck.d.ts +6 -0
- package/dist/preflight/WebPortPreflightCheck.js +10 -0
- package/dist/preflight/runStartupPreflightChecks.d.ts +18 -0
- package/dist/preflight/runStartupPreflightChecks.js +42 -0
- package/dist/templates/api.env.tpl +3 -0
- package/package.json +2 -2
- package/src/templates/api.env.tpl +3 -0
- package/dist/core/runtime/ProjectPaths.d.ts +0 -7
- 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
|
-
|
|
34
|
+
runnerEntrypoint,
|
|
35
|
+
...(runnerCliOverridePath ? [runnerCliOverridePath] : []),
|
|
27
36
|
"--config-path",
|
|
28
37
|
this.configPath,
|
|
29
38
|
"start",
|
|
@@ -39,7 +48,8 @@ export class RunnerSupervisor {
|
|
|
39
48
|
input.secret,
|
|
40
49
|
"--log-level",
|
|
41
50
|
logLevel
|
|
42
|
-
]
|
|
51
|
+
],
|
|
52
|
+
env
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
buildStopArgs() {
|
|
@@ -63,6 +73,13 @@ export class RunnerSupervisor {
|
|
|
63
73
|
const packageJsonPath = require.resolve("@companyhelm/runner/package.json");
|
|
64
74
|
return path.resolve(path.dirname(packageJsonPath), "dist/cli.js");
|
|
65
75
|
}
|
|
76
|
+
resolveRunnerCliOverridePath() {
|
|
77
|
+
const overridePath = String(process.env.COMPANYHELM_RUNNER_CLI_PATH || "").trim();
|
|
78
|
+
return overridePath || null;
|
|
79
|
+
}
|
|
80
|
+
resolveRunnerEntrypointPath() {
|
|
81
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "runner-bootstrap.js");
|
|
82
|
+
}
|
|
66
83
|
resolveHostDockerPath() {
|
|
67
84
|
const dockerHost = String(process.env.DOCKER_HOST || "").trim();
|
|
68
85
|
if (dockerHost) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
function applyCurrentWorkingDirectoryMode(projectRoot) {
|
|
6
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
7
|
+
const fsModule = require("node:fs");
|
|
8
|
+
const originalRmSync = fsModule.rmSync.bind(fsModule);
|
|
9
|
+
fsModule.rmSync = ((target, options) => {
|
|
10
|
+
if (path.resolve(String(target)) === resolvedProjectRoot) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
return originalRmSync(target, options);
|
|
14
|
+
});
|
|
15
|
+
const configModule = require("@companyhelm/runner/dist/config.js");
|
|
16
|
+
const originalParse = configModule.config.parse.bind(configModule.config);
|
|
17
|
+
configModule.config.parse = ((input = {}) => originalParse({
|
|
18
|
+
...input,
|
|
19
|
+
workspaces_directory: resolvedProjectRoot
|
|
20
|
+
}));
|
|
21
|
+
const threadLifecycle = require("@companyhelm/runner/dist/service/thread_lifecycle.js");
|
|
22
|
+
threadLifecycle.resolveThreadDirectory = (() => resolvedProjectRoot);
|
|
23
|
+
}
|
|
24
|
+
export function resolveRunnerCliEntrypointArg(argv) {
|
|
25
|
+
const candidate = String(argv[2] || "").trim();
|
|
26
|
+
if (!candidate || candidate.startsWith("-")) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
const workspaceMode = String(process.env.COMPANYHELM_RUNNER_WORKSPACE_MODE || "").trim();
|
|
32
|
+
const projectRoot = String(process.env.COMPANYHELM_RUNNER_PROJECT_ROOT || "").trim();
|
|
33
|
+
export async function runRunnerBootstrap() {
|
|
34
|
+
if (workspaceMode === "current-working-directory" && projectRoot) {
|
|
35
|
+
applyCurrentWorkingDirectoryMode(projectRoot);
|
|
36
|
+
}
|
|
37
|
+
const runnerEntrypoint = resolveRunnerCliEntrypointArg(process.argv);
|
|
38
|
+
if (runnerEntrypoint) {
|
|
39
|
+
process.argv.splice(2, 1);
|
|
40
|
+
}
|
|
41
|
+
await import(runnerEntrypoint ? pathToFileURL(runnerEntrypoint).href : "@companyhelm/runner/dist/cli.js");
|
|
42
|
+
}
|
|
43
|
+
const invokedAsEntrypoint = process.argv[1]
|
|
44
|
+
? pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url
|
|
45
|
+
: false;
|
|
46
|
+
if (invokedAsEntrypoint) {
|
|
47
|
+
await runRunnerBootstrap();
|
|
48
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
1
|
+
export type AgentWorkspaceMode = "dedicated" | "current-working-directory";
|
|
2
2
|
export interface LocalConfig {
|
|
3
|
-
|
|
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
|
-
|
|
10
|
+
setAgentWorkspaceMode(agentWorkspaceMode: AgentWorkspaceMode): {
|
|
11
11
|
configPath: string;
|
|
12
|
-
|
|
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 =
|
|
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 {
|
|
18
|
+
return {};
|
|
15
19
|
}
|
|
16
20
|
return this.parse(fs.readFileSync(configPath, "utf8"));
|
|
17
21
|
}
|
|
18
|
-
|
|
22
|
+
setAgentWorkspaceMode(agentWorkspaceMode) {
|
|
19
23
|
const nextConfig = this.load();
|
|
20
|
-
nextConfig.
|
|
24
|
+
nextConfig.agentWorkspaceMode = agentWorkspaceMode;
|
|
21
25
|
this.save(nextConfig);
|
|
22
|
-
return { configPath: this.configPath(),
|
|
26
|
+
return { configPath: this.configPath(), agentWorkspaceMode };
|
|
23
27
|
}
|
|
24
28
|
save(config) {
|
|
25
|
-
const lines = [
|
|
26
|
-
if (config.
|
|
27
|
-
lines.push(`
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 {
|
|
48
|
+
return { agentWorkspaceMode };
|
|
58
49
|
}
|
|
59
50
|
}
|
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
93
|
-
|
|
90
|
+
const existingCreatedAt = createdAtByDigest.get(configDigest);
|
|
91
|
+
if (existingCreatedAt) {
|
|
92
|
+
return await existingCreatedAt;
|
|
94
93
|
}
|
|
95
|
-
const
|
|
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,
|
|
99
|
-
return
|
|
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
|
+
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
import { PortAvailabilityPreflightCheck } from "./PortAvailabilityPreflightCheck.js";
|
|
2
|
+
export class ApiPortPreflightCheck {
|
|
3
|
+
delegate;
|
|
4
|
+
constructor(port) {
|
|
5
|
+
this.delegate = new PortAvailabilityPreflightCheck("companyhelm-api", port);
|
|
6
|
+
}
|
|
7
|
+
run() {
|
|
8
|
+
return this.delegate.run();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CommandRunner } from "../core/process/CommandRunner.js";
|
|
2
|
+
import type { PreflightCheck } from "./PreflightCheck.js";
|
|
3
|
+
export declare class DockerInstalledPreflightCheck implements PreflightCheck {
|
|
4
|
+
private readonly commandRunner;
|
|
5
|
+
constructor(commandRunner?: CommandRunner);
|
|
6
|
+
run(): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CommandRunner } from "../core/process/CommandRunner.js";
|
|
2
|
+
export class DockerInstalledPreflightCheck {
|
|
3
|
+
commandRunner;
|
|
4
|
+
constructor(commandRunner = new CommandRunner()) {
|
|
5
|
+
this.commandRunner = commandRunner;
|
|
6
|
+
}
|
|
7
|
+
async run() {
|
|
8
|
+
try {
|
|
9
|
+
await this.commandRunner.capture("docker", ["--version"]);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("Docker is required for `companyhelm up`, but the `docker` command is unavailable. Install Docker and make sure it is on your PATH.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PreflightCheck } from "./PreflightCheck.js";
|
|
2
|
+
export declare class PortAvailabilityPreflightCheck implements PreflightCheck {
|
|
3
|
+
private readonly serviceName;
|
|
4
|
+
private readonly port;
|
|
5
|
+
constructor(serviceName: string, port: number);
|
|
6
|
+
run(): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
export class PortAvailabilityPreflightCheck {
|
|
3
|
+
serviceName;
|
|
4
|
+
port;
|
|
5
|
+
constructor(serviceName, port) {
|
|
6
|
+
this.serviceName = serviceName;
|
|
7
|
+
this.port = port;
|
|
8
|
+
}
|
|
9
|
+
async run() {
|
|
10
|
+
await new Promise((resolve, reject) => {
|
|
11
|
+
const server = net.createServer();
|
|
12
|
+
server.once("error", (error) => {
|
|
13
|
+
if (error.code === "EADDRINUSE") {
|
|
14
|
+
reject(new Error(`${this.serviceName} cannot start because port ${this.port} is already in use.`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
reject(new Error(`${this.serviceName} cannot verify port ${this.port}: ${error.message}`));
|
|
18
|
+
});
|
|
19
|
+
server.once("listening", () => {
|
|
20
|
+
server.close((closeError) => {
|
|
21
|
+
if (closeError) {
|
|
22
|
+
reject(closeError);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
server.listen(this.port, "0.0.0.0");
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PortAvailabilityPreflightCheck } from "./PortAvailabilityPreflightCheck.js";
|
|
2
|
+
export class PostgresPortPreflightCheck {
|
|
3
|
+
delegate;
|
|
4
|
+
constructor(port = 5432) {
|
|
5
|
+
this.delegate = new PortAvailabilityPreflightCheck("Postgres", port);
|
|
6
|
+
}
|
|
7
|
+
run() {
|
|
8
|
+
return this.delegate.run();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PortAvailabilityPreflightCheck } from "./PortAvailabilityPreflightCheck.js";
|
|
2
|
+
export class WebPortPreflightCheck {
|
|
3
|
+
delegate;
|
|
4
|
+
constructor(port) {
|
|
5
|
+
this.delegate = new PortAvailabilityPreflightCheck("companyhelm-web", port);
|
|
6
|
+
}
|
|
7
|
+
run() {
|
|
8
|
+
return this.delegate.run();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResolvedServiceSources } from "../core/local/LocalRepoSourceResolver.js";
|
|
2
|
+
import type { CommandRunner } from "../core/process/CommandRunner.js";
|
|
3
|
+
import type { RuntimePorts, RuntimeState } from "../core/runtime/RuntimeState.js";
|
|
4
|
+
import type { ManagedServiceStatus } from "../core/status/StatusService.js";
|
|
5
|
+
interface StartupStatusSnapshot {
|
|
6
|
+
postgres: ManagedServiceStatus;
|
|
7
|
+
api: ManagedServiceStatus;
|
|
8
|
+
frontend: ManagedServiceStatus;
|
|
9
|
+
}
|
|
10
|
+
interface StartupPreflightOptions {
|
|
11
|
+
commandRunner: CommandRunner;
|
|
12
|
+
currentState: RuntimeState | null;
|
|
13
|
+
desiredSources: ResolvedServiceSources;
|
|
14
|
+
ports: RuntimePorts;
|
|
15
|
+
readStatus: () => Promise<StartupStatusSnapshot>;
|
|
16
|
+
}
|
|
17
|
+
export declare function runStartupPreflightChecks(options: StartupPreflightOptions): Promise<void>;
|
|
18
|
+
export {};
|