@companyhelm/cli 0.3.0 → 0.4.1
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/dist/commands/dependencies.js +5 -9
- package/dist/commands/reset.js +1 -1
- package/dist/commands/set-image-version.d.ts +2 -2
- package/dist/commands/set-image-version.js +3 -3
- package/dist/config/image_config.d.ts +4 -0
- package/dist/config/image_config.js +4 -0
- package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +0 -1
- package/dist/core/bootstrap/DeploymentBootstrapper.js +0 -5
- package/dist/core/config/ApiEnvFileWriter.d.ts +4 -2
- 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/runtime/CliRoot.d.ts +2 -0
- package/dist/core/runtime/CliRoot.js +23 -0
- package/dist/core/runtime/ImageCatalog.d.ts +3 -0
- package/dist/core/runtime/ImageCatalog.js +8 -4
- package/dist/core/runtime/ImageConfigStore.d.ts +15 -0
- package/dist/core/runtime/ImageConfigStore.js +51 -0
- package/dist/core/runtime/LocalConfigStore.d.ts +0 -6
- package/dist/core/runtime/LocalConfigStore.js +4 -37
- package/dist/core/runtime/PublicImageTagRegistry.d.ts +1 -0
- package/dist/core/runtime/PublicImageTagRegistry.js +34 -14
- package/dist/core/runtime/RuntimePaths.d.ts +2 -0
- package/dist/core/runtime/RuntimePaths.js +6 -0
- package/dist/templates/api.env.tpl +3 -0
- package/package.json +5 -4
- package/src/config/image_config.ts +4 -0
- 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,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,8 +9,8 @@ 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";
|
|
@@ -25,7 +23,7 @@ import { runStartupPreflightChecks } from "../preflight/runStartupPreflightCheck
|
|
|
25
23
|
import { ensureGithubAppConfig } from "./setup-github-app.js";
|
|
26
24
|
import { ensureAgentWorkspaceMode } from "./startup-preferences.js";
|
|
27
25
|
function runtimeRoot() {
|
|
28
|
-
return
|
|
26
|
+
return defaultCliRoot();
|
|
29
27
|
}
|
|
30
28
|
export function createDefaultDependencies() {
|
|
31
29
|
const root = runtimeRoot();
|
|
@@ -37,10 +35,9 @@ export function createDefaultDependencies() {
|
|
|
37
35
|
const runnerSupervisor = new RunnerSupervisor(runtimePaths.runnerConfigPath());
|
|
38
36
|
const bootstrapper = new DeploymentBootstrapper();
|
|
39
37
|
const githubAppConfigStore = new GithubAppConfigStore();
|
|
40
|
-
const apiEnvFileWriter = new ApiEnvFileWriter(
|
|
41
|
-
const projectPaths = new ProjectPaths(process.cwd());
|
|
38
|
+
const apiEnvFileWriter = new ApiEnvFileWriter(root);
|
|
42
39
|
const localRepoSourceResolver = new LocalRepoSourceResolver(process.cwd());
|
|
43
|
-
const localConfigStore = new LocalConfigStore(
|
|
40
|
+
const localConfigStore = new LocalConfigStore();
|
|
44
41
|
const localServiceProcessManager = new LocalServiceProcessManager();
|
|
45
42
|
const apiLocalService = new ApiLocalService(localServiceProcessManager, commandRunner);
|
|
46
43
|
const webLocalService = new WebLocalService(localServiceProcessManager, commandRunner);
|
|
@@ -157,7 +154,7 @@ export function createDefaultDependencies() {
|
|
|
157
154
|
await commandRunner.run(configureSdkCommand.command, configureSdkCommand.args);
|
|
158
155
|
const startCommand = runnerSupervisor.buildStartArgs({
|
|
159
156
|
serverUrl: `127.0.0.1:${state.ports.runnerGrpc}`,
|
|
160
|
-
agentApiUrl: `127.0.0.1:${state.ports.
|
|
157
|
+
agentApiUrl: `http://127.0.0.1:${state.ports.apiHttp}/agent/v1`,
|
|
161
158
|
logPath: runtimePaths.runnerLogPath(),
|
|
162
159
|
secret: state.runner.secret,
|
|
163
160
|
logLevel,
|
|
@@ -264,7 +261,6 @@ export function createDefaultDependencies() {
|
|
|
264
261
|
await stopLocalServicesFromState(state, localServiceProcessManager);
|
|
265
262
|
}
|
|
266
263
|
await dockerStackManager.down({ removeVolumes: true });
|
|
267
|
-
fs.rmSync(projectPaths.apiEnvPath(), { force: true });
|
|
268
264
|
fs.rmSync(localConfigStore.configPath(), { force: true });
|
|
269
265
|
fs.rmSync(root, { recursive: true, force: true });
|
|
270
266
|
if (options.removeGithubAppConfig) {
|
package/dist/commands/reset.js
CHANGED
|
@@ -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,
|
|
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,
|
|
@@ -13,7 +13,7 @@ export interface InteractiveImageSelector {
|
|
|
13
13
|
listAvailableTags(service: ManagedImageService, limit: number): Promise<AvailableImageTag[]>;
|
|
14
14
|
buildImageReference(service: ManagedImageService, tag: string): string;
|
|
15
15
|
}
|
|
16
|
-
export interface
|
|
16
|
+
export interface ImageVersionStore {
|
|
17
17
|
load(): {
|
|
18
18
|
images: Partial<Record<ManagedImageService, string>>;
|
|
19
19
|
};
|
|
@@ -26,6 +26,6 @@ export declare function runSetImageVersion(options: SetImageVersionOptions, depe
|
|
|
26
26
|
input?: Readable;
|
|
27
27
|
output?: Writable;
|
|
28
28
|
registry?: InteractiveImageSelector;
|
|
29
|
-
configStore?:
|
|
29
|
+
configStore?: ImageVersionStore;
|
|
30
30
|
}): Promise<void>;
|
|
31
31
|
export declare function registerSetImageVersionCommand(program: Command): void;
|
|
@@ -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 { ImageConfigStore } from "../core/runtime/ImageConfigStore.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
|
|
62
|
+
const configStore = dependencies.configStore ?? new ImageConfigStore();
|
|
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 the
|
|
81
|
+
.description("Interactively choose an API or frontend image tag and store it in the packaged image 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) => {
|
|
@@ -8,7 +8,6 @@ export declare class DeploymentBootstrapper {
|
|
|
8
8
|
databaseHost?: string;
|
|
9
9
|
appPort?: number;
|
|
10
10
|
runnerGrpcPort?: number;
|
|
11
|
-
agentGrpcPort?: number;
|
|
12
11
|
githubAppConfig?: GithubAppConfig | null;
|
|
13
12
|
}): string;
|
|
14
13
|
writeFrontendConfig(root: string, state: RuntimeState): string;
|
|
@@ -23,7 +23,6 @@ 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";
|
|
28
27
|
const githubConfigLines = options.githubAppConfig
|
|
29
28
|
? [
|
|
@@ -59,10 +58,6 @@ export class DeploymentBootstrapper {
|
|
|
59
58
|
" jitterSeconds: 60",
|
|
60
59
|
" batchSize: 10",
|
|
61
60
|
" leaseSeconds: 120",
|
|
62
|
-
"agent:",
|
|
63
|
-
" grpc:",
|
|
64
|
-
' host: "0.0.0.0"',
|
|
65
|
-
` port: ${agentGrpcPort}`,
|
|
66
61
|
"database:",
|
|
67
62
|
' name: "companyhelm"',
|
|
68
63
|
` host: "${databaseHost}"`,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { GithubAppConfig } from "./GithubAppConfig.js";
|
|
2
2
|
export declare class ApiEnvFileWriter {
|
|
3
|
-
private readonly
|
|
4
|
-
|
|
3
|
+
private readonly runtimePaths;
|
|
4
|
+
private readonly templatePath;
|
|
5
|
+
constructor(root: string);
|
|
5
6
|
write(config: GithubAppConfig | null): string;
|
|
7
|
+
private render;
|
|
6
8
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
17
|
-
const contents =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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:
|
|
25
|
+
apiEnvPath: this.runtimePaths.apiEnvPath(),
|
|
27
26
|
frontendConfigPath: this.runtimePaths.frontendConfigPath(),
|
|
28
27
|
seedFilePath: this.runtimePaths.seedFilePath()
|
|
29
28
|
}, {
|
|
@@ -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,11 @@
|
|
|
1
|
+
import { ImageConfigStore } from "./ImageConfigStore.js";
|
|
1
2
|
export interface RuntimeImages {
|
|
2
3
|
api: string;
|
|
3
4
|
frontend: string;
|
|
4
5
|
postgres: string;
|
|
5
6
|
}
|
|
6
7
|
export declare class ImageCatalog {
|
|
8
|
+
private readonly configStore;
|
|
9
|
+
constructor(configStore?: ImageConfigStore);
|
|
7
10
|
resolve(): RuntimeImages;
|
|
8
11
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { LocalConfigStore } from "./LocalConfigStore.js";
|
|
2
1
|
import { defaultManagedImageReference } from "./ManagedImages.js";
|
|
2
|
+
import { ImageConfigStore } from "./ImageConfigStore.js";
|
|
3
3
|
export class ImageCatalog {
|
|
4
|
+
configStore;
|
|
5
|
+
constructor(configStore = new ImageConfigStore()) {
|
|
6
|
+
this.configStore = configStore;
|
|
7
|
+
}
|
|
4
8
|
resolve() {
|
|
5
|
-
const configuredImages =
|
|
9
|
+
const configuredImages = this.configStore.load().images;
|
|
6
10
|
return {
|
|
7
|
-
api:
|
|
8
|
-
frontend:
|
|
11
|
+
api: process.env.COMPANYHELM_API_IMAGE || configuredImages.api || defaultManagedImageReference("api"),
|
|
12
|
+
frontend: process.env.COMPANYHELM_WEB_IMAGE || configuredImages.frontend || defaultManagedImageReference("frontend"),
|
|
9
13
|
postgres: process.env.COMPANYHELM_POSTGRES_IMAGE || "postgres:16-alpine"
|
|
10
14
|
};
|
|
11
15
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ManagedImageService } from "./ManagedImages.js";
|
|
2
|
+
export interface ImageConfig {
|
|
3
|
+
images: Partial<Record<ManagedImageService, string>>;
|
|
4
|
+
}
|
|
5
|
+
export declare class ImageConfigStore {
|
|
6
|
+
private readonly root;
|
|
7
|
+
constructor(root?: string);
|
|
8
|
+
configPath(): string;
|
|
9
|
+
load(): ImageConfig;
|
|
10
|
+
setImage(service: ManagedImageService, image: string): {
|
|
11
|
+
configPath: string;
|
|
12
|
+
image: string;
|
|
13
|
+
};
|
|
14
|
+
save(config: ImageConfig): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { MANAGED_IMAGE_SERVICES, defaultManagedImageReference } from "./ManagedImages.js";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
function defaultPackageRoot() {
|
|
8
|
+
return path.resolve(__dirname, "../../..");
|
|
9
|
+
}
|
|
10
|
+
export class ImageConfigStore {
|
|
11
|
+
root;
|
|
12
|
+
constructor(root = defaultPackageRoot()) {
|
|
13
|
+
this.root = root;
|
|
14
|
+
}
|
|
15
|
+
configPath() {
|
|
16
|
+
return path.join(this.root, "src", "config", "image_config.ts");
|
|
17
|
+
}
|
|
18
|
+
load() {
|
|
19
|
+
const configPath = this.configPath();
|
|
20
|
+
if (!fs.existsSync(configPath)) {
|
|
21
|
+
return { images: {} };
|
|
22
|
+
}
|
|
23
|
+
const images = {};
|
|
24
|
+
for (const rawLine of fs.readFileSync(configPath, "utf8").split(/\r?\n/)) {
|
|
25
|
+
const match = rawLine.match(/^\s+(api|frontend):\s+"([^"]+)",?$/);
|
|
26
|
+
if (match) {
|
|
27
|
+
images[match[1]] = match[2];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { images };
|
|
31
|
+
}
|
|
32
|
+
setImage(service, image) {
|
|
33
|
+
const nextConfig = this.load();
|
|
34
|
+
nextConfig.images[service] = image;
|
|
35
|
+
this.save(nextConfig);
|
|
36
|
+
return { configPath: this.configPath(), image };
|
|
37
|
+
}
|
|
38
|
+
save(config) {
|
|
39
|
+
const lines = [
|
|
40
|
+
"export const PACKAGED_IMAGE_CONFIG = {",
|
|
41
|
+
...MANAGED_IMAGE_SERVICES.map((service) => {
|
|
42
|
+
const image = config.images[service] ?? defaultManagedImageReference(service);
|
|
43
|
+
return ` ${service}: "${image}"`;
|
|
44
|
+
}).map((line, index, all) => `${line}${index < all.length - 1 ? "," : ""}`),
|
|
45
|
+
"} as const;",
|
|
46
|
+
""
|
|
47
|
+
];
|
|
48
|
+
fs.mkdirSync(path.dirname(this.configPath()), { recursive: true });
|
|
49
|
+
fs.writeFileSync(this.configPath(), lines.join("\n"), "utf8");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import type { ManagedImageService } from "./ManagedImages.js";
|
|
2
1
|
export type AgentWorkspaceMode = "dedicated" | "current-working-directory";
|
|
3
2
|
export interface LocalConfig {
|
|
4
3
|
agentWorkspaceMode?: AgentWorkspaceMode;
|
|
5
|
-
images: Partial<Record<ManagedImageService, string>>;
|
|
6
4
|
}
|
|
7
5
|
export declare class LocalConfigStore {
|
|
8
6
|
private readonly root;
|
|
9
7
|
constructor(root?: string);
|
|
10
8
|
configPath(): string;
|
|
11
9
|
load(): LocalConfig;
|
|
12
|
-
setImage(service: ManagedImageService, image: string): {
|
|
13
|
-
configPath: string;
|
|
14
|
-
image: string;
|
|
15
|
-
};
|
|
16
10
|
setAgentWorkspaceMode(agentWorkspaceMode: AgentWorkspaceMode): {
|
|
17
11
|
configPath: string;
|
|
18
12
|
agentWorkspaceMode: AgentWorkspaceMode;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { defaultCliConfigRoot } from "./CliRoot.js";
|
|
4
4
|
function defaultLocalConfigRoot() {
|
|
5
|
-
return
|
|
5
|
+
return defaultCliConfigRoot();
|
|
6
6
|
}
|
|
7
7
|
export class LocalConfigStore {
|
|
8
8
|
root;
|
|
@@ -15,16 +15,10 @@ export class LocalConfigStore {
|
|
|
15
15
|
load() {
|
|
16
16
|
const configPath = this.configPath();
|
|
17
17
|
if (!fs.existsSync(configPath)) {
|
|
18
|
-
return {
|
|
18
|
+
return {};
|
|
19
19
|
}
|
|
20
20
|
return this.parse(fs.readFileSync(configPath, "utf8"));
|
|
21
21
|
}
|
|
22
|
-
setImage(service, image) {
|
|
23
|
-
const nextConfig = this.load();
|
|
24
|
-
nextConfig.images[service] = image;
|
|
25
|
-
this.save(nextConfig);
|
|
26
|
-
return { configPath: this.configPath(), image };
|
|
27
|
-
}
|
|
28
22
|
setAgentWorkspaceMode(agentWorkspaceMode) {
|
|
29
23
|
const nextConfig = this.load();
|
|
30
24
|
nextConfig.agentWorkspaceMode = agentWorkspaceMode;
|
|
@@ -36,48 +30,21 @@ export class LocalConfigStore {
|
|
|
36
30
|
if (config.agentWorkspaceMode) {
|
|
37
31
|
lines.push(`agent_workspace_mode: ${config.agentWorkspaceMode}`);
|
|
38
32
|
}
|
|
39
|
-
lines.push("images:");
|
|
40
|
-
if (config.images.api) {
|
|
41
|
-
lines.push(` api: ${config.images.api}`);
|
|
42
|
-
}
|
|
43
|
-
if (config.images.frontend) {
|
|
44
|
-
lines.push(` frontend: ${config.images.frontend}`);
|
|
45
|
-
}
|
|
46
33
|
fs.mkdirSync(path.dirname(this.configPath()), { recursive: true });
|
|
47
34
|
fs.writeFileSync(this.configPath(), `${lines.join("\n")}\n`, "utf8");
|
|
48
35
|
}
|
|
49
36
|
parse(content) {
|
|
50
|
-
const images = {};
|
|
51
37
|
let agentWorkspaceMode;
|
|
52
|
-
let inImagesSection = false;
|
|
53
38
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
54
39
|
const line = rawLine.trimEnd();
|
|
55
40
|
if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
|
|
56
41
|
continue;
|
|
57
42
|
}
|
|
58
|
-
if (line === "images:") {
|
|
59
|
-
inImagesSection = true;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
43
|
const workspaceModeMatch = line.match(/^agent_workspace_mode:\s*(dedicated|current-working-directory)$/);
|
|
63
44
|
if (workspaceModeMatch) {
|
|
64
45
|
agentWorkspaceMode = workspaceModeMatch[1];
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (inImagesSection && /^[^\s]/.test(line)) {
|
|
68
|
-
inImagesSection = false;
|
|
69
|
-
}
|
|
70
|
-
if (!inImagesSection) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const match = line.match(/^ (api|frontend):\s*(.+)$/);
|
|
74
|
-
if (match) {
|
|
75
|
-
images[match[1]] = match[2];
|
|
76
46
|
}
|
|
77
47
|
}
|
|
78
|
-
return {
|
|
79
|
-
agentWorkspaceMode,
|
|
80
|
-
images
|
|
81
|
-
};
|
|
48
|
+
return { agentWorkspaceMode };
|
|
82
49
|
}
|
|
83
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
|
}
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@companyhelm/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Bootstrap and manage a local CompanyHelm deployment.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -17,17 +17,18 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
-
"src/templates"
|
|
20
|
+
"src/templates",
|
|
21
|
+
"src/config/image_config.ts"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
|
-
"build": "tsc -p tsconfig.json && node scripts/copy-templates.cjs",
|
|
24
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && node scripts/copy-templates.cjs",
|
|
24
25
|
"set-image-version": "npm run build && node dist/cli.js set-image-version",
|
|
25
26
|
"start": "npm run build && node dist/cli.js",
|
|
26
27
|
"test": "npm run build && vitest run"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"@clack/prompts": "^1.1.0",
|
|
30
|
-
"@companyhelm/runner": "^0.
|
|
31
|
+
"@companyhelm/runner": "^0.2.0",
|
|
31
32
|
"chalk": "^5.6.2",
|
|
32
33
|
"commander": "^14.0.1",
|
|
33
34
|
"dockerode": "^4.0.9",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
export class ProjectPaths {
|
|
3
|
-
root;
|
|
4
|
-
constructor(root = process.cwd()) {
|
|
5
|
-
this.root = root;
|
|
6
|
-
}
|
|
7
|
-
companyhelmRootPath() {
|
|
8
|
-
return path.join(this.root, ".companyhelm");
|
|
9
|
-
}
|
|
10
|
-
apiDirectoryPath() {
|
|
11
|
-
return path.join(this.companyhelmRootPath(), "api");
|
|
12
|
-
}
|
|
13
|
-
apiEnvPath() {
|
|
14
|
-
return path.join(this.apiDirectoryPath(), ".env");
|
|
15
|
-
}
|
|
16
|
-
}
|