@rulebricks/cli 1.9.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.
Files changed (93) hide show
  1. package/README.md +62 -0
  2. package/dist/commands/clone.d.ts +6 -0
  3. package/dist/commands/clone.js +60 -0
  4. package/dist/commands/deploy.d.ts +8 -0
  5. package/dist/commands/deploy.js +409 -0
  6. package/dist/commands/destroy.d.ts +8 -0
  7. package/dist/commands/destroy.js +298 -0
  8. package/dist/commands/init.d.ts +7 -0
  9. package/dist/commands/init.js +201 -0
  10. package/dist/commands/logs.d.ts +9 -0
  11. package/dist/commands/logs.js +222 -0
  12. package/dist/commands/open.d.ts +7 -0
  13. package/dist/commands/open.js +139 -0
  14. package/dist/commands/status.d.ts +5 -0
  15. package/dist/commands/status.js +125 -0
  16. package/dist/commands/upgrade.d.ts +7 -0
  17. package/dist/commands/upgrade.js +239 -0
  18. package/dist/components/DNSWaitScreen.d.ts +9 -0
  19. package/dist/components/DNSWaitScreen.js +73 -0
  20. package/dist/components/Wizard/WizardContext.d.ts +176 -0
  21. package/dist/components/Wizard/WizardContext.js +346 -0
  22. package/dist/components/Wizard/index.d.ts +2 -0
  23. package/dist/components/Wizard/index.js +2 -0
  24. package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
  25. package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
  26. package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
  27. package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
  28. package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
  29. package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
  30. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
  31. package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
  32. package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
  33. package/dist/components/Wizard/steps/DomainStep.js +126 -0
  34. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
  35. package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
  36. package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
  38. package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
  39. package/dist/components/Wizard/steps/ReviewStep.js +56 -0
  40. package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
  41. package/dist/components/Wizard/steps/SMTPStep.js +191 -0
  42. package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
  43. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
  44. package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
  45. package/dist/components/Wizard/steps/TierStep.js +29 -0
  46. package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
  47. package/dist/components/Wizard/steps/VersionStep.js +113 -0
  48. package/dist/components/Wizard/steps/index.d.ts +12 -0
  49. package/dist/components/Wizard/steps/index.js +12 -0
  50. package/dist/components/common/AppShell.d.ts +31 -0
  51. package/dist/components/common/AppShell.js +31 -0
  52. package/dist/components/common/Box.d.ts +20 -0
  53. package/dist/components/common/Box.js +20 -0
  54. package/dist/components/common/Logo.d.ts +7 -0
  55. package/dist/components/common/Logo.js +22 -0
  56. package/dist/components/common/Spinner.d.ts +12 -0
  57. package/dist/components/common/Spinner.js +28 -0
  58. package/dist/components/common/index.d.ts +6 -0
  59. package/dist/components/common/index.js +5 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +202 -0
  62. package/dist/lib/cloudCli.d.ts +156 -0
  63. package/dist/lib/cloudCli.js +691 -0
  64. package/dist/lib/config.d.ts +91 -0
  65. package/dist/lib/config.js +278 -0
  66. package/dist/lib/dns.d.ts +41 -0
  67. package/dist/lib/dns.js +235 -0
  68. package/dist/lib/dockerHub.d.ts +57 -0
  69. package/dist/lib/dockerHub.js +128 -0
  70. package/dist/lib/helm.d.ts +53 -0
  71. package/dist/lib/helm.js +209 -0
  72. package/dist/lib/helmValues.d.ts +17 -0
  73. package/dist/lib/helmValues.js +693 -0
  74. package/dist/lib/kubernetes.d.ts +161 -0
  75. package/dist/lib/kubernetes.js +755 -0
  76. package/dist/lib/terraform.d.ts +44 -0
  77. package/dist/lib/terraform.js +230 -0
  78. package/dist/lib/theme.d.ts +81 -0
  79. package/dist/lib/theme.js +115 -0
  80. package/dist/lib/validation.d.ts +47 -0
  81. package/dist/lib/validation.js +164 -0
  82. package/dist/lib/versions.d.ts +69 -0
  83. package/dist/lib/versions.js +139 -0
  84. package/dist/types/index.d.ts +718 -0
  85. package/dist/types/index.js +556 -0
  86. package/email-templates/email_change.html +325 -0
  87. package/email-templates/invite.html +383 -0
  88. package/email-templates/password_change.html +414 -0
  89. package/email-templates/verify.html +396 -0
  90. package/package.json +78 -0
  91. package/terraform/aws/main.tf +327 -0
  92. package/terraform/azure/main.tf +326 -0
  93. package/terraform/gcp/main.tf +369 -0
@@ -0,0 +1,91 @@
1
+ import { DeploymentConfig, DeploymentState, ProfileConfig } from "../types/index.js";
2
+ /**
3
+ * Ensures the base directories exist
4
+ */
5
+ export declare function ensureDirectories(): Promise<void>;
6
+ /**
7
+ * Gets the deployment directory path
8
+ */
9
+ export declare function getDeploymentDir(name: string): string;
10
+ /**
11
+ * Lists all deployments
12
+ */
13
+ export declare function listDeployments(): Promise<string[]>;
14
+ /**
15
+ * Checks if a deployment exists
16
+ */
17
+ export declare function deploymentExists(name: string): Promise<boolean>;
18
+ /**
19
+ * Saves a deployment configuration
20
+ */
21
+ export declare function saveDeploymentConfig(config: DeploymentConfig): Promise<void>;
22
+ /**
23
+ * Loads a deployment configuration
24
+ */
25
+ export declare function loadDeploymentConfig(name: string): Promise<DeploymentConfig>;
26
+ /**
27
+ * Clones a deployment configuration to a new name
28
+ * Only copies config.yaml with the new name - state and terraform are not copied
29
+ */
30
+ export declare function cloneDeploymentConfig(sourceName: string, targetName: string): Promise<DeploymentConfig>;
31
+ /**
32
+ * Saves the deployment state
33
+ */
34
+ export declare function saveDeploymentState(name: string, state: DeploymentState): Promise<void>;
35
+ /**
36
+ * Loads the deployment state
37
+ */
38
+ export declare function loadDeploymentState(name: string): Promise<DeploymentState | null>;
39
+ /**
40
+ * Saves the generated Helm values
41
+ */
42
+ export declare function saveHelmValues(name: string, values: Record<string, unknown>): Promise<string>;
43
+ /**
44
+ * Loads the Helm values
45
+ */
46
+ export declare function loadHelmValues(name: string): Promise<Record<string, unknown> | null>;
47
+ /**
48
+ * Gets the Helm values file path
49
+ */
50
+ export declare function getHelmValuesPath(name: string): string;
51
+ /**
52
+ * Saves Terraform variables
53
+ */
54
+ export declare function saveTerraformVars(name: string, vars: Record<string, unknown>): Promise<string>;
55
+ /**
56
+ * Gets the Terraform working directory
57
+ */
58
+ export declare function getTerraformDir(name: string): string;
59
+ /**
60
+ * Deletes a deployment and all its files
61
+ */
62
+ export declare function deleteDeployment(name: string): Promise<void>;
63
+ /**
64
+ * Updates the deployment state status
65
+ */
66
+ export declare function updateDeploymentStatus(name: string, status: DeploymentState["status"], updates?: Partial<DeploymentState>): Promise<void>;
67
+ /**
68
+ * Extracts the domain suffix from a full domain.
69
+ * e.g., "app.example.com" -> ".example.com"
70
+ * e.g., "sub.app.example.com" -> ".app.example.com"
71
+ */
72
+ export declare function extractDomainSuffix(domain: string): string | undefined;
73
+ /**
74
+ * Loads the user profile from ~/.rulebricks/profile.yaml
75
+ * Returns null if the profile doesn't exist or is invalid
76
+ */
77
+ export declare function loadProfile(): Promise<ProfileConfig | null>;
78
+ /**
79
+ * Saves the user profile to ~/.rulebricks/profile.yaml
80
+ */
81
+ export declare function saveProfile(profile: ProfileConfig): Promise<void>;
82
+ /**
83
+ * Extracts profile-worthy values from a deployment configuration.
84
+ * These are values that are likely to be reused across deployments.
85
+ */
86
+ export declare function extractProfileFromConfig(config: DeploymentConfig): ProfileConfig;
87
+ /**
88
+ * Merges a new profile with an existing one, preferring new non-undefined values.
89
+ * This allows incremental updates without losing existing preferences.
90
+ */
91
+ export declare function updateProfile(newValues: ProfileConfig): Promise<void>;
@@ -0,0 +1,278 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import yaml from "yaml";
5
+ import { DeploymentConfigSchema, ProfileConfigSchema, } from "../types/index.js";
6
+ const RULEBRICKS_DIR = path.join(os.homedir(), ".rulebricks");
7
+ const DEPLOYMENTS_DIR = path.join(RULEBRICKS_DIR, "deployments");
8
+ const PROFILE_FILE = "profile.yaml";
9
+ /**
10
+ * Ensures the base directories exist
11
+ */
12
+ export async function ensureDirectories() {
13
+ await fs.mkdir(DEPLOYMENTS_DIR, { recursive: true });
14
+ }
15
+ /**
16
+ * Gets the deployment directory path
17
+ */
18
+ export function getDeploymentDir(name) {
19
+ return path.join(DEPLOYMENTS_DIR, name);
20
+ }
21
+ /**
22
+ * Lists all deployments
23
+ */
24
+ export async function listDeployments() {
25
+ await ensureDirectories();
26
+ try {
27
+ const entries = await fs.readdir(DEPLOYMENTS_DIR, { withFileTypes: true });
28
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ /**
35
+ * Checks if a deployment exists
36
+ */
37
+ export async function deploymentExists(name) {
38
+ const dir = getDeploymentDir(name);
39
+ try {
40
+ await fs.access(dir);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ /**
48
+ * Saves a deployment configuration
49
+ */
50
+ export async function saveDeploymentConfig(config) {
51
+ const dir = getDeploymentDir(config.name);
52
+ await fs.mkdir(dir, { recursive: true });
53
+ const configPath = path.join(dir, "config.yaml");
54
+ await fs.writeFile(configPath, yaml.stringify(config), "utf-8");
55
+ }
56
+ /**
57
+ * Loads a deployment configuration
58
+ */
59
+ export async function loadDeploymentConfig(name) {
60
+ const configPath = path.join(getDeploymentDir(name), "config.yaml");
61
+ const content = await fs.readFile(configPath, "utf-8");
62
+ const parsed = yaml.parse(content);
63
+ return DeploymentConfigSchema.parse(parsed);
64
+ }
65
+ /**
66
+ * Clones a deployment configuration to a new name
67
+ * Only copies config.yaml with the new name - state and terraform are not copied
68
+ */
69
+ export async function cloneDeploymentConfig(sourceName, targetName) {
70
+ const sourceConfig = await loadDeploymentConfig(sourceName);
71
+ const clonedConfig = {
72
+ ...sourceConfig,
73
+ name: targetName,
74
+ };
75
+ await saveDeploymentConfig(clonedConfig);
76
+ return clonedConfig;
77
+ }
78
+ /**
79
+ * Saves the deployment state
80
+ */
81
+ export async function saveDeploymentState(name, state) {
82
+ const dir = getDeploymentDir(name);
83
+ await fs.mkdir(dir, { recursive: true });
84
+ const statePath = path.join(dir, "state.yaml");
85
+ await fs.writeFile(statePath, yaml.stringify(state), "utf-8");
86
+ }
87
+ /**
88
+ * Loads the deployment state
89
+ */
90
+ export async function loadDeploymentState(name) {
91
+ const statePath = path.join(getDeploymentDir(name), "state.yaml");
92
+ try {
93
+ const content = await fs.readFile(statePath, "utf-8");
94
+ return yaml.parse(content);
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ /**
101
+ * Saves the generated Helm values
102
+ */
103
+ export async function saveHelmValues(name, values) {
104
+ const dir = getDeploymentDir(name);
105
+ await fs.mkdir(dir, { recursive: true });
106
+ const valuesPath = path.join(dir, "values.yaml");
107
+ await fs.writeFile(valuesPath, yaml.stringify(values), "utf-8");
108
+ return valuesPath;
109
+ }
110
+ /**
111
+ * Loads the Helm values
112
+ */
113
+ export async function loadHelmValues(name) {
114
+ const valuesPath = path.join(getDeploymentDir(name), "values.yaml");
115
+ try {
116
+ const content = await fs.readFile(valuesPath, "utf-8");
117
+ return yaml.parse(content);
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ /**
124
+ * Gets the Helm values file path
125
+ */
126
+ export function getHelmValuesPath(name) {
127
+ return path.join(getDeploymentDir(name), "values.yaml");
128
+ }
129
+ /**
130
+ * Saves Terraform variables
131
+ */
132
+ export async function saveTerraformVars(name, vars) {
133
+ const dir = path.join(getDeploymentDir(name), "terraform");
134
+ await fs.mkdir(dir, { recursive: true });
135
+ // Convert to HCL-compatible format
136
+ let content = "";
137
+ for (const [key, value] of Object.entries(vars)) {
138
+ if (typeof value === "string") {
139
+ content += `${key} = "${value}"\n`;
140
+ }
141
+ else if (typeof value === "number" || typeof value === "boolean") {
142
+ content += `${key} = ${value}\n`;
143
+ }
144
+ else {
145
+ content += `${key} = ${JSON.stringify(value)}\n`;
146
+ }
147
+ }
148
+ const varsPath = path.join(dir, "terraform.tfvars");
149
+ await fs.writeFile(varsPath, content, "utf-8");
150
+ return varsPath;
151
+ }
152
+ /**
153
+ * Gets the Terraform working directory
154
+ */
155
+ export function getTerraformDir(name) {
156
+ return path.join(getDeploymentDir(name), "terraform");
157
+ }
158
+ /**
159
+ * Deletes a deployment and all its files
160
+ */
161
+ export async function deleteDeployment(name) {
162
+ const dir = getDeploymentDir(name);
163
+ await fs.rm(dir, { recursive: true, force: true });
164
+ }
165
+ /**
166
+ * Updates the deployment state status
167
+ */
168
+ export async function updateDeploymentStatus(name, status, updates) {
169
+ const state = await loadDeploymentState(name);
170
+ if (state) {
171
+ // Deep merge nested objects like application, infrastructure, dnsRecords
172
+ const updatedState = {
173
+ ...state,
174
+ ...updates,
175
+ // Deep merge application object to preserve existing fields
176
+ application: updates?.application
177
+ ? { ...state.application, ...updates.application }
178
+ : state.application,
179
+ // Deep merge infrastructure object
180
+ infrastructure: updates?.infrastructure
181
+ ? { ...state.infrastructure, ...updates.infrastructure }
182
+ : state.infrastructure,
183
+ status,
184
+ updatedAt: new Date().toISOString(),
185
+ };
186
+ await saveDeploymentState(name, updatedState);
187
+ }
188
+ }
189
+ // ============================================================================
190
+ // Profile Management - Persistent user preferences across deployments
191
+ // ============================================================================
192
+ /**
193
+ * Extracts the domain suffix from a full domain.
194
+ * e.g., "app.example.com" -> ".example.com"
195
+ * e.g., "sub.app.example.com" -> ".app.example.com"
196
+ */
197
+ export function extractDomainSuffix(domain) {
198
+ if (!domain)
199
+ return undefined;
200
+ const parts = domain.split(".");
201
+ if (parts.length < 2)
202
+ return undefined;
203
+ // Return everything after the first segment
204
+ return "." + parts.slice(1).join(".");
205
+ }
206
+ /**
207
+ * Loads the user profile from ~/.rulebricks/profile.yaml
208
+ * Returns null if the profile doesn't exist or is invalid
209
+ */
210
+ export async function loadProfile() {
211
+ const profilePath = path.join(RULEBRICKS_DIR, PROFILE_FILE);
212
+ try {
213
+ const content = await fs.readFile(profilePath, "utf-8");
214
+ const data = yaml.parse(content);
215
+ return ProfileConfigSchema.parse(data);
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ /**
222
+ * Saves the user profile to ~/.rulebricks/profile.yaml
223
+ */
224
+ export async function saveProfile(profile) {
225
+ await fs.mkdir(RULEBRICKS_DIR, { recursive: true });
226
+ const profilePath = path.join(RULEBRICKS_DIR, PROFILE_FILE);
227
+ // Filter out undefined values to keep the file clean
228
+ const cleanProfile = Object.fromEntries(Object.entries(profile).filter(([_, v]) => v !== undefined));
229
+ await fs.writeFile(profilePath, yaml.stringify(cleanProfile), "utf-8");
230
+ }
231
+ /**
232
+ * Extracts profile-worthy values from a deployment configuration.
233
+ * These are values that are likely to be reused across deployments.
234
+ */
235
+ export function extractProfileFromConfig(config) {
236
+ return {
237
+ // Infrastructure
238
+ provider: config.infrastructure.provider,
239
+ region: config.infrastructure.region,
240
+ clusterName: config.infrastructure.clusterName,
241
+ infrastructureMode: config.infrastructure.mode,
242
+ // Domain - store suffix for suggesting new domains
243
+ domainSuffix: extractDomainSuffix(config.domain),
244
+ adminEmail: config.adminEmail,
245
+ tlsEmail: config.tlsEmail,
246
+ dnsProvider: config.dns.provider,
247
+ // SMTP
248
+ smtpHost: config.smtp.host,
249
+ smtpPort: config.smtp.port,
250
+ smtpUser: config.smtp.user,
251
+ smtpPass: config.smtp.pass,
252
+ smtpFrom: config.smtp.from,
253
+ smtpFromName: config.smtp.fromName,
254
+ // API Keys
255
+ openaiApiKey: config.features.ai.openaiApiKey,
256
+ licenseKey: config.licenseKey,
257
+ // Preferences
258
+ tier: config.tier,
259
+ databaseType: config.database.type,
260
+ // SSO
261
+ ssoProvider: config.features.sso.provider,
262
+ ssoUrl: config.features.sso.url,
263
+ ssoClientId: config.features.sso.clientId,
264
+ ssoClientSecret: config.features.sso.clientSecret,
265
+ };
266
+ }
267
+ /**
268
+ * Merges a new profile with an existing one, preferring new non-undefined values.
269
+ * This allows incremental updates without losing existing preferences.
270
+ */
271
+ export async function updateProfile(newValues) {
272
+ const existing = await loadProfile();
273
+ const merged = {
274
+ ...existing,
275
+ ...Object.fromEntries(Object.entries(newValues).filter(([_, v]) => v !== undefined)),
276
+ };
277
+ await saveProfile(merged);
278
+ }
@@ -0,0 +1,41 @@
1
+ import { DNSRecord } from "../types/index.js";
2
+ /**
3
+ * Checks if a DNS record resolves
4
+ */
5
+ export declare function checkDNSRecord(hostname: string, expectedTarget?: string): Promise<{
6
+ resolved: boolean;
7
+ records: string[];
8
+ type: "A" | "CNAME" | null;
9
+ matchesTarget: boolean;
10
+ }>;
11
+ /**
12
+ * Gets the load balancer address from Kubernetes
13
+ */
14
+ export declare function getLoadBalancerAddress(namespace?: string): Promise<{
15
+ address: string | null;
16
+ type: "ip" | "hostname" | null;
17
+ }>;
18
+ /**
19
+ * Gets all required DNS records for a deployment
20
+ */
21
+ export declare function getRequiredDNSRecords(domain: string, loadBalancerAddress: string, loadBalancerType: "ip" | "hostname", selfHostedSupabase: boolean): DNSRecord[];
22
+ /**
23
+ * Polls DNS records until they resolve or timeout
24
+ */
25
+ export declare function waitForDNSRecords(records: DNSRecord[], options?: {
26
+ pollIntervalMs?: number;
27
+ timeoutMs?: number;
28
+ onUpdate?: (records: DNSRecord[]) => void;
29
+ }): Promise<{
30
+ success: boolean;
31
+ records: DNSRecord[];
32
+ failedRecords: DNSRecord[];
33
+ }>;
34
+ /**
35
+ * Formats a DNS record for display
36
+ */
37
+ export declare function formatDNSRecord(record: DNSRecord): string;
38
+ /**
39
+ * Checks if DNS propagation is complete for all records
40
+ */
41
+ export declare function isDNSComplete(records: DNSRecord[]): boolean;
@@ -0,0 +1,235 @@
1
+ import dns from "dns";
2
+ import { promisify } from "util";
3
+ import { execa } from "execa";
4
+ import { DEFAULT_NAMESPACE } from "../types/index.js";
5
+ const resolve4 = promisify(dns.resolve4);
6
+ const resolveCname = promisify(dns.resolveCname);
7
+ /**
8
+ * Helper to detect if a string is an IP address
9
+ */
10
+ function isIPAddress(target) {
11
+ return /^(\d{1,3}\.){3}\d{1,3}$/.test(target);
12
+ }
13
+ /**
14
+ * Helper to check if CNAME records match expected target
15
+ */
16
+ function cnameMatchesTarget(cnameRecords, expectedTarget) {
17
+ return cnameRecords.some((r) => r === expectedTarget ||
18
+ r.endsWith(expectedTarget) ||
19
+ r.replace(/\.$/, "") === expectedTarget.replace(/\.$/, ""));
20
+ }
21
+ /**
22
+ * Checks if a DNS record resolves
23
+ */
24
+ export async function checkDNSRecord(hostname, expectedTarget) {
25
+ try {
26
+ // If expected target is a hostname (not an IP), check CNAME first
27
+ if (expectedTarget && !isIPAddress(expectedTarget)) {
28
+ try {
29
+ const cnameRecords = await resolveCname(hostname);
30
+ // CNAME records found - return the comparison result directly
31
+ // Don't fall through to A record check, as that would incorrectly
32
+ // compare IPs against a hostname target
33
+ const matchesTarget = cnameMatchesTarget(cnameRecords, expectedTarget);
34
+ return {
35
+ resolved: true,
36
+ records: cnameRecords,
37
+ type: "CNAME",
38
+ matchesTarget,
39
+ };
40
+ }
41
+ catch {
42
+ // No CNAME record exists - try A record check
43
+ // For hostname targets, we need to resolve both the hostname and target
44
+ // to IPs and compare them
45
+ try {
46
+ const [hostnameIPs, targetIPs] = await Promise.all([
47
+ resolve4(hostname),
48
+ resolve4(expectedTarget),
49
+ ]);
50
+ // Check if any of the hostname's IPs match any of the target's IPs
51
+ const matchesTarget = hostnameIPs.some((ip) => targetIPs.includes(ip));
52
+ return {
53
+ resolved: true,
54
+ records: hostnameIPs,
55
+ type: "A",
56
+ matchesTarget,
57
+ };
58
+ }
59
+ catch {
60
+ // Could not resolve - DNS not configured
61
+ return {
62
+ resolved: false,
63
+ records: [],
64
+ type: null,
65
+ matchesTarget: false,
66
+ };
67
+ }
68
+ }
69
+ }
70
+ // Expected target is an IP address - check A records directly
71
+ try {
72
+ const aRecords = await resolve4(hostname);
73
+ const matchesTarget = expectedTarget
74
+ ? aRecords.some((r) => r === expectedTarget)
75
+ : true;
76
+ return {
77
+ resolved: true,
78
+ records: aRecords,
79
+ type: "A",
80
+ matchesTarget,
81
+ };
82
+ }
83
+ catch {
84
+ // Try CNAME if A fails (fallback for when no expected target)
85
+ const cnameRecords = await resolveCname(hostname);
86
+ const matchesTarget = expectedTarget
87
+ ? cnameMatchesTarget(cnameRecords, expectedTarget)
88
+ : true;
89
+ return {
90
+ resolved: true,
91
+ records: cnameRecords,
92
+ type: "CNAME",
93
+ matchesTarget,
94
+ };
95
+ }
96
+ }
97
+ catch {
98
+ return {
99
+ resolved: false,
100
+ records: [],
101
+ type: null,
102
+ matchesTarget: false,
103
+ };
104
+ }
105
+ }
106
+ /**
107
+ * Gets the load balancer address from Kubernetes
108
+ */
109
+ export async function getLoadBalancerAddress(namespace = DEFAULT_NAMESPACE) {
110
+ try {
111
+ // Get the Traefik service which is typically the load balancer
112
+ const { stdout } = await execa("kubectl", [
113
+ "get",
114
+ "service",
115
+ "-n",
116
+ namespace,
117
+ "-l",
118
+ "app.kubernetes.io/name=traefik",
119
+ "-o",
120
+ "jsonpath={.items[0].status.loadBalancer.ingress[0]}",
121
+ ]);
122
+ if (!stdout || stdout === "{}") {
123
+ // Try looking for any LoadBalancer service
124
+ const { stdout: allServices } = await execa("kubectl", [
125
+ "get",
126
+ "service",
127
+ "-n",
128
+ namespace,
129
+ "--field-selector=spec.type=LoadBalancer",
130
+ "-o",
131
+ "jsonpath={.items[0].status.loadBalancer.ingress[0]}",
132
+ ]);
133
+ if (!allServices || allServices === "{}") {
134
+ return { address: null, type: null };
135
+ }
136
+ const parsed = JSON.parse(allServices || "{}");
137
+ if (parsed.ip) {
138
+ return { address: parsed.ip, type: "ip" };
139
+ }
140
+ if (parsed.hostname) {
141
+ return { address: parsed.hostname, type: "hostname" };
142
+ }
143
+ }
144
+ const parsed = JSON.parse(stdout || "{}");
145
+ if (parsed.ip) {
146
+ return { address: parsed.ip, type: "ip" };
147
+ }
148
+ if (parsed.hostname) {
149
+ return { address: parsed.hostname, type: "hostname" };
150
+ }
151
+ return { address: null, type: null };
152
+ }
153
+ catch {
154
+ return { address: null, type: null };
155
+ }
156
+ }
157
+ /**
158
+ * Gets all required DNS records for a deployment
159
+ */
160
+ export function getRequiredDNSRecords(domain, loadBalancerAddress, loadBalancerType, selfHostedSupabase) {
161
+ const records = [
162
+ {
163
+ hostname: domain,
164
+ type: loadBalancerType === "ip" ? "A" : "CNAME",
165
+ target: loadBalancerAddress,
166
+ verified: false,
167
+ required: true,
168
+ },
169
+ ];
170
+ // If self-hosted Supabase, need supabase subdomain
171
+ if (selfHostedSupabase) {
172
+ records.push({
173
+ hostname: `supabase.${domain}`,
174
+ type: loadBalancerType === "ip" ? "A" : "CNAME",
175
+ target: loadBalancerAddress,
176
+ verified: false,
177
+ required: true,
178
+ });
179
+ }
180
+ return records;
181
+ }
182
+ /**
183
+ * Polls DNS records until they resolve or timeout
184
+ */
185
+ export async function waitForDNSRecords(records, options = {}) {
186
+ const { pollIntervalMs = 5000, timeoutMs = 300000, // 5 minutes default
187
+ onUpdate, } = options;
188
+ const startTime = Date.now();
189
+ const updatedRecords = [...records];
190
+ while (Date.now() - startTime < timeoutMs) {
191
+ let allResolved = true;
192
+ for (let i = 0; i < updatedRecords.length; i++) {
193
+ const record = updatedRecords[i];
194
+ if (record.verified) {
195
+ continue;
196
+ }
197
+ const result = await checkDNSRecord(record.hostname, record.target);
198
+ if (result.resolved && result.matchesTarget) {
199
+ updatedRecords[i] = { ...record, verified: true };
200
+ }
201
+ else {
202
+ allResolved = false;
203
+ }
204
+ }
205
+ onUpdate?.(updatedRecords);
206
+ if (allResolved) {
207
+ return {
208
+ success: true,
209
+ records: updatedRecords,
210
+ failedRecords: [],
211
+ };
212
+ }
213
+ // Wait before next poll
214
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
215
+ }
216
+ // Timeout reached
217
+ const failedRecords = updatedRecords.filter((r) => !r.verified);
218
+ return {
219
+ success: false,
220
+ records: updatedRecords,
221
+ failedRecords,
222
+ };
223
+ }
224
+ /**
225
+ * Formats a DNS record for display
226
+ */
227
+ export function formatDNSRecord(record) {
228
+ return `${record.hostname} → ${record.type} → ${record.target}`;
229
+ }
230
+ /**
231
+ * Checks if DNS propagation is complete for all records
232
+ */
233
+ export function isDNSComplete(records) {
234
+ return records.every((r) => r.verified);
235
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Docker Hub API client for fetching image tags
3
+ *
4
+ * Uses the license key as a Docker PAT for authentication
5
+ * to access private Rulebricks images.
6
+ */
7
+ /**
8
+ * Represents a Docker image tag with metadata
9
+ */
10
+ export interface ImageTag {
11
+ /** Tag name (e.g., "1.2.3" or "v1.2.3") */
12
+ name: string;
13
+ /** When the tag was last updated/pushed */
14
+ lastUpdated: Date;
15
+ /** Image digest */
16
+ digest: string;
17
+ /** Full image size in bytes */
18
+ fullSize: number;
19
+ }
20
+ /**
21
+ * Formats the license key as a Docker PAT
22
+ */
23
+ export declare function formatDockerPat(licenseKey: string): string;
24
+ /**
25
+ * Authenticates with Docker Hub using the license key as a PAT
26
+ *
27
+ * @param licenseKey - The Rulebricks license key (used as Docker PAT)
28
+ * @returns JWT token for subsequent API calls
29
+ */
30
+ export declare function authenticateDockerHub(licenseKey: string): Promise<string>;
31
+ /**
32
+ * Fetches available tags for a Docker Hub repository
33
+ *
34
+ * @param repo - Repository name (e.g., "rulebricks/app")
35
+ * @param token - JWT token from authentication
36
+ * @param pageSize - Number of tags to fetch per page (max 100)
37
+ * @returns Array of image tags sorted by last updated (newest first)
38
+ */
39
+ export declare function fetchImageTags(repo: string, token: string, pageSize?: number): Promise<ImageTag[]>;
40
+ /**
41
+ * Fetches tags for both app and HPS repositories
42
+ *
43
+ * @param licenseKey - The Rulebricks license key
44
+ * @returns Object containing app and HPS tags
45
+ */
46
+ export declare function fetchAllImageTags(licenseKey: string): Promise<{
47
+ appTags: ImageTag[];
48
+ hpsTags: ImageTag[];
49
+ }>;
50
+ /**
51
+ * Normalizes a version string by removing leading 'v'
52
+ */
53
+ export declare function normalizeVersion(version: string): string;
54
+ /**
55
+ * Formats a version for display (ensures 'v' prefix)
56
+ */
57
+ export declare function formatVersionDisplay(version: string): string;