@companyhelm/cli 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -33
  3. package/dist/cli.js +11 -1
  4. package/dist/commands/dependencies.d.ts +22 -3
  5. package/dist/commands/dependencies.js +218 -23
  6. package/dist/commands/interactive.d.ts +6 -0
  7. package/dist/commands/interactive.js +22 -0
  8. package/dist/commands/logs.js +6 -1
  9. package/dist/commands/register-commands.js +4 -0
  10. package/dist/commands/reset.d.ts +4 -0
  11. package/dist/commands/reset.js +43 -4
  12. package/dist/commands/set-image-version.d.ts +31 -0
  13. package/dist/commands/set-image-version.js +87 -0
  14. package/dist/commands/setup-github-app.d.ts +10 -0
  15. package/dist/commands/setup-github-app.js +211 -0
  16. package/dist/commands/status.js +3 -1
  17. package/dist/commands/up.js +36 -2
  18. package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +7 -2
  19. package/dist/core/bootstrap/DeploymentBootstrapper.js +13 -11
  20. package/dist/core/bootstrap/SeedSqlRenderer.js +23 -5
  21. package/dist/core/config/ApiEnvFileWriter.d.ts +6 -0
  22. package/dist/core/config/ApiEnvFileWriter.js +26 -0
  23. package/dist/core/config/GithubAppConfig.d.ts +6 -0
  24. package/dist/core/config/GithubAppConfig.js +26 -0
  25. package/dist/core/config/GithubAppConfigStore.d.ts +11 -0
  26. package/dist/core/config/GithubAppConfigStore.js +65 -0
  27. package/dist/core/docker/ComposeTemplateRenderer.d.ts +9 -1
  28. package/dist/core/docker/ComposeTemplateRenderer.js +48 -5
  29. package/dist/core/docker/DockerStackManager.d.ts +18 -3
  30. package/dist/core/docker/DockerStackManager.js +70 -8
  31. package/dist/core/local/ApiLocalService.d.ts +22 -0
  32. package/dist/core/local/ApiLocalService.js +65 -0
  33. package/dist/core/local/LocalRepoSourceResolver.d.ts +24 -0
  34. package/dist/core/local/LocalRepoSourceResolver.js +33 -0
  35. package/dist/core/local/LocalServiceProcessManager.d.ts +18 -0
  36. package/dist/core/local/LocalServiceProcessManager.js +83 -0
  37. package/dist/core/local/WebLocalService.d.ts +23 -0
  38. package/dist/core/local/WebLocalService.js +101 -0
  39. package/dist/core/process/CommandRunner.d.ts +2 -2
  40. package/dist/core/process/CommandRunner.js +10 -2
  41. package/dist/core/runner/RunnerSupervisor.d.ts +6 -0
  42. package/dist/core/runner/RunnerSupervisor.js +31 -3
  43. package/dist/core/runtime/ImageCatalog.js +5 -2
  44. package/dist/core/runtime/LocalConfigStore.d.ts +16 -0
  45. package/dist/core/runtime/LocalConfigStore.js +59 -0
  46. package/dist/core/runtime/ManagedImages.d.ts +10 -0
  47. package/dist/core/runtime/ManagedImages.js +27 -0
  48. package/dist/core/runtime/ProjectPaths.d.ts +7 -0
  49. package/dist/core/runtime/ProjectPaths.js +16 -0
  50. package/dist/core/runtime/PublicImageTagRegistry.d.ts +16 -0
  51. package/dist/core/runtime/PublicImageTagRegistry.js +148 -0
  52. package/dist/core/runtime/RuntimePaths.d.ts +2 -0
  53. package/dist/core/runtime/RuntimePaths.js +7 -1
  54. package/dist/core/runtime/RuntimeState.d.ts +15 -1
  55. package/dist/core/runtime/RuntimeStateStore.d.ts +2 -0
  56. package/dist/core/runtime/RuntimeStateStore.js +33 -4
  57. package/dist/core/runtime/VersionCatalog.d.ts +10 -0
  58. package/dist/core/runtime/VersionCatalog.js +21 -0
  59. package/dist/core/status/StatusService.d.ts +8 -1
  60. package/dist/core/status/StatusService.js +16 -4
  61. package/dist/core/ui/TerminalRenderer.d.ts +10 -0
  62. package/dist/core/ui/TerminalRenderer.js +48 -0
  63. package/dist/templates/docker-compose.yaml.tpl +3 -27
  64. package/dist/templates/seed.sql.tpl +32 -13
  65. package/package.json +7 -3
  66. package/src/templates/docker-compose.yaml.tpl +3 -27
  67. package/src/templates/seed.sql.tpl +32 -13
@@ -1,20 +1,31 @@
1
1
  import path from "node:path";
2
2
  import { createRequire } from "node:module";
3
3
  const require = createRequire(import.meta.url);
4
+ const DEFAULT_HOST_DOCKER_PATH = "unix:///var/run/docker.sock";
4
5
  export class RunnerSupervisor {
5
6
  configPath;
6
7
  constructor(configPath) {
7
8
  this.configPath = configPath;
8
9
  }
10
+ buildUseHostAuthArgs() {
11
+ const runnerCliPath = this.resolveRunnerCliPath();
12
+ return {
13
+ command: process.execPath,
14
+ args: [runnerCliPath, "--config-path", this.configPath, "sdk", "codex", "use-host-auth"]
15
+ };
16
+ }
9
17
  buildStartArgs(input) {
10
18
  const runnerCliPath = this.resolveRunnerCliPath();
19
+ const logLevel = (input.logLevel ?? "info").toUpperCase();
20
+ const hostDockerArgs = input.useHostDockerRuntime
21
+ ? ["--use-host-docker-runtime", "--host-docker-path", this.resolveHostDockerPath()]
22
+ : [];
11
23
  return {
12
24
  command: process.execPath,
13
25
  args: [
14
26
  runnerCliPath,
15
27
  "--config-path",
16
28
  this.configPath,
17
- "runner",
18
29
  "start",
19
30
  "--daemon",
20
31
  "--server-url",
@@ -23,8 +34,11 @@ export class RunnerSupervisor {
23
34
  input.agentApiUrl,
24
35
  "--log-path",
25
36
  input.logPath,
37
+ ...hostDockerArgs,
26
38
  "--secret",
27
- input.secret
39
+ input.secret,
40
+ "--log-level",
41
+ logLevel
28
42
  ]
29
43
  };
30
44
  }
@@ -32,7 +46,14 @@ export class RunnerSupervisor {
32
46
  const runnerCliPath = this.resolveRunnerCliPath();
33
47
  return {
34
48
  command: process.execPath,
35
- args: [runnerCliPath, "--config-path", this.configPath, "runner", "stop"]
49
+ args: [runnerCliPath, "--config-path", this.configPath, "stop"]
50
+ };
51
+ }
52
+ buildStatusArgs() {
53
+ const runnerCliPath = this.resolveRunnerCliPath();
54
+ return {
55
+ command: process.execPath,
56
+ args: [runnerCliPath, "--config-path", this.configPath, "status"]
36
57
  };
37
58
  }
38
59
  resolveRunnerCliPath() {
@@ -42,4 +63,11 @@ export class RunnerSupervisor {
42
63
  const packageJsonPath = require.resolve("@companyhelm/runner/package.json");
43
64
  return path.resolve(path.dirname(packageJsonPath), "dist/cli.js");
44
65
  }
66
+ resolveHostDockerPath() {
67
+ const dockerHost = String(process.env.DOCKER_HOST || "").trim();
68
+ if (dockerHost) {
69
+ return dockerHost;
70
+ }
71
+ return DEFAULT_HOST_DOCKER_PATH;
72
+ }
45
73
  }
@@ -1,8 +1,11 @@
1
+ import { LocalConfigStore } from "./LocalConfigStore.js";
2
+ import { defaultManagedImageReference } from "./ManagedImages.js";
1
3
  export class ImageCatalog {
2
4
  resolve() {
5
+ const configuredImages = new LocalConfigStore().load().images;
3
6
  return {
4
- api: process.env.COMPANYHELM_API_IMAGE || "companyhelm-api:latest",
5
- frontend: process.env.COMPANYHELM_WEB_IMAGE || "companyhelm-web:latest",
7
+ api: configuredImages.api || process.env.COMPANYHELM_API_IMAGE || defaultManagedImageReference("api"),
8
+ frontend: configuredImages.frontend || process.env.COMPANYHELM_WEB_IMAGE || defaultManagedImageReference("frontend"),
6
9
  postgres: process.env.COMPANYHELM_POSTGRES_IMAGE || "postgres:16-alpine"
7
10
  };
8
11
  }
@@ -0,0 +1,16 @@
1
+ import type { ManagedImageService } from "./ManagedImages.js";
2
+ export interface LocalConfig {
3
+ images: Partial<Record<ManagedImageService, string>>;
4
+ }
5
+ export declare class LocalConfigStore {
6
+ private readonly root;
7
+ constructor(root?: string);
8
+ configPath(): string;
9
+ load(): LocalConfig;
10
+ setImage(service: ManagedImageService, image: string): {
11
+ configPath: string;
12
+ image: string;
13
+ };
14
+ save(config: LocalConfig): void;
15
+ private parse;
16
+ }
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export class LocalConfigStore {
4
+ root;
5
+ constructor(root = process.cwd()) {
6
+ this.root = root;
7
+ }
8
+ configPath() {
9
+ return path.join(this.root, "config.yaml");
10
+ }
11
+ load() {
12
+ const configPath = this.configPath();
13
+ if (!fs.existsSync(configPath)) {
14
+ return { images: {} };
15
+ }
16
+ return this.parse(fs.readFileSync(configPath, "utf8"));
17
+ }
18
+ setImage(service, image) {
19
+ const nextConfig = this.load();
20
+ nextConfig.images[service] = image;
21
+ this.save(nextConfig);
22
+ return { configPath: this.configPath(), image };
23
+ }
24
+ 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}`);
31
+ }
32
+ fs.writeFileSync(this.configPath(), `${lines.join("\n")}\n`, "utf8");
33
+ }
34
+ parse(content) {
35
+ const images = {};
36
+ let inImagesSection = false;
37
+ for (const rawLine of content.split(/\r?\n/)) {
38
+ const line = rawLine.trimEnd();
39
+ if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
40
+ continue;
41
+ }
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];
55
+ }
56
+ }
57
+ return { images };
58
+ }
59
+ }
@@ -0,0 +1,10 @@
1
+ export declare const MANAGED_IMAGE_SERVICES: readonly ["api", "frontend"];
2
+ export type ManagedImageService = (typeof MANAGED_IMAGE_SERVICES)[number];
3
+ export interface ManagedImageDefinition {
4
+ imageUri: string;
5
+ repositoryPath: string;
6
+ }
7
+ export declare function requireManagedImageService(value: string): ManagedImageService;
8
+ export declare function getManagedImageDefinition(service: ManagedImageService): ManagedImageDefinition;
9
+ export declare function buildManagedImageReference(service: ManagedImageService, tag: string): string;
10
+ export declare function defaultManagedImageReference(service: ManagedImageService): string;
@@ -0,0 +1,27 @@
1
+ export const MANAGED_IMAGE_SERVICES = ["api", "frontend"];
2
+ const MANAGED_IMAGE_DEFINITIONS = {
3
+ api: {
4
+ imageUri: "public.ecr.aws/x6n0f2k4/companyhelm-api",
5
+ repositoryPath: "x6n0f2k4/companyhelm-api"
6
+ },
7
+ frontend: {
8
+ imageUri: "public.ecr.aws/x6n0f2k4/companyhelm-web",
9
+ repositoryPath: "x6n0f2k4/companyhelm-web"
10
+ }
11
+ };
12
+ export function requireManagedImageService(value) {
13
+ const normalized = value.trim().toLowerCase();
14
+ if (normalized === "api" || normalized === "frontend") {
15
+ return normalized;
16
+ }
17
+ throw new Error(`Unsupported image service "${value}". Expected one of: api, frontend.`);
18
+ }
19
+ export function getManagedImageDefinition(service) {
20
+ return MANAGED_IMAGE_DEFINITIONS[service];
21
+ }
22
+ export function buildManagedImageReference(service, tag) {
23
+ return `${MANAGED_IMAGE_DEFINITIONS[service].imageUri}:${tag}`;
24
+ }
25
+ export function defaultManagedImageReference(service) {
26
+ return buildManagedImageReference(service, "latest");
27
+ }
@@ -0,0 +1,7 @@
1
+ export declare class ProjectPaths {
2
+ private readonly root;
3
+ constructor(root?: string);
4
+ companyhelmRootPath(): string;
5
+ apiDirectoryPath(): string;
6
+ apiEnvPath(): string;
7
+ }
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,16 @@
1
+ import { type ManagedImageService } from "./ManagedImages.js";
2
+ interface PublicRegistryImageTag {
3
+ tag: string;
4
+ createdAt?: string;
5
+ }
6
+ export declare class PublicImageTagRegistry {
7
+ listAvailableTags(service: ManagedImageService, limit?: number): Promise<PublicRegistryImageTag[]>;
8
+ buildImageReference(service: ManagedImageService, tag: string): string;
9
+ private fetchToken;
10
+ private fetchCreatedAt;
11
+ private selectManifestDigest;
12
+ private fetchJson;
13
+ private parseRetryAfterMs;
14
+ private sleep;
15
+ }
16
+ export {};
@@ -0,0 +1,148 @@
1
+ import { buildManagedImageReference, getManagedImageDefinition } from "./ManagedImages.js";
2
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
3
+ const MAX_FETCH_ATTEMPTS = 3;
4
+ const DEFAULT_RETRY_DELAY_MS = 250;
5
+ class PublicRegistryRequestError extends Error {
6
+ status;
7
+ retryAfterMs;
8
+ constructor(url, status, retryAfterMs) {
9
+ super(`Public registry returned ${status} for ${url}.`);
10
+ this.status = status;
11
+ this.retryAfterMs = retryAfterMs;
12
+ }
13
+ }
14
+ export class PublicImageTagRegistry {
15
+ async listAvailableTags(service, limit = 20) {
16
+ if (!Number.isInteger(limit) || limit < 1) {
17
+ throw new Error("Image tag limit must be a positive integer.");
18
+ }
19
+ const { repositoryPath } = getManagedImageDefinition(service);
20
+ const token = await this.fetchToken(repositoryPath);
21
+ const response = await fetch(`https://public.ecr.aws/v2/${repositoryPath}/tags/list`, {
22
+ headers: {
23
+ Authorization: `Bearer ${token}`
24
+ }
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`Public registry returned ${response.status} for ${service} tags.`);
28
+ }
29
+ const payload = (await response.json());
30
+ if (payload.errors?.length) {
31
+ throw new Error(payload.errors[0]?.message || `Unable to load ${service} image tags.`);
32
+ }
33
+ const uniqueTags = [...new Set(payload.tags ?? [])].map((tag, position) => ({ tag, position }));
34
+ 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
+ }
43
+ return tagsWithMetadata
44
+ .sort((left, right) => {
45
+ const leftTimestamp = left.createdAt ? Date.parse(left.createdAt) : Number.NEGATIVE_INFINITY;
46
+ const rightTimestamp = right.createdAt ? Date.parse(right.createdAt) : Number.NEGATIVE_INFINITY;
47
+ if (rightTimestamp !== leftTimestamp) {
48
+ return rightTimestamp - leftTimestamp;
49
+ }
50
+ return left.position - right.position;
51
+ })
52
+ .slice(0, limit)
53
+ .map(({ tag, createdAt }) => ({ tag, createdAt }));
54
+ }
55
+ buildImageReference(service, tag) {
56
+ return buildManagedImageReference(service, tag);
57
+ }
58
+ async fetchToken(repositoryPath) {
59
+ const scope = `repository:${repositoryPath}:pull`;
60
+ const response = await fetch(`https://public.ecr.aws/token/?service=public.ecr.aws&scope=${encodeURIComponent(scope)}`);
61
+ if (!response.ok) {
62
+ throw new Error(`Public registry token request returned ${response.status}.`);
63
+ }
64
+ const payload = (await response.json());
65
+ if (!payload.token) {
66
+ throw new Error("Public registry token response did not include a token.");
67
+ }
68
+ return payload.token;
69
+ }
70
+ async fetchCreatedAt(repositoryPath, token, tag, createdAtByDigest) {
71
+ try {
72
+ const manifestReference = await this.fetchJson(`https://public.ecr.aws/v2/${repositoryPath}/manifests/${encodeURIComponent(tag)}`, {
73
+ Authorization: `Bearer ${token}`,
74
+ Accept: [
75
+ "application/vnd.oci.image.index.v1+json",
76
+ "application/vnd.docker.distribution.manifest.list.v2+json",
77
+ "application/vnd.oci.image.manifest.v1+json",
78
+ "application/vnd.docker.distribution.manifest.v2+json"
79
+ ].join(", ")
80
+ });
81
+ const digest = this.selectManifestDigest(manifestReference);
82
+ const manifest = digest
83
+ ? await this.fetchJson(`https://public.ecr.aws/v2/${repositoryPath}/manifests/${encodeURIComponent(digest)}`, {
84
+ Authorization: `Bearer ${token}`,
85
+ Accept: ["application/vnd.oci.image.manifest.v1+json", "application/vnd.docker.distribution.manifest.v2+json"].join(", ")
86
+ })
87
+ : manifestReference;
88
+ const configDigest = "config" in manifest ? manifest.config?.digest : undefined;
89
+ if (!configDigest) {
90
+ return undefined;
91
+ }
92
+ if (createdAtByDigest.has(configDigest)) {
93
+ return createdAtByDigest.get(configDigest);
94
+ }
95
+ const config = await this.fetchJson(`https://public.ecr.aws/v2/${repositoryPath}/blobs/${configDigest}`, {
96
+ Authorization: `Bearer ${token}`
97
+ });
98
+ createdAtByDigest.set(configDigest, config.created);
99
+ return config.created;
100
+ }
101
+ catch (error) {
102
+ if (error instanceof PublicRegistryRequestError) {
103
+ return undefined;
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+ selectManifestDigest(manifest) {
109
+ if (!("manifests" in manifest) || !manifest.manifests?.length) {
110
+ return undefined;
111
+ }
112
+ return (manifest.manifests.find((entry) => entry.platform?.os === "linux" && entry.platform?.architecture === "amd64")
113
+ ?.digest ?? manifest.manifests[0]?.digest);
114
+ }
115
+ async fetchJson(url, headers) {
116
+ for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt += 1) {
117
+ const response = await fetch(url, { headers });
118
+ if (response.ok) {
119
+ return (await response.json());
120
+ }
121
+ const retryAfterMs = this.parseRetryAfterMs(response);
122
+ if (attempt < MAX_FETCH_ATTEMPTS && RETRYABLE_STATUS_CODES.has(response.status)) {
123
+ await this.sleep(retryAfterMs ?? DEFAULT_RETRY_DELAY_MS * attempt);
124
+ continue;
125
+ }
126
+ throw new PublicRegistryRequestError(url, response.status, retryAfterMs);
127
+ }
128
+ throw new PublicRegistryRequestError(url, 500);
129
+ }
130
+ parseRetryAfterMs(response) {
131
+ const retryAfter = response.headers.get("retry-after");
132
+ if (!retryAfter) {
133
+ return undefined;
134
+ }
135
+ const retryAfterSeconds = Number.parseInt(retryAfter, 10);
136
+ if (Number.isFinite(retryAfterSeconds)) {
137
+ return Math.max(retryAfterSeconds, 0) * 1000;
138
+ }
139
+ const retryAt = Date.parse(retryAfter);
140
+ if (Number.isNaN(retryAt)) {
141
+ return undefined;
142
+ }
143
+ return Math.max(retryAt - Date.now(), 0);
144
+ }
145
+ async sleep(delayMs) {
146
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
147
+ }
148
+ }
@@ -9,4 +9,6 @@ export declare class RuntimePaths {
9
9
  runnerConfigPath(): string;
10
10
  runnerStateDbPath(): string;
11
11
  runnerLogPath(): string;
12
+ serviceRuntimePath(): string;
13
+ serviceLogPath(service: "api" | "frontend"): string;
12
14
  }
@@ -5,7 +5,7 @@ export class RuntimePaths {
5
5
  this.root = root;
6
6
  }
7
7
  stateFilePath() {
8
- return path.join(this.root, "state.json");
8
+ return path.join(this.root, "state.yaml");
9
9
  }
10
10
  composeFilePath() {
11
11
  return path.join(this.root, "docker-compose.yaml");
@@ -28,4 +28,10 @@ export class RuntimePaths {
28
28
  runnerLogPath() {
29
29
  return path.join(this.runnerConfigPath(), "daemon.log");
30
30
  }
31
+ serviceRuntimePath() {
32
+ return path.join(this.root, "services");
33
+ }
34
+ serviceLogPath(service) {
35
+ return path.join(this.serviceRuntimePath(), `${service}.log`);
36
+ }
31
37
  }
@@ -4,6 +4,16 @@ export interface RuntimePorts {
4
4
  runnerGrpc: number;
5
5
  agentCliGrpc: number;
6
6
  }
7
+ export interface DockerManagedServiceRuntime {
8
+ source: "docker";
9
+ }
10
+ export interface LocalManagedServiceRuntime {
11
+ source: "local";
12
+ repoPath: string;
13
+ logPath: string;
14
+ pid: number;
15
+ }
16
+ export type ManagedServiceRuntime = DockerManagedServiceRuntime | LocalManagedServiceRuntime;
7
17
  export interface RuntimeState {
8
18
  version: 1;
9
19
  company: {
@@ -11,7 +21,7 @@ export interface RuntimeState {
11
21
  name: string;
12
22
  };
13
23
  auth: {
14
- username: "admin";
24
+ username: string;
15
25
  password: string;
16
26
  jwtPrivateKeyPem: string;
17
27
  jwtPublicKeyPem: string;
@@ -21,4 +31,8 @@ export interface RuntimeState {
21
31
  secret: string;
22
32
  };
23
33
  ports: RuntimePorts;
34
+ services: {
35
+ api: ManagedServiceRuntime;
36
+ frontend: ManagedServiceRuntime;
37
+ };
24
38
  }
@@ -2,8 +2,10 @@ import type { RuntimeState } from "./RuntimeState.js";
2
2
  export declare class RuntimeStateStore {
3
3
  private readonly root;
4
4
  private readonly runtimePaths;
5
+ private static readonly DEFAULT_USERNAME;
5
6
  constructor(root: string);
6
7
  initialize(): RuntimeState;
7
8
  load(): RuntimeState | null;
9
+ persist(state: RuntimeState): void;
8
10
  private save;
9
11
  }
@@ -1,10 +1,12 @@
1
1
  import fs from "node:fs";
2
+ import YAML from "yaml";
2
3
  import { PortAllocator } from "./PortAllocator.js";
3
4
  import { RuntimePaths } from "./RuntimePaths.js";
4
5
  import { createPemKeyPair, randomCompanyId, randomSecret } from "./Secrets.js";
5
6
  export class RuntimeStateStore {
6
7
  root;
7
8
  runtimePaths;
9
+ static DEFAULT_USERNAME = "admin@local";
8
10
  constructor(root) {
9
11
  this.root = root;
10
12
  this.runtimePaths = new RuntimePaths(root);
@@ -23,7 +25,7 @@ export class RuntimeStateStore {
23
25
  name: "Local CompanyHelm"
24
26
  },
25
27
  auth: {
26
- username: "admin",
28
+ username: RuntimeStateStore.DEFAULT_USERNAME,
27
29
  password: randomSecret(),
28
30
  jwtPrivateKeyPem: authKeys.privateKeyPem,
29
31
  jwtPublicKeyPem: authKeys.publicKeyPem
@@ -32,7 +34,15 @@ export class RuntimeStateStore {
32
34
  name: "local-runner",
33
35
  secret: randomSecret()
34
36
  },
35
- ports: new PortAllocator().allocate()
37
+ ports: new PortAllocator().allocate(),
38
+ services: {
39
+ api: {
40
+ source: "docker"
41
+ },
42
+ frontend: {
43
+ source: "docker"
44
+ }
45
+ }
36
46
  };
37
47
  this.save(state);
38
48
  return state;
@@ -41,10 +51,29 @@ export class RuntimeStateStore {
41
51
  if (!fs.existsSync(this.runtimePaths.stateFilePath())) {
42
52
  return null;
43
53
  }
44
- return JSON.parse(fs.readFileSync(this.runtimePaths.stateFilePath(), "utf8"));
54
+ const state = YAML.parse(fs.readFileSync(this.runtimePaths.stateFilePath(), "utf8"));
55
+ if (state.auth.username !== RuntimeStateStore.DEFAULT_USERNAME && state.auth.username === "admin") {
56
+ state.auth.username = RuntimeStateStore.DEFAULT_USERNAME;
57
+ this.save(state);
58
+ }
59
+ if (!state.services) {
60
+ state.services = {
61
+ api: {
62
+ source: "docker"
63
+ },
64
+ frontend: {
65
+ source: "docker"
66
+ }
67
+ };
68
+ this.save(state);
69
+ }
70
+ return state;
71
+ }
72
+ persist(state) {
73
+ this.save(state);
45
74
  }
46
75
  save(state) {
47
- fs.writeFileSync(this.runtimePaths.stateFilePath(), `${JSON.stringify(state, null, 2)}\n`, {
76
+ fs.writeFileSync(this.runtimePaths.stateFilePath(), YAML.stringify(state), {
48
77
  encoding: "utf8",
49
78
  mode: 0o600
50
79
  });
@@ -0,0 +1,10 @@
1
+ import { type RuntimeImages } from "./ImageCatalog.js";
2
+ export interface RuntimeVersions {
3
+ cliPackage: string;
4
+ runnerPackage: string;
5
+ images: RuntimeImages;
6
+ }
7
+ export declare class VersionCatalog {
8
+ resolve(): RuntimeVersions;
9
+ private readPackageVersion;
10
+ }
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ImageCatalog } from "./ImageCatalog.js";
6
+ const require = createRequire(import.meta.url);
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ export class VersionCatalog {
10
+ resolve() {
11
+ return {
12
+ cliPackage: this.readPackageVersion(path.resolve(__dirname, "../../../package.json")),
13
+ runnerPackage: this.readPackageVersion(require.resolve("@companyhelm/runner/package.json")),
14
+ images: new ImageCatalog().resolve()
15
+ };
16
+ }
17
+ readPackageVersion(packageJsonPath) {
18
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
19
+ return `${packageJson.name}@${packageJson.version}`;
20
+ }
21
+ }
@@ -5,8 +5,15 @@ export interface StatusSnapshot {
5
5
  frontend: ManagedServiceStatus;
6
6
  runner: ManagedServiceStatus;
7
7
  }
8
+ export interface StatusOverrides {
9
+ api?: () => Promise<boolean | undefined> | boolean | undefined;
10
+ frontend?: () => Promise<boolean | undefined> | boolean | undefined;
11
+ runner?: () => Promise<boolean | undefined> | boolean | undefined;
12
+ }
8
13
  export declare class StatusService {
9
14
  private readonly listRunningServices;
10
- constructor(listRunningServices: () => Promise<string>);
15
+ private readonly overrides;
16
+ constructor(listRunningServices: () => Promise<string>, overrides?: StatusOverrides);
11
17
  read(): Promise<StatusSnapshot>;
18
+ private resolveServiceRunning;
12
19
  }
@@ -1,18 +1,30 @@
1
1
  export class StatusService {
2
2
  listRunningServices;
3
- constructor(listRunningServices) {
3
+ overrides;
4
+ constructor(listRunningServices, overrides = {}) {
4
5
  this.listRunningServices = listRunningServices;
6
+ this.overrides = overrides;
5
7
  }
6
8
  async read() {
7
9
  const running = new Set((await this.listRunningServices())
8
10
  .split("\n")
9
11
  .map((service) => service.trim())
10
12
  .filter(Boolean));
13
+ const apiRunning = await this.resolveServiceRunning("api", this.overrides.api, running);
14
+ const frontendRunning = await this.resolveServiceRunning("frontend", this.overrides.frontend, running);
15
+ const runnerRunning = await this.resolveServiceRunning("runner", this.overrides.runner, running);
11
16
  return {
12
17
  postgres: running.has("postgres") ? "running" : "stopped",
13
- api: running.has("api") ? "running" : "stopped",
14
- frontend: running.has("frontend") ? "running" : "stopped",
15
- runner: running.has("runner") ? "running" : "stopped"
18
+ api: apiRunning ? "running" : "stopped",
19
+ frontend: frontendRunning ? "running" : "stopped",
20
+ runner: runnerRunning ? "running" : "stopped"
16
21
  };
17
22
  }
23
+ async resolveServiceRunning(service, override, running) {
24
+ if (!override) {
25
+ return running.has(service);
26
+ }
27
+ const result = await override();
28
+ return typeof result === "boolean" ? result : running.has(service);
29
+ }
18
30
  }
@@ -1,7 +1,17 @@
1
+ import type { StatusReport } from "../../commands/dependencies.js";
1
2
  export declare class TerminalRenderer {
2
3
  private readonly useColor;
4
+ private static readonly OSC;
5
+ private static readonly BEL;
3
6
  constructor(useColor?: boolean);
4
7
  renderBanner(): string;
5
8
  success(message: string): string;
9
+ progress(message: string): string;
10
+ successHighlight(message: string): string;
11
+ clickableUrl(url: string): string;
12
+ renderStatus(report: StatusReport): string;
13
+ private renderServiceLine;
14
+ private warn;
15
+ private formatDetail;
6
16
  private colorize;
7
17
  }