@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.
- package/README.md +62 -0
- package/dist/commands/clone.d.ts +6 -0
- package/dist/commands/clone.js +60 -0
- package/dist/commands/deploy.d.ts +8 -0
- package/dist/commands/deploy.js +409 -0
- package/dist/commands/destroy.d.ts +8 -0
- package/dist/commands/destroy.js +298 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +201 -0
- package/dist/commands/logs.d.ts +9 -0
- package/dist/commands/logs.js +222 -0
- package/dist/commands/open.d.ts +7 -0
- package/dist/commands/open.js +139 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +125 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +239 -0
- package/dist/components/DNSWaitScreen.d.ts +9 -0
- package/dist/components/DNSWaitScreen.js +73 -0
- package/dist/components/Wizard/WizardContext.d.ts +176 -0
- package/dist/components/Wizard/WizardContext.js +346 -0
- package/dist/components/Wizard/index.d.ts +2 -0
- package/dist/components/Wizard/index.js +2 -0
- package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
- package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
- package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
- package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
- package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
- package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
- package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
- package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
- package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
- package/dist/components/Wizard/steps/DomainStep.js +126 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
- package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
- package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
- package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ReviewStep.js +56 -0
- package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
- package/dist/components/Wizard/steps/SMTPStep.js +191 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
- package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
- package/dist/components/Wizard/steps/TierStep.js +29 -0
- package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
- package/dist/components/Wizard/steps/VersionStep.js +113 -0
- package/dist/components/Wizard/steps/index.d.ts +12 -0
- package/dist/components/Wizard/steps/index.js +12 -0
- package/dist/components/common/AppShell.d.ts +31 -0
- package/dist/components/common/AppShell.js +31 -0
- package/dist/components/common/Box.d.ts +20 -0
- package/dist/components/common/Box.js +20 -0
- package/dist/components/common/Logo.d.ts +7 -0
- package/dist/components/common/Logo.js +22 -0
- package/dist/components/common/Spinner.d.ts +12 -0
- package/dist/components/common/Spinner.js +28 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/index.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +202 -0
- package/dist/lib/cloudCli.d.ts +156 -0
- package/dist/lib/cloudCli.js +691 -0
- package/dist/lib/config.d.ts +91 -0
- package/dist/lib/config.js +278 -0
- package/dist/lib/dns.d.ts +41 -0
- package/dist/lib/dns.js +235 -0
- package/dist/lib/dockerHub.d.ts +57 -0
- package/dist/lib/dockerHub.js +128 -0
- package/dist/lib/helm.d.ts +53 -0
- package/dist/lib/helm.js +209 -0
- package/dist/lib/helmValues.d.ts +17 -0
- package/dist/lib/helmValues.js +693 -0
- package/dist/lib/kubernetes.d.ts +161 -0
- package/dist/lib/kubernetes.js +755 -0
- package/dist/lib/terraform.d.ts +44 -0
- package/dist/lib/terraform.js +230 -0
- package/dist/lib/theme.d.ts +81 -0
- package/dist/lib/theme.js +115 -0
- package/dist/lib/validation.d.ts +47 -0
- package/dist/lib/validation.js +164 -0
- package/dist/lib/versions.d.ts +69 -0
- package/dist/lib/versions.js +139 -0
- package/dist/types/index.d.ts +718 -0
- package/dist/types/index.js +556 -0
- package/email-templates/email_change.html +325 -0
- package/email-templates/invite.html +383 -0
- package/email-templates/password_change.html +414 -0
- package/email-templates/verify.html +396 -0
- package/package.json +78 -0
- package/terraform/aws/main.tf +327 -0
- package/terraform/azure/main.tf +326 -0
- 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;
|
package/dist/lib/dns.js
ADDED
|
@@ -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;
|