@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,44 @@
|
|
|
1
|
+
import { CloudProvider } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if Terraform is installed
|
|
4
|
+
*/
|
|
5
|
+
export declare function isTerraformInstalled(): Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* Gets the installed Terraform version
|
|
8
|
+
*/
|
|
9
|
+
export declare function getTerraformVersion(): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Copies terraform templates to the deployment directory
|
|
12
|
+
*/
|
|
13
|
+
export declare function setupTerraformWorkspace(deploymentName: string, provider: CloudProvider): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Initializes Terraform in the deployment directory
|
|
16
|
+
*/
|
|
17
|
+
export declare function terraformInit(deploymentName: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Plans Terraform changes
|
|
20
|
+
*/
|
|
21
|
+
export declare function terraformPlan(deploymentName: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Applies Terraform changes
|
|
24
|
+
*/
|
|
25
|
+
export declare function terraformApply(deploymentName: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Destroys Terraform infrastructure
|
|
28
|
+
*/
|
|
29
|
+
export declare function terraformDestroy(deploymentName: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Gets Terraform outputs
|
|
32
|
+
*/
|
|
33
|
+
export declare function getTerraformOutputs(deploymentName: string): Promise<Record<string, string>>;
|
|
34
|
+
/**
|
|
35
|
+
* Checks if Terraform state exists for a deployment
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasTerraformState(deploymentName: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Updates kubeconfig for the provisioned cluster
|
|
40
|
+
*/
|
|
41
|
+
export declare function updateKubeconfig(provider: CloudProvider, clusterName: string, region: string, options?: {
|
|
42
|
+
gcpProjectId?: string;
|
|
43
|
+
azureResourceGroup?: string;
|
|
44
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { getTerraformDir } from './config.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
// Path to embedded terraform templates
|
|
9
|
+
const TERRAFORM_TEMPLATES_DIR = path.resolve(__dirname, '../../terraform');
|
|
10
|
+
/**
|
|
11
|
+
* Extracts meaningful error message from execa error
|
|
12
|
+
*/
|
|
13
|
+
function getErrorMessage(error, fallback) {
|
|
14
|
+
const execaError = error;
|
|
15
|
+
// Try stderr first, then stdout (terraform sometimes writes errors to stdout)
|
|
16
|
+
const output = execaError.stderr || execaError.stdout || '';
|
|
17
|
+
if (output) {
|
|
18
|
+
// Get last 500 chars of output for the error message
|
|
19
|
+
const truncated = output.length > 500 ? '...' + output.slice(-500) : output;
|
|
20
|
+
return truncated;
|
|
21
|
+
}
|
|
22
|
+
return execaError.shortMessage || execaError.message || fallback;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Saves command output to a log file
|
|
26
|
+
*/
|
|
27
|
+
async function saveLogFile(workDir, command, stdout, stderr) {
|
|
28
|
+
const logFile = path.join(workDir, `${command}-${Date.now()}.log`);
|
|
29
|
+
const content = `=== STDOUT ===\n${stdout}\n\n=== STDERR ===\n${stderr}`;
|
|
30
|
+
await fs.writeFile(logFile, content);
|
|
31
|
+
return logFile;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checks if Terraform is installed
|
|
35
|
+
*/
|
|
36
|
+
export async function isTerraformInstalled() {
|
|
37
|
+
try {
|
|
38
|
+
await execa('terraform', ['version']);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Gets the installed Terraform version
|
|
47
|
+
*/
|
|
48
|
+
export async function getTerraformVersion() {
|
|
49
|
+
const { stdout } = await execa('terraform', ['version', '-json']);
|
|
50
|
+
const info = JSON.parse(stdout);
|
|
51
|
+
return info.terraform_version;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Copies terraform templates to the deployment directory
|
|
55
|
+
*/
|
|
56
|
+
export async function setupTerraformWorkspace(deploymentName, provider) {
|
|
57
|
+
const sourceDir = path.join(TERRAFORM_TEMPLATES_DIR, provider);
|
|
58
|
+
const targetDir = getTerraformDir(deploymentName);
|
|
59
|
+
// Create target directory
|
|
60
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
61
|
+
// Copy all terraform files
|
|
62
|
+
await copyDirectory(sourceDir, targetDir);
|
|
63
|
+
return targetDir;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Recursively copies a directory
|
|
67
|
+
*/
|
|
68
|
+
async function copyDirectory(src, dest) {
|
|
69
|
+
await fs.mkdir(dest, { recursive: true });
|
|
70
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const srcPath = path.join(src, entry.name);
|
|
73
|
+
const destPath = path.join(dest, entry.name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
await copyDirectory(srcPath, destPath);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
await fs.copyFile(srcPath, destPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Initializes Terraform in the deployment directory
|
|
84
|
+
*/
|
|
85
|
+
export async function terraformInit(deploymentName) {
|
|
86
|
+
const workDir = getTerraformDir(deploymentName);
|
|
87
|
+
try {
|
|
88
|
+
// Use 'pipe' to capture output instead of 'inherit' to avoid
|
|
89
|
+
// interfering with Ink's terminal rendering
|
|
90
|
+
await execa('terraform', ['init', '-upgrade'], {
|
|
91
|
+
cwd: workDir
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const execaError = error;
|
|
96
|
+
// Save logs for debugging
|
|
97
|
+
if (execaError.stdout || execaError.stderr) {
|
|
98
|
+
await saveLogFile(workDir, 'init', execaError.stdout || '', execaError.stderr || '');
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Terraform init failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Plans Terraform changes
|
|
105
|
+
*/
|
|
106
|
+
export async function terraformPlan(deploymentName) {
|
|
107
|
+
const workDir = getTerraformDir(deploymentName);
|
|
108
|
+
try {
|
|
109
|
+
await execa('terraform', ['plan', '-out=tfplan'], {
|
|
110
|
+
cwd: workDir
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const execaError = error;
|
|
115
|
+
if (execaError.stdout || execaError.stderr) {
|
|
116
|
+
await saveLogFile(workDir, 'plan', execaError.stdout || '', execaError.stderr || '');
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Terraform plan failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Applies Terraform changes
|
|
123
|
+
*/
|
|
124
|
+
export async function terraformApply(deploymentName) {
|
|
125
|
+
const workDir = getTerraformDir(deploymentName);
|
|
126
|
+
try {
|
|
127
|
+
await execa('terraform', ['apply', '-auto-approve', 'tfplan'], {
|
|
128
|
+
cwd: workDir
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const execaError = error;
|
|
133
|
+
if (execaError.stdout || execaError.stderr) {
|
|
134
|
+
await saveLogFile(workDir, 'apply', execaError.stdout || '', execaError.stderr || '');
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Terraform apply failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Destroys Terraform infrastructure
|
|
141
|
+
*/
|
|
142
|
+
export async function terraformDestroy(deploymentName) {
|
|
143
|
+
const workDir = getTerraformDir(deploymentName);
|
|
144
|
+
try {
|
|
145
|
+
await execa('terraform', ['destroy', '-auto-approve'], {
|
|
146
|
+
cwd: workDir
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const execaError = error;
|
|
151
|
+
if (execaError.stdout || execaError.stderr) {
|
|
152
|
+
await saveLogFile(workDir, 'destroy', execaError.stdout || '', execaError.stderr || '');
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Terraform destroy failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Gets Terraform outputs
|
|
159
|
+
*/
|
|
160
|
+
export async function getTerraformOutputs(deploymentName) {
|
|
161
|
+
const workDir = getTerraformDir(deploymentName);
|
|
162
|
+
try {
|
|
163
|
+
const { stdout } = await execa('terraform', ['output', '-json'], {
|
|
164
|
+
cwd: workDir
|
|
165
|
+
});
|
|
166
|
+
const outputs = JSON.parse(stdout);
|
|
167
|
+
const result = {};
|
|
168
|
+
for (const [key, data] of Object.entries(outputs)) {
|
|
169
|
+
result[key] = String(data.value);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Checks if Terraform state exists for a deployment
|
|
179
|
+
*/
|
|
180
|
+
export async function hasTerraformState(deploymentName) {
|
|
181
|
+
const workDir = getTerraformDir(deploymentName);
|
|
182
|
+
const statePath = path.join(workDir, 'terraform.tfstate');
|
|
183
|
+
try {
|
|
184
|
+
await fs.access(statePath);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Updates kubeconfig for the provisioned cluster
|
|
193
|
+
*/
|
|
194
|
+
export async function updateKubeconfig(provider, clusterName, region, options = {}) {
|
|
195
|
+
try {
|
|
196
|
+
switch (provider) {
|
|
197
|
+
case 'aws':
|
|
198
|
+
await execa('aws', [
|
|
199
|
+
'eks', 'update-kubeconfig',
|
|
200
|
+
'--name', clusterName,
|
|
201
|
+
'--region', region
|
|
202
|
+
]);
|
|
203
|
+
break;
|
|
204
|
+
case 'gcp':
|
|
205
|
+
if (!options.gcpProjectId) {
|
|
206
|
+
throw new Error('GCP project ID is required');
|
|
207
|
+
}
|
|
208
|
+
await execa('gcloud', [
|
|
209
|
+
'container', 'clusters', 'get-credentials',
|
|
210
|
+
clusterName,
|
|
211
|
+
'--region', region,
|
|
212
|
+
'--project', options.gcpProjectId
|
|
213
|
+
]);
|
|
214
|
+
break;
|
|
215
|
+
case 'azure':
|
|
216
|
+
if (!options.azureResourceGroup) {
|
|
217
|
+
throw new Error('Azure resource group is required');
|
|
218
|
+
}
|
|
219
|
+
await execa('az', [
|
|
220
|
+
'aks', 'get-credentials',
|
|
221
|
+
'--name', clusterName,
|
|
222
|
+
'--resource-group', options.azureResourceGroup
|
|
223
|
+
]);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
throw new Error(`Failed to update kubeconfig:\n${getErrorMessage(error, 'Unknown error')}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Command theme types - each command has its own visual identity
|
|
4
|
+
*/
|
|
5
|
+
export type CommandTheme = "init" | "deploy" | "upgrade" | "destroy" | "status" | "logs";
|
|
6
|
+
/**
|
|
7
|
+
* Theme color configuration
|
|
8
|
+
*/
|
|
9
|
+
export interface ThemeColors {
|
|
10
|
+
/** Primary accent color for borders, highlights */
|
|
11
|
+
accent: string;
|
|
12
|
+
/** Brighter variant for emphasis */
|
|
13
|
+
accentBright: string;
|
|
14
|
+
/** Color for selected/active items */
|
|
15
|
+
selected: string;
|
|
16
|
+
/** Color for success states */
|
|
17
|
+
success: string;
|
|
18
|
+
/** Color for error states */
|
|
19
|
+
error: string;
|
|
20
|
+
/** Color for warning states */
|
|
21
|
+
warning: string;
|
|
22
|
+
/** Dimmed/muted color */
|
|
23
|
+
muted: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Theme definitions for each command
|
|
27
|
+
*
|
|
28
|
+
* - init: Magenta - fresh start, creative setup
|
|
29
|
+
* - deploy: Blue - action, progress, trust
|
|
30
|
+
* - upgrade: #ea9d34 - caution, change, attention
|
|
31
|
+
* - destroy: Red - danger, destructive action
|
|
32
|
+
* - status: #4c9c81 - health, success, information
|
|
33
|
+
* - logs: #c2b5ab - neutral, observational
|
|
34
|
+
*/
|
|
35
|
+
export declare const THEMES: Record<CommandTheme, ThemeColors>;
|
|
36
|
+
/**
|
|
37
|
+
* Default theme (used when no provider is present)
|
|
38
|
+
*/
|
|
39
|
+
export declare const DEFAULT_THEME: CommandTheme;
|
|
40
|
+
/**
|
|
41
|
+
* Theme context value
|
|
42
|
+
*/
|
|
43
|
+
interface ThemeContextValue {
|
|
44
|
+
theme: CommandTheme;
|
|
45
|
+
colors: ThemeColors;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* ThemeProvider props
|
|
49
|
+
*/
|
|
50
|
+
interface ThemeProviderProps {
|
|
51
|
+
theme: CommandTheme;
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* ThemeProvider component - wraps a command to provide themed styling
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <ThemeProvider theme="destroy">
|
|
60
|
+
* <DestroyCommand />
|
|
61
|
+
* </ThemeProvider>
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function ThemeProvider({ theme, children, }: ThemeProviderProps): React.ReactElement;
|
|
65
|
+
/**
|
|
66
|
+
* Hook to access current theme colors
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* function MyComponent() {
|
|
71
|
+
* const { colors } = useTheme();
|
|
72
|
+
* return <Text color={colors.accent}>Themed text</Text>;
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function useTheme(): ThemeContextValue;
|
|
77
|
+
/**
|
|
78
|
+
* Get theme colors directly without hook (for non-component code)
|
|
79
|
+
*/
|
|
80
|
+
export declare function getThemeColors(theme: CommandTheme): ThemeColors;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Theme definitions for each command
|
|
4
|
+
*
|
|
5
|
+
* - init: Magenta - fresh start, creative setup
|
|
6
|
+
* - deploy: Blue - action, progress, trust
|
|
7
|
+
* - upgrade: #ea9d34 - caution, change, attention
|
|
8
|
+
* - destroy: Red - danger, destructive action
|
|
9
|
+
* - status: #4c9c81 - health, success, information
|
|
10
|
+
* - logs: #c2b5ab - neutral, observational
|
|
11
|
+
*/
|
|
12
|
+
export const THEMES = {
|
|
13
|
+
init: {
|
|
14
|
+
accent: "#c4a7e7",
|
|
15
|
+
accentBright: "#a78bc7",
|
|
16
|
+
selected: "#c4a7e7",
|
|
17
|
+
success: "#4c9c81",
|
|
18
|
+
error: "#d7827e",
|
|
19
|
+
warning: "#ea9d34",
|
|
20
|
+
muted: "#c2b5ab",
|
|
21
|
+
},
|
|
22
|
+
deploy: {
|
|
23
|
+
accent: "#64a5b0",
|
|
24
|
+
accentBright: "#5fbac9",
|
|
25
|
+
selected: "#64a5b0",
|
|
26
|
+
success: "#4c9c81",
|
|
27
|
+
error: "#d7827e",
|
|
28
|
+
warning: "#ea9d34",
|
|
29
|
+
muted: "#c2b5ab",
|
|
30
|
+
},
|
|
31
|
+
upgrade: {
|
|
32
|
+
accent: "#3e8fb0",
|
|
33
|
+
accentBright: "#5aabcc",
|
|
34
|
+
selected: "#3e8fb0",
|
|
35
|
+
success: "#4c9c81",
|
|
36
|
+
error: "#d7827e",
|
|
37
|
+
warning: "#ea9d34",
|
|
38
|
+
muted: "#c2b5ab",
|
|
39
|
+
},
|
|
40
|
+
destroy: {
|
|
41
|
+
accent: "#d7827e",
|
|
42
|
+
accentBright: "#ea9a97",
|
|
43
|
+
selected: "#d7827e",
|
|
44
|
+
success: "#4c9c81",
|
|
45
|
+
error: "#d7827e",
|
|
46
|
+
warning: "#cf6d69",
|
|
47
|
+
muted: "#a18581",
|
|
48
|
+
},
|
|
49
|
+
status: {
|
|
50
|
+
accent: "#4c9c81",
|
|
51
|
+
accentBright: "#4c9c81",
|
|
52
|
+
selected: "#4c9c81",
|
|
53
|
+
success: "#4c9c81",
|
|
54
|
+
error: "#d7827e",
|
|
55
|
+
warning: "#ea9d34",
|
|
56
|
+
muted: "#c2b5ab",
|
|
57
|
+
},
|
|
58
|
+
logs: {
|
|
59
|
+
accent: "#524f67",
|
|
60
|
+
accentBright: "#56526e",
|
|
61
|
+
selected: "#cecacd",
|
|
62
|
+
success: "#4c9c81",
|
|
63
|
+
error: "#d7827e",
|
|
64
|
+
warning: "#ea9d34",
|
|
65
|
+
muted: "#c2b5ab",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Default theme (used when no provider is present)
|
|
70
|
+
*/
|
|
71
|
+
export const DEFAULT_THEME = "init";
|
|
72
|
+
/**
|
|
73
|
+
* Theme context - provides current theme to all child components
|
|
74
|
+
*/
|
|
75
|
+
const ThemeContext = createContext({
|
|
76
|
+
theme: DEFAULT_THEME,
|
|
77
|
+
colors: THEMES[DEFAULT_THEME],
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* ThemeProvider component - wraps a command to provide themed styling
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* <ThemeProvider theme="destroy">
|
|
85
|
+
* <DestroyCommand />
|
|
86
|
+
* </ThemeProvider>
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function ThemeProvider({ theme, children, }) {
|
|
90
|
+
const value = {
|
|
91
|
+
theme,
|
|
92
|
+
colors: THEMES[theme],
|
|
93
|
+
};
|
|
94
|
+
return React.createElement(ThemeContext.Provider, { value }, children);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Hook to access current theme colors
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* function MyComponent() {
|
|
102
|
+
* const { colors } = useTheme();
|
|
103
|
+
* return <Text color={colors.accent}>Themed text</Text>;
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function useTheme() {
|
|
108
|
+
return useContext(ThemeContext);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get theme colors directly without hook (for non-component code)
|
|
112
|
+
*/
|
|
113
|
+
export function getThemeColors(theme) {
|
|
114
|
+
return THEMES[theme];
|
|
115
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates an email address using a comprehensive regex
|
|
3
|
+
*/
|
|
4
|
+
export declare function isValidEmail(email: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Extracts the base domain from a full domain name
|
|
7
|
+
* e.g., "rulebricks.example.com" -> "example.com"
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractBaseDomain(fullDomain: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Validates that a domain is properly formatted
|
|
12
|
+
*/
|
|
13
|
+
export declare function isValidDomainFormat(domain: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Checks if the base domain has active DNS records
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateBaseDomain(fullDomain: string): Promise<{
|
|
18
|
+
valid: boolean;
|
|
19
|
+
baseDomain: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a specific hostname resolves to a given target
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkDNSRecord(hostname: string, expectedTarget?: string): Promise<{
|
|
26
|
+
resolved: boolean;
|
|
27
|
+
records: string[];
|
|
28
|
+
matchesTarget: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Validates SMTP configuration format
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateSMTPConfig(config: {
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
user: string;
|
|
37
|
+
pass: string;
|
|
38
|
+
from: string;
|
|
39
|
+
fromName: string;
|
|
40
|
+
}): {
|
|
41
|
+
valid: boolean;
|
|
42
|
+
errors: string[];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Generates a secure random string for secrets
|
|
46
|
+
*/
|
|
47
|
+
export declare function generateSecureSecret(length?: number): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import dns from 'dns';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const resolveDns = promisify(dns.resolve);
|
|
4
|
+
const resolve4 = promisify(dns.resolve4);
|
|
5
|
+
const resolveCname = promisify(dns.resolveCname);
|
|
6
|
+
/**
|
|
7
|
+
* Validates an email address using a comprehensive regex
|
|
8
|
+
*/
|
|
9
|
+
export function isValidEmail(email) {
|
|
10
|
+
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
11
|
+
return emailRegex.test(email) && email.length <= 254;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Extracts the base domain from a full domain name
|
|
15
|
+
* e.g., "rulebricks.example.com" -> "example.com"
|
|
16
|
+
*/
|
|
17
|
+
export function extractBaseDomain(fullDomain) {
|
|
18
|
+
const parts = fullDomain.toLowerCase().split('.');
|
|
19
|
+
// Handle common multi-part TLDs
|
|
20
|
+
const multiPartTlds = ['co.uk', 'com.au', 'co.nz', 'co.jp', 'com.br', 'co.za'];
|
|
21
|
+
if (parts.length >= 3) {
|
|
22
|
+
const lastTwo = `${parts[parts.length - 2]}.${parts[parts.length - 1]}`;
|
|
23
|
+
if (multiPartTlds.includes(lastTwo)) {
|
|
24
|
+
// For multi-part TLDs, take the last 3 parts
|
|
25
|
+
return parts.slice(-3).join('.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// For standard TLDs, take the last 2 parts
|
|
29
|
+
if (parts.length >= 2) {
|
|
30
|
+
return parts.slice(-2).join('.');
|
|
31
|
+
}
|
|
32
|
+
return fullDomain;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validates that a domain is properly formatted
|
|
36
|
+
*/
|
|
37
|
+
export function isValidDomainFormat(domain) {
|
|
38
|
+
// Basic domain format validation
|
|
39
|
+
const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
40
|
+
return domainRegex.test(domain);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Checks if the base domain has active DNS records
|
|
44
|
+
*/
|
|
45
|
+
export async function validateBaseDomain(fullDomain) {
|
|
46
|
+
if (!isValidDomainFormat(fullDomain)) {
|
|
47
|
+
return {
|
|
48
|
+
valid: false,
|
|
49
|
+
baseDomain: '',
|
|
50
|
+
error: 'Invalid domain format'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const baseDomain = extractBaseDomain(fullDomain);
|
|
54
|
+
try {
|
|
55
|
+
// Try to resolve the base domain
|
|
56
|
+
// First try A records
|
|
57
|
+
try {
|
|
58
|
+
await resolve4(baseDomain);
|
|
59
|
+
return { valid: true, baseDomain };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// If A record fails, try any record type
|
|
63
|
+
await resolveDns(baseDomain, 'ANY');
|
|
64
|
+
return { valid: true, baseDomain };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const error = err;
|
|
69
|
+
if (error.code === 'ENOTFOUND') {
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
baseDomain,
|
|
73
|
+
error: `Base domain "${baseDomain}" does not exist or has no DNS records`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (error.code === 'ENODATA') {
|
|
77
|
+
// Domain exists but no records of the requested type
|
|
78
|
+
// This is actually OK for our purposes
|
|
79
|
+
return { valid: true, baseDomain };
|
|
80
|
+
}
|
|
81
|
+
// For other errors, assume the domain might be valid
|
|
82
|
+
// (could be temporary DNS issues)
|
|
83
|
+
return { valid: true, baseDomain };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Checks if a specific hostname resolves to a given target
|
|
88
|
+
*/
|
|
89
|
+
export async function checkDNSRecord(hostname, expectedTarget) {
|
|
90
|
+
try {
|
|
91
|
+
// Try A record first
|
|
92
|
+
try {
|
|
93
|
+
const aRecords = await resolve4(hostname);
|
|
94
|
+
const matchesTarget = expectedTarget
|
|
95
|
+
? aRecords.some(r => r === expectedTarget)
|
|
96
|
+
: true;
|
|
97
|
+
return {
|
|
98
|
+
resolved: true,
|
|
99
|
+
records: aRecords,
|
|
100
|
+
matchesTarget
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Try CNAME
|
|
105
|
+
const cnameRecords = await resolveCname(hostname);
|
|
106
|
+
const matchesTarget = expectedTarget
|
|
107
|
+
? cnameRecords.some(r => r === expectedTarget || r.endsWith(expectedTarget))
|
|
108
|
+
: true;
|
|
109
|
+
return {
|
|
110
|
+
resolved: true,
|
|
111
|
+
records: cnameRecords,
|
|
112
|
+
matchesTarget
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return {
|
|
118
|
+
resolved: false,
|
|
119
|
+
records: [],
|
|
120
|
+
matchesTarget: false
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validates SMTP configuration format
|
|
126
|
+
*/
|
|
127
|
+
export function validateSMTPConfig(config) {
|
|
128
|
+
const errors = [];
|
|
129
|
+
if (!config.host || config.host.length < 3) {
|
|
130
|
+
errors.push('SMTP host is required');
|
|
131
|
+
}
|
|
132
|
+
if (!config.port || config.port < 1 || config.port > 65535) {
|
|
133
|
+
errors.push('SMTP port must be between 1 and 65535');
|
|
134
|
+
}
|
|
135
|
+
if (!config.user) {
|
|
136
|
+
errors.push('SMTP username is required');
|
|
137
|
+
}
|
|
138
|
+
if (!config.pass) {
|
|
139
|
+
errors.push('SMTP password is required');
|
|
140
|
+
}
|
|
141
|
+
if (!config.from || !isValidEmail(config.from)) {
|
|
142
|
+
errors.push('Valid SMTP from address is required');
|
|
143
|
+
}
|
|
144
|
+
if (!config.fromName || config.fromName.length < 1) {
|
|
145
|
+
errors.push('SMTP from name is required');
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
valid: errors.length === 0,
|
|
149
|
+
errors
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Generates a secure random string for secrets
|
|
154
|
+
*/
|
|
155
|
+
export function generateSecureSecret(length = 32) {
|
|
156
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
157
|
+
let result = '';
|
|
158
|
+
const randomValues = new Uint8Array(length);
|
|
159
|
+
crypto.getRandomValues(randomValues);
|
|
160
|
+
for (let i = 0; i < length; i++) {
|
|
161
|
+
result += chars[randomValues[i] % chars.length];
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|