@companyhelm/cli 0.1.2 → 0.1.4

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 (55) 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 +18 -3
  5. package/dist/commands/dependencies.js +76 -13
  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 +11 -2
  18. package/dist/core/bootstrap/DeploymentBootstrapper.d.ts +2 -2
  19. package/dist/core/bootstrap/DeploymentBootstrapper.js +5 -7
  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 +6 -1
  28. package/dist/core/docker/ComposeTemplateRenderer.js +22 -4
  29. package/dist/core/docker/DockerStackManager.d.ts +15 -3
  30. package/dist/core/docker/DockerStackManager.js +67 -8
  31. package/dist/core/runner/RunnerSupervisor.d.ts +4 -0
  32. package/dist/core/runner/RunnerSupervisor.js +19 -3
  33. package/dist/core/runtime/ImageCatalog.js +5 -2
  34. package/dist/core/runtime/LocalConfigStore.d.ts +16 -0
  35. package/dist/core/runtime/LocalConfigStore.js +59 -0
  36. package/dist/core/runtime/ManagedImages.d.ts +10 -0
  37. package/dist/core/runtime/ManagedImages.js +27 -0
  38. package/dist/core/runtime/ProjectPaths.d.ts +7 -0
  39. package/dist/core/runtime/ProjectPaths.js +16 -0
  40. package/dist/core/runtime/PublicImageTagRegistry.d.ts +16 -0
  41. package/dist/core/runtime/PublicImageTagRegistry.js +148 -0
  42. package/dist/core/runtime/RuntimeState.d.ts +1 -1
  43. package/dist/core/runtime/RuntimeStateStore.d.ts +1 -0
  44. package/dist/core/runtime/RuntimeStateStore.js +8 -2
  45. package/dist/core/runtime/VersionCatalog.d.ts +10 -0
  46. package/dist/core/runtime/VersionCatalog.js +21 -0
  47. package/dist/core/status/StatusService.d.ts +5 -1
  48. package/dist/core/status/StatusService.js +5 -2
  49. package/dist/core/ui/TerminalRenderer.d.ts +10 -0
  50. package/dist/core/ui/TerminalRenderer.js +48 -0
  51. package/dist/templates/docker-compose.yaml.tpl +4 -13
  52. package/dist/templates/seed.sql.tpl +32 -13
  53. package/package.json +7 -3
  54. package/src/templates/docker-compose.yaml.tpl +4 -13
  55. package/src/templates/seed.sql.tpl +32 -13
@@ -1,17 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import { CommandRunner } from "../process/CommandRunner.js";
3
+ import { ProjectPaths } from "../runtime/ProjectPaths.js";
3
4
  import { RuntimePaths } from "../runtime/RuntimePaths.js";
4
5
  import { ComposeTemplateRenderer } from "./ComposeTemplateRenderer.js";
5
6
  export class DockerStackManager {
6
7
  commandRunner;
7
8
  composeRenderer;
9
+ static BOOTSTRAP_RETRY_COUNT = 60;
10
+ static BOOTSTRAP_RETRY_DELAY_MS = 1000;
8
11
  runtimePaths;
9
12
  constructor(root, commandRunner = new CommandRunner(), composeRenderer = new ComposeTemplateRenderer()) {
10
13
  this.commandRunner = commandRunner;
11
14
  this.composeRenderer = composeRenderer;
12
15
  this.runtimePaths = new RuntimePaths(root);
13
16
  }
14
- async up(state) {
17
+ async up(state, options = {}) {
15
18
  fs.mkdirSync(this.runtimePaths.runnerConfigPath(), { recursive: true });
16
19
  fs.writeFileSync(this.runtimePaths.composeFilePath(), this.composeRenderer.render({
17
20
  apiHttpPort: state.ports.apiHttp,
@@ -20,8 +23,11 @@ export class DockerStackManager {
20
23
  agentCliGrpcPort: state.ports.agentCliGrpc
21
24
  }, {
22
25
  apiConfigPath: this.runtimePaths.apiConfigPath(),
26
+ apiEnvPath: new ProjectPaths(process.cwd()).apiEnvPath(),
23
27
  frontendConfigPath: this.runtimePaths.frontendConfigPath(),
24
28
  seedFilePath: this.runtimePaths.seedFilePath()
29
+ }, {
30
+ frontendLogLevel: options.frontendLogLevel
25
31
  }), "utf8");
26
32
  await this.commandRunner.run("docker", [
27
33
  "compose",
@@ -31,13 +37,20 @@ export class DockerStackManager {
31
37
  "-d"
32
38
  ]);
33
39
  }
34
- async applySeedSql() {
40
+ async applySeedSql(seedEmail) {
35
41
  if (!fs.existsSync(this.runtimePaths.seedFilePath())) {
36
42
  return;
37
43
  }
38
44
  let lastError = null;
39
- for (let attempt = 0; attempt < 20; attempt += 1) {
45
+ for (let attempt = 0; attempt < DockerStackManager.BOOTSTRAP_RETRY_COUNT; attempt += 1) {
40
46
  try {
47
+ if (!(await this.seedSchemaReady())) {
48
+ await this.waitForNextBootstrapAttempt();
49
+ continue;
50
+ }
51
+ if (await this.seedAlreadyApplied(seedEmail)) {
52
+ return;
53
+ }
41
54
  await this.commandRunner.run("docker", [
42
55
  "compose",
43
56
  "-f",
@@ -57,23 +70,69 @@ export class DockerStackManager {
57
70
  }
58
71
  catch (error) {
59
72
  lastError = error;
60
- await new Promise((resolve) => {
61
- setTimeout(resolve, 1000);
62
- });
73
+ await this.waitForNextBootstrapAttempt();
63
74
  }
64
75
  }
65
76
  throw lastError ?? new Error("Failed to apply seed SQL.");
66
77
  }
67
- async down() {
78
+ async seedSchemaReady() {
79
+ const output = await this.commandRunner.capture("docker", [
80
+ "compose",
81
+ "-f",
82
+ this.runtimePaths.composeFilePath(),
83
+ "exec",
84
+ "-T",
85
+ "postgres",
86
+ "psql",
87
+ "-U",
88
+ "postgres",
89
+ "-d",
90
+ "companyhelm",
91
+ "-tAc",
92
+ "SELECT to_regclass('public.user_auths') IS NOT NULL"
93
+ ]);
94
+ return output.trim() === "t";
95
+ }
96
+ async seedAlreadyApplied(seedEmail) {
97
+ const escapedEmail = seedEmail.replaceAll("'", "''");
98
+ const output = await this.commandRunner.capture("docker", [
99
+ "compose",
100
+ "-f",
101
+ this.runtimePaths.composeFilePath(),
102
+ "exec",
103
+ "-T",
104
+ "postgres",
105
+ "psql",
106
+ "-U",
107
+ "postgres",
108
+ "-d",
109
+ "companyhelm",
110
+ "-tAc",
111
+ `SELECT 1 FROM user_auths WHERE email = '${escapedEmail}' LIMIT 1`
112
+ ]);
113
+ return output.trim() === "1";
114
+ }
115
+ async waitForNextBootstrapAttempt() {
116
+ await new Promise((resolve) => {
117
+ setTimeout(resolve, DockerStackManager.BOOTSTRAP_RETRY_DELAY_MS);
118
+ });
119
+ }
120
+ async down(options = {}) {
68
121
  if (!fs.existsSync(this.runtimePaths.composeFilePath())) {
69
122
  return;
70
123
  }
71
- await this.commandRunner.run("docker", [
124
+ const args = [
72
125
  "compose",
73
126
  "-f",
74
127
  this.runtimePaths.composeFilePath(),
75
128
  "down",
76
129
  "--remove-orphans"
130
+ ];
131
+ if (options.removeVolumes) {
132
+ args.push("--volumes");
133
+ }
134
+ await this.commandRunner.run("docker", [
135
+ ...args
77
136
  ]);
78
137
  }
79
138
  async logs(service) {
@@ -1,8 +1,10 @@
1
+ import type { LogLevel } from "../../commands/dependencies.js";
1
2
  export interface RunnerStartInput {
2
3
  serverUrl: string;
3
4
  agentApiUrl: string;
4
5
  logPath: string;
5
6
  secret: string;
7
+ logLevel?: LogLevel;
6
8
  }
7
9
  export interface RunnerStartCommand {
8
10
  command: string;
@@ -11,7 +13,9 @@ export interface RunnerStartCommand {
11
13
  export declare class RunnerSupervisor {
12
14
  private readonly configPath;
13
15
  constructor(configPath: string);
16
+ buildUseHostAuthArgs(): RunnerStartCommand;
14
17
  buildStartArgs(input: RunnerStartInput): RunnerStartCommand;
15
18
  buildStopArgs(): RunnerStartCommand;
19
+ buildStatusArgs(): RunnerStartCommand;
16
20
  private resolveRunnerCliPath;
17
21
  }
@@ -6,15 +6,22 @@ export class RunnerSupervisor {
6
6
  constructor(configPath) {
7
7
  this.configPath = configPath;
8
8
  }
9
+ buildUseHostAuthArgs() {
10
+ const runnerCliPath = this.resolveRunnerCliPath();
11
+ return {
12
+ command: process.execPath,
13
+ args: [runnerCliPath, "--config-path", this.configPath, "sdk", "codex", "use-host-auth"]
14
+ };
15
+ }
9
16
  buildStartArgs(input) {
10
17
  const runnerCliPath = this.resolveRunnerCliPath();
18
+ const logLevel = (input.logLevel ?? "info").toUpperCase();
11
19
  return {
12
20
  command: process.execPath,
13
21
  args: [
14
22
  runnerCliPath,
15
23
  "--config-path",
16
24
  this.configPath,
17
- "runner",
18
25
  "start",
19
26
  "--daemon",
20
27
  "--server-url",
@@ -24,7 +31,9 @@ export class RunnerSupervisor {
24
31
  "--log-path",
25
32
  input.logPath,
26
33
  "--secret",
27
- input.secret
34
+ input.secret,
35
+ "--log-level",
36
+ logLevel
28
37
  ]
29
38
  };
30
39
  }
@@ -32,7 +41,14 @@ export class RunnerSupervisor {
32
41
  const runnerCliPath = this.resolveRunnerCliPath();
33
42
  return {
34
43
  command: process.execPath,
35
- args: [runnerCliPath, "--config-path", this.configPath, "runner", "stop"]
44
+ args: [runnerCliPath, "--config-path", this.configPath, "stop"]
45
+ };
46
+ }
47
+ buildStatusArgs() {
48
+ const runnerCliPath = this.resolveRunnerCliPath();
49
+ return {
50
+ command: process.execPath,
51
+ args: [runnerCliPath, "--config-path", this.configPath, "status"]
36
52
  };
37
53
  }
38
54
  resolveRunnerCliPath() {
@@ -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
+ }
@@ -11,7 +11,7 @@ export interface RuntimeState {
11
11
  name: string;
12
12
  };
13
13
  auth: {
14
- username: "admin";
14
+ username: string;
15
15
  password: string;
16
16
  jwtPrivateKeyPem: string;
17
17
  jwtPublicKeyPem: string;
@@ -2,6 +2,7 @@ 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;
@@ -5,6 +5,7 @@ import { createPemKeyPair, randomCompanyId, randomSecret } from "./Secrets.js";
5
5
  export class RuntimeStateStore {
6
6
  root;
7
7
  runtimePaths;
8
+ static DEFAULT_USERNAME = "admin@local";
8
9
  constructor(root) {
9
10
  this.root = root;
10
11
  this.runtimePaths = new RuntimePaths(root);
@@ -23,7 +24,7 @@ export class RuntimeStateStore {
23
24
  name: "Local CompanyHelm"
24
25
  },
25
26
  auth: {
26
- username: "admin",
27
+ username: RuntimeStateStore.DEFAULT_USERNAME,
27
28
  password: randomSecret(),
28
29
  jwtPrivateKeyPem: authKeys.privateKeyPem,
29
30
  jwtPublicKeyPem: authKeys.publicKeyPem
@@ -41,7 +42,12 @@ export class RuntimeStateStore {
41
42
  if (!fs.existsSync(this.runtimePaths.stateFilePath())) {
42
43
  return null;
43
44
  }
44
- return JSON.parse(fs.readFileSync(this.runtimePaths.stateFilePath(), "utf8"));
45
+ const state = JSON.parse(fs.readFileSync(this.runtimePaths.stateFilePath(), "utf8"));
46
+ if (state.auth.username !== RuntimeStateStore.DEFAULT_USERNAME && state.auth.username === "admin") {
47
+ state.auth.username = RuntimeStateStore.DEFAULT_USERNAME;
48
+ this.save(state);
49
+ }
50
+ return state;
45
51
  }
46
52
  save(state) {
47
53
  fs.writeFileSync(this.runtimePaths.stateFilePath(), `${JSON.stringify(state, null, 2)}\n`, {
@@ -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,12 @@ export interface StatusSnapshot {
5
5
  frontend: ManagedServiceStatus;
6
6
  runner: ManagedServiceStatus;
7
7
  }
8
+ export interface StatusOverrides {
9
+ runner?: () => Promise<boolean> | boolean;
10
+ }
8
11
  export declare class StatusService {
9
12
  private readonly listRunningServices;
10
- constructor(listRunningServices: () => Promise<string>);
13
+ private readonly overrides;
14
+ constructor(listRunningServices: () => Promise<string>, overrides?: StatusOverrides);
11
15
  read(): Promise<StatusSnapshot>;
12
16
  }
@@ -1,18 +1,21 @@
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 runnerRunning = this.overrides.runner ? await this.overrides.runner() : running.has("runner");
11
14
  return {
12
15
  postgres: running.has("postgres") ? "running" : "stopped",
13
16
  api: running.has("api") ? "running" : "stopped",
14
17
  frontend: running.has("frontend") ? "running" : "stopped",
15
- runner: running.has("runner") ? "running" : "stopped"
18
+ runner: runnerRunning ? "running" : "stopped"
16
19
  };
17
20
  }
18
21
  }
@@ -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
  }