@companyhelm/cli 0.3.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.
@@ -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 process.env.COMPANYHELM_HOME || path.join(os.homedir(), ".companyhelm");
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(process.cwd());
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(root);
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.agentCliGrpc}`,
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) {
@@ -4,7 +4,7 @@ import { requireInteractiveTerminal, unwrapPromptResult } from "./interactive.js
4
4
  export async function confirmReset(input = process.stdin, output = process.stdout) {
5
5
  requireInteractiveTerminal(input, output, "reset requires confirmation from a TTY. Re-run with --yes to skip the prompt.");
6
6
  const confirmed = await clack.confirm({
7
- message: "This will remove CompanyHelm containers, Postgres data, local runtime state, generated .companyhelm/api/.env, and the CompanyHelm home config. Continue?",
7
+ message: "This will remove CompanyHelm containers, Postgres data, generated runtime files under ~/.companyhelm/cli/runtime, and the CLI workspace config under ~/.companyhelm/cli/config.yaml. Continue?",
8
8
  active: "Yes",
9
9
  inactive: "No",
10
10
  initialValue: false,
@@ -1,7 +1,7 @@
1
1
  import * as clack from "@clack/prompts";
2
- import { LocalConfigStore } from "../core/runtime/LocalConfigStore.js";
3
2
  import { MANAGED_IMAGE_SERVICES, requireManagedImageService } from "../core/runtime/ManagedImages.js";
4
3
  import { PublicImageTagRegistry } from "../core/runtime/PublicImageTagRegistry.js";
4
+ import { RepoConfigStore } from "../core/runtime/RepoConfigStore.js";
5
5
  import { requireInteractiveTerminal, unwrapPromptResult } from "./interactive.js";
6
6
  function parsePositiveInteger(value) {
7
7
  const parsed = Number.parseInt(value, 10);
@@ -59,7 +59,7 @@ export async function runSetImageVersion(options, dependencies = {}) {
59
59
  const input = dependencies.input ?? process.stdin;
60
60
  const output = dependencies.output ?? process.stdout;
61
61
  const registry = dependencies.registry ?? new PublicImageTagRegistry();
62
- const configStore = dependencies.configStore ?? new LocalConfigStore();
62
+ const configStore = dependencies.configStore ?? new RepoConfigStore();
63
63
  clack.intro("CompanyHelm image selection", { output });
64
64
  const selectedService = options.service
65
65
  ? requireManagedImageService(options.service)
@@ -78,7 +78,7 @@ export async function runSetImageVersion(options, dependencies = {}) {
78
78
  export function registerSetImageVersionCommand(program) {
79
79
  program
80
80
  .command("set-image-version")
81
- .description("Interactively choose an API or frontend image tag and store it in the CompanyHelm home config.")
81
+ .description("Interactively choose an API or frontend image tag and store it in the project config.")
82
82
  .option("-s, --service <service>", "Prefill the service to update (api or frontend)")
83
83
  .option("-l, --limit <count>", "How many image tags to show", parsePositiveInteger, 20)
84
84
  .action(async (options) => {
@@ -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 projectPaths;
4
- constructor(projectRoot?: string);
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 { ProjectPaths } from "../runtime/ProjectPaths.js";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { RuntimePaths } from "../runtime/RuntimePaths.js";
3
5
  function escapeEnvValue(value) {
4
6
  return String(value || "")
5
7
  .replace(/\\/g, "\\\\")
@@ -8,19 +10,24 @@ function escapeEnvValue(value) {
8
10
  .replace(/\n/g, "\\n");
9
11
  }
10
12
  export class ApiEnvFileWriter {
11
- projectPaths;
12
- constructor(projectRoot = process.cwd()) {
13
- this.projectPaths = new ProjectPaths(projectRoot);
13
+ runtimePaths;
14
+ templatePath;
15
+ constructor(root) {
16
+ this.runtimePaths = new RuntimePaths(root);
17
+ this.templatePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../templates/api.env.tpl");
14
18
  }
15
19
  write(config) {
16
- fs.mkdirSync(this.projectPaths.apiDirectoryPath(), { recursive: true });
17
- const contents = [
18
- `GITHUB_APP_URL=${escapeEnvValue(config?.appUrl ?? "")}`,
19
- `GITHUB_APP_CLIENT_ID=${escapeEnvValue(config?.appClientId ?? "")}`,
20
- `GITHUB_APP_PRIVATE_KEY_PEM=${escapeEnvValue(config?.appPrivateKeyPem ?? "")}`,
21
- "",
22
- ].join("\n");
23
- fs.writeFileSync(this.projectPaths.apiEnvPath(), contents, "utf8");
24
- return this.projectPaths.apiEnvPath();
20
+ fs.mkdirSync(this.runtimePaths.apiDirectoryPath(), { recursive: true });
21
+ const contents = this.render(config);
22
+ fs.writeFileSync(this.runtimePaths.apiEnvPath(), contents, "utf8");
23
+ return this.runtimePaths.apiEnvPath();
24
+ }
25
+ render(config) {
26
+ const template = fs.readFileSync(this.templatePath, "utf8");
27
+ const rendered = template
28
+ .replace("{{GITHUB_APP_URL}}", escapeEnvValue(config?.appUrl ?? ""))
29
+ .replace("{{GITHUB_APP_CLIENT_ID}}", escapeEnvValue(config?.appClientId ?? ""))
30
+ .replace("{{GITHUB_APP_PRIVATE_KEY_PEM}}", escapeEnvValue(config?.appPrivateKeyPem ?? ""));
31
+ return rendered.endsWith("\n") ? rendered : `${rendered}\n`;
25
32
  }
26
33
  }
@@ -1,18 +1,10 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import { parse, stringify } from "yaml";
5
4
  import { normalizeGithubAppConfig } from "./GithubAppConfig.js";
5
+ import { defaultCliConfigRoot } from "../runtime/CliRoot.js";
6
6
  function defaultConfigRoot() {
7
- const explicitRoot = String(process.env.COMPANYHELM_CONFIG_HOME || "").trim();
8
- if (explicitRoot) {
9
- return path.resolve(explicitRoot);
10
- }
11
- const xdgRoot = String(process.env.XDG_CONFIG_HOME || "").trim();
12
- if (xdgRoot) {
13
- return path.resolve(xdgRoot, "companyhelm");
14
- }
15
- return path.join(os.homedir(), ".config", "companyhelm");
7
+ return defaultCliConfigRoot();
16
8
  }
17
9
  export class GithubAppConfigStore {
18
10
  configRoot;
@@ -29,7 +29,6 @@ export class ComposeTemplateRenderer {
29
29
  " ports:",
30
30
  ` - "${ports.apiHttpPort}:4000"`,
31
31
  ` - "${ports.runnerGrpcPort}:${ports.runnerGrpcPort}"`,
32
- ` - "${ports.agentCliGrpcPort}:${ports.agentCliGrpcPort}"`,
33
32
  " volumes:",
34
33
  ` - "${paths.apiConfigPath}:/run/companyhelm/config.yaml:ro"`,
35
34
  " networks:",
@@ -1,6 +1,5 @@
1
1
  import fs from "node:fs";
2
2
  import { CommandRunner } from "../process/CommandRunner.js";
3
- import { ProjectPaths } from "../runtime/ProjectPaths.js";
4
3
  import { RuntimePaths } from "../runtime/RuntimePaths.js";
5
4
  import { ComposeTemplateRenderer } from "./ComposeTemplateRenderer.js";
6
5
  export class DockerStackManager {
@@ -23,7 +22,7 @@ export class DockerStackManager {
23
22
  agentCliGrpcPort: state.ports.agentCliGrpc
24
23
  }, {
25
24
  apiConfigPath: this.runtimePaths.apiConfigPath(),
26
- apiEnvPath: new ProjectPaths(process.cwd()).apiEnvPath(),
25
+ apiEnvPath: this.runtimePaths.apiEnvPath(),
27
26
  frontendConfigPath: this.runtimePaths.frontendConfigPath(),
28
27
  seedFilePath: this.runtimePaths.seedFilePath()
29
28
  }, {
@@ -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,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 process.env.COMPANYHELM_HOME || path.join(os.homedir(), ".companyhelm");
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 { images: {} };
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
  }
@@ -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,3 @@
1
+ GITHUB_APP_URL={{GITHUB_APP_URL}}
2
+ GITHUB_APP_CLIENT_ID={{GITHUB_APP_CLIENT_ID}}
3
+ GITHUB_APP_PRIVATE_KEY_PEM={{GITHUB_APP_PRIVATE_KEY_PEM}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companyhelm/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Bootstrap and manage a local CompanyHelm deployment.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.1.0",
30
- "@companyhelm/runner": "^0.1.3",
30
+ "@companyhelm/runner": "^0.2.0",
31
31
  "chalk": "^5.6.2",
32
32
  "commander": "^14.0.1",
33
33
  "dockerode": "^4.0.9",
@@ -0,0 +1,3 @@
1
+ GITHUB_APP_URL={{GITHUB_APP_URL}}
2
+ GITHUB_APP_CLIENT_ID={{GITHUB_APP_CLIENT_ID}}
3
+ GITHUB_APP_PRIVATE_KEY_PEM={{GITHUB_APP_PRIVATE_KEY_PEM}}
@@ -1,7 +0,0 @@
1
- export declare class ProjectPaths {
2
- private readonly root;
3
- constructor(root?: string);
4
- companyhelmRootPath(): string;
5
- apiDirectoryPath(): string;
6
- apiEnvPath(): string;
7
- }
@@ -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
- }