@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,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { useWizard } from '../WizardContext.js';
|
|
5
|
+
import { BorderBox, useTheme } from '../../common/index.js';
|
|
6
|
+
import { LOGGING_SINK_INFO } from '../../../types/index.js';
|
|
7
|
+
// Five features: AI, SSO, Monitoring, External Logging, Custom Emails
|
|
8
|
+
// External DNS is handled in Domain step, not here
|
|
9
|
+
const FEATURES = [
|
|
10
|
+
{
|
|
11
|
+
id: 'ai',
|
|
12
|
+
label: 'AI Features',
|
|
13
|
+
description: 'Enable AI-powered rule generation (requires OpenAI API key)',
|
|
14
|
+
requiresConfig: true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'sso',
|
|
18
|
+
label: 'Single Sign-On',
|
|
19
|
+
description: 'Enable SSO via OIDC provider (Azure AD, Google, Okta, etc.)',
|
|
20
|
+
requiresConfig: true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'monitoring',
|
|
24
|
+
label: 'Monitoring',
|
|
25
|
+
description: 'Enable Prometheus metrics collection. Optionally send to external system.',
|
|
26
|
+
requiresConfig: true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'logging',
|
|
30
|
+
label: 'External Logging',
|
|
31
|
+
description: 'Forward logs to cloud storage or logging platforms (Datadog, Splunk, etc.)',
|
|
32
|
+
requiresConfig: true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'customEmails',
|
|
36
|
+
label: 'Custom Email Templates',
|
|
37
|
+
description: 'Use custom HTML templates for Supabase auth emails (invite, confirm, reset, etc.)',
|
|
38
|
+
requiresConfig: true
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
export function FeaturesStep({ onComplete, onBack }) {
|
|
42
|
+
const { state, dispatch } = useWizard();
|
|
43
|
+
const { colors } = useTheme();
|
|
44
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
45
|
+
const enabledFeatures = {
|
|
46
|
+
ai: state.aiEnabled,
|
|
47
|
+
sso: state.ssoEnabled,
|
|
48
|
+
monitoring: state.monitoringEnabled,
|
|
49
|
+
logging: state.loggingSink !== 'console', // External logging is "enabled" if not console-only (includes 'pending')
|
|
50
|
+
customEmails: state.customEmailsEnabled
|
|
51
|
+
};
|
|
52
|
+
useInput((input, key) => {
|
|
53
|
+
if (key.escape) {
|
|
54
|
+
onBack();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.upArrow) {
|
|
58
|
+
setCurrentIndex(i => Math.max(0, i - 1));
|
|
59
|
+
}
|
|
60
|
+
else if (key.downArrow) {
|
|
61
|
+
setCurrentIndex(i => Math.min(FEATURES.length, i + 1));
|
|
62
|
+
}
|
|
63
|
+
else if (input === ' ' || input === 'x') {
|
|
64
|
+
if (currentIndex < FEATURES.length) {
|
|
65
|
+
const feature = FEATURES[currentIndex];
|
|
66
|
+
toggleFeature(feature.id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (key.return) {
|
|
70
|
+
if (currentIndex === FEATURES.length) {
|
|
71
|
+
// "Continue" button
|
|
72
|
+
onComplete();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Toggle the feature on Enter too
|
|
76
|
+
const feature = FEATURES[currentIndex];
|
|
77
|
+
toggleFeature(feature.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const toggleFeature = (id) => {
|
|
82
|
+
switch (id) {
|
|
83
|
+
case 'ai':
|
|
84
|
+
dispatch({ type: 'SET_AI_ENABLED', enabled: !state.aiEnabled });
|
|
85
|
+
break;
|
|
86
|
+
case 'sso':
|
|
87
|
+
dispatch({ type: 'SET_SSO_ENABLED', enabled: !state.ssoEnabled });
|
|
88
|
+
break;
|
|
89
|
+
case 'monitoring':
|
|
90
|
+
dispatch({ type: 'SET_MONITORING', enabled: !state.monitoringEnabled });
|
|
91
|
+
break;
|
|
92
|
+
case 'logging':
|
|
93
|
+
// Toggle between console-only and needing to pick a sink
|
|
94
|
+
// If currently console, mark as pending - FeatureConfigStep will handle sink selection
|
|
95
|
+
if (state.loggingSink === 'console') {
|
|
96
|
+
dispatch({ type: 'SET_LOGGING_SINK', sink: 'pending' });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
dispatch({ type: 'SET_LOGGING_SINK', sink: 'console' });
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case 'customEmails':
|
|
103
|
+
dispatch({ type: 'SET_CUSTOM_EMAILS_ENABLED', enabled: !state.customEmailsEnabled });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// Get current logging sink description
|
|
108
|
+
const getLoggingStatusText = () => {
|
|
109
|
+
if (state.loggingSink === 'console') {
|
|
110
|
+
return 'Console only (default)';
|
|
111
|
+
}
|
|
112
|
+
return LOGGING_SINK_INFO[state.loggingSink]?.name || state.loggingSink;
|
|
113
|
+
};
|
|
114
|
+
return (_jsxs(BorderBox, { title: "Optional Features", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Select features to enable:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Use arrows to navigate, space/enter to toggle" })] }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [FEATURES.map((feature, index) => {
|
|
115
|
+
const isSelected = index === currentIndex;
|
|
116
|
+
const isEnabled = enabledFeatures[feature.id];
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: isSelected ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? colors.accent : undefined, children: isSelected ? '❯ ' : ' ' }), _jsx(Text, { color: isEnabled ? colors.success : colors.muted, children: isEnabled ? '[✓]' : '[ ]' }), _jsxs(Text, { color: isSelected ? colors.accent : undefined, children: [' ', feature.label] }), feature.id === 'logging' && state.loggingSink !== 'console' && state.loggingSink !== 'pending' && (_jsxs(Text, { color: colors.accent, children: [" \u2192 ", getLoggingStatusText()] }))] }), isSelected && (_jsx(Box, { marginLeft: 6, children: _jsx(Text, { color: "gray", dimColor: true, children: feature.description }) }))] }, feature.id));
|
|
118
|
+
}), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: currentIndex === FEATURES.length ? colors.accent : colors.muted, children: currentIndex === FEATURES.length ? '❯ ' : ' ' }), _jsx(Text, { color: currentIndex === FEATURES.length ? colors.success : colors.muted, bold: currentIndex === FEATURES.length, children: "[Continue \u2192]" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", dimColor: true, children: "Space/Enter to toggle \u2022 \u2191/\u2193 to navigate \u2022 Esc to go back" }), (state.aiEnabled || state.ssoEnabled || state.monitoringEnabled || state.loggingSink !== 'console') && (_jsx(Text, { color: "yellow", dimColor: true, children: "Note: Enabled features will be configured in the next step" }))] })] }));
|
|
119
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { useWizard } from '../WizardContext.js';
|
|
6
|
+
import { BorderBox, useTheme } from '../../common/index.js';
|
|
7
|
+
import { TIER_CONFIGS, DNS_PROVIDER_NAMES, isSupportedDnsProvider } from '../../../types/index.js';
|
|
8
|
+
export function ReviewStep({ onComplete, onBack }) {
|
|
9
|
+
const { state, dispatch } = useWizard();
|
|
10
|
+
const { colors } = useTheme();
|
|
11
|
+
const [editingName, setEditingName] = useState(!state.name);
|
|
12
|
+
const [name, setName] = useState(state.name || '');
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (editingName)
|
|
16
|
+
return;
|
|
17
|
+
if (key.escape) {
|
|
18
|
+
onBack();
|
|
19
|
+
}
|
|
20
|
+
else if (key.return) {
|
|
21
|
+
if (state.name) {
|
|
22
|
+
onComplete();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (input === 'e') {
|
|
26
|
+
setEditingName(true);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const handleNameSubmit = () => {
|
|
30
|
+
if (!name) {
|
|
31
|
+
setError('Name is required');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) {
|
|
35
|
+
setError('Name must be lowercase letters, numbers, and hyphens');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (name.length > 63) {
|
|
39
|
+
setError('Name must be 63 characters or less');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setError(null);
|
|
43
|
+
dispatch({ type: 'SET_NAME', name });
|
|
44
|
+
setEditingName(false);
|
|
45
|
+
};
|
|
46
|
+
const tierConfig = state.tier ? TIER_CONFIGS[state.tier] : null;
|
|
47
|
+
const externalDnsEnabled = state.dnsAutoManage && isSupportedDnsProvider(state.dnsProvider);
|
|
48
|
+
if (editingName) {
|
|
49
|
+
return (_jsx(BorderBox, { title: "Deployment Name", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter a name for this deployment:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Lowercase letters, numbers, and hyphens only" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: name, onChange: setName, onSubmit: handleNameSubmit, placeholder: "my-deployment" }) }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) }))] }) }));
|
|
50
|
+
}
|
|
51
|
+
// Helper to render a config row
|
|
52
|
+
const ConfigRow = ({ label, value, valueColor }) => (_jsxs(Box, { children: [_jsx(Box, { width: 16, children: _jsx(Text, { color: colors.muted, children: label }) }), _jsx(Text, { color: valueColor || colors.accent, children: value })] }));
|
|
53
|
+
// Helper to render a section header
|
|
54
|
+
const SectionHeader = ({ title }) => (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { bold: true, color: colors.accent, children: ["\u2500\u2500 ", title, " \u2500\u2500"] }) }));
|
|
55
|
+
return (_jsxs(BorderBox, { title: "Review Configuration", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(SectionHeader, { title: "Deployment" }), _jsx(ConfigRow, { label: "Name", value: state.name }), state.appVersion && (_jsx(ConfigRow, { label: "App Version", value: state.appVersion })), _jsx(SectionHeader, { title: "Infrastructure" }), _jsx(ConfigRow, { label: "Mode", value: state.infrastructureMode === 'provision' ? 'Provision new cluster' : 'Use existing cluster' }), state.provider && (_jsx(ConfigRow, { label: "Provider", value: state.provider.toUpperCase() })), state.region && (_jsx(ConfigRow, { label: "Region", value: state.region })), _jsx(SectionHeader, { title: "Domain & DNS" }), _jsx(ConfigRow, { label: "Domain", value: state.domain }), _jsx(ConfigRow, { label: "Admin Email", value: state.adminEmail }), _jsx(ConfigRow, { label: "TLS Email", value: state.tlsEmail }), _jsxs(Box, { children: [_jsx(Box, { width: 16, children: _jsx(Text, { color: colors.muted, children: "DNS" }) }), _jsx(Text, { color: colors.accent, children: DNS_PROVIDER_NAMES[state.dnsProvider] }), externalDnsEnabled && _jsx(Text, { color: colors.success, children: " (auto)" })] }), _jsx(SectionHeader, { title: "SMTP" }), _jsx(ConfigRow, { label: "Host", value: `${state.smtpHost}:${state.smtpPort}` }), _jsx(ConfigRow, { label: "From", value: `${state.smtpFromName} <${state.smtpFrom}>` }), _jsx(SectionHeader, { title: "Database" }), _jsx(ConfigRow, { label: "Type", value: state.databaseType === 'supabase-cloud' ? 'Supabase Cloud' : 'Self-hosted' }), _jsx(SectionHeader, { title: "Performance" }), _jsxs(Box, { children: [_jsx(Box, { width: 16, children: _jsx(Text, { color: colors.muted, children: "Tier" }) }), _jsxs(Text, { color: colors.accent, bold: true, children: [state.tier?.charAt(0).toUpperCase(), state.tier?.slice(1)] }), tierConfig && _jsxs(Text, { color: colors.muted, children: [" (", tierConfig.throughput, ")"] })] }), _jsx(SectionHeader, { title: "Features" }), _jsxs(Box, { children: [_jsxs(Text, { color: state.aiEnabled ? colors.success : colors.muted, children: [state.aiEnabled ? '✓' : '○', " AI"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: state.ssoEnabled ? colors.success : colors.muted, children: [state.ssoEnabled ? '✓' : '○', " SSO"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: state.monitoringEnabled ? colors.success : colors.muted, children: [state.monitoringEnabled ? '✓' : '○', " Monitoring"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: state.loggingSink !== 'console' ? colors.success : colors.muted, children: [state.loggingSink !== 'console' ? '✓' : '○', " Logging"] })] }), _jsx(SectionHeader, { title: "License" }), _jsx(ConfigRow, { label: "Key", value: `${state.licenseKey?.substring(0, 12)}...` })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.success, bold: true, children: "Press Enter to save this configuration" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "e to edit name \u2022 Esc to go back" })] })] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import SelectInput from "ink-select-input";
|
|
5
|
+
import TextInput from "ink-text-input";
|
|
6
|
+
import { useWizard } from "../WizardContext.js";
|
|
7
|
+
import { BorderBox, useTheme } from "../../common/index.js";
|
|
8
|
+
import { SMTP_PROVIDERS } from "../../../types/index.js";
|
|
9
|
+
import { isValidEmail } from "../../../lib/validation.js";
|
|
10
|
+
const PROVIDER_ITEMS = [
|
|
11
|
+
{ label: "AWS SES", value: "aws-ses" },
|
|
12
|
+
{ label: "SendGrid", value: "sendgrid" },
|
|
13
|
+
{ label: "Resend", value: "resend" },
|
|
14
|
+
{ label: "Mailgun", value: "mailgun" },
|
|
15
|
+
{ label: "Postmark", value: "postmark" },
|
|
16
|
+
{ label: "Mailtrap (testing)", value: "mailtrap" },
|
|
17
|
+
{ label: "Custom SMTP Server", value: "custom" },
|
|
18
|
+
];
|
|
19
|
+
// Detect which provider preset matches a given SMTP host
|
|
20
|
+
function detectProviderFromHost(host) {
|
|
21
|
+
if (!host)
|
|
22
|
+
return null;
|
|
23
|
+
const hostLower = host.toLowerCase();
|
|
24
|
+
if (hostLower.includes("amazonaws.com") || hostLower.includes("ses"))
|
|
25
|
+
return "aws-ses";
|
|
26
|
+
if (hostLower.includes("sendgrid"))
|
|
27
|
+
return "sendgrid";
|
|
28
|
+
if (hostLower.includes("resend"))
|
|
29
|
+
return "resend";
|
|
30
|
+
if (hostLower.includes("mailgun"))
|
|
31
|
+
return "mailgun";
|
|
32
|
+
if (hostLower.includes("postmark"))
|
|
33
|
+
return "postmark";
|
|
34
|
+
if (hostLower.includes("mailtrap"))
|
|
35
|
+
return "mailtrap";
|
|
36
|
+
return "custom";
|
|
37
|
+
}
|
|
38
|
+
export function SMTPStep({ onComplete, onBack }) {
|
|
39
|
+
const { state, dispatch } = useWizard();
|
|
40
|
+
const { colors } = useTheme();
|
|
41
|
+
// Determine initial step based on whether we have pre-populated SMTP settings
|
|
42
|
+
const getInitialSubStep = () => {
|
|
43
|
+
// If we have pre-populated SMTP host from profile, skip provider selection
|
|
44
|
+
if (state.smtpHost && state.smtpUser) {
|
|
45
|
+
return "pass"; // Start at password entry (most sensitive, likely needs re-entry)
|
|
46
|
+
}
|
|
47
|
+
if (state.smtpHost) {
|
|
48
|
+
return "user"; // Have host but no user
|
|
49
|
+
}
|
|
50
|
+
return "provider"; // Fresh start
|
|
51
|
+
};
|
|
52
|
+
const [subStep, setSubStep] = useState(getInitialSubStep);
|
|
53
|
+
const [provider, setProvider] = useState(detectProviderFromHost(state.smtpHost) || "");
|
|
54
|
+
const [host, setHost] = useState(state.smtpHost || "");
|
|
55
|
+
const [port, setPort] = useState(state.smtpPort?.toString() || "587");
|
|
56
|
+
const [user, setUser] = useState(state.smtpUser || "");
|
|
57
|
+
const [pass, setPass] = useState(state.smtpPass || "");
|
|
58
|
+
const [from, setFrom] = useState(state.smtpFrom || "");
|
|
59
|
+
const [fromName, setFromName] = useState(state.smtpFromName || "Rulebricks");
|
|
60
|
+
const [error, setError] = useState(null);
|
|
61
|
+
useInput((input, key) => {
|
|
62
|
+
if (key.escape) {
|
|
63
|
+
setError(null);
|
|
64
|
+
if (subStep === "provider") {
|
|
65
|
+
onBack();
|
|
66
|
+
}
|
|
67
|
+
else if (subStep === "host") {
|
|
68
|
+
setSubStep("provider");
|
|
69
|
+
}
|
|
70
|
+
else if (subStep === "port") {
|
|
71
|
+
setSubStep("host");
|
|
72
|
+
}
|
|
73
|
+
else if (subStep === "user") {
|
|
74
|
+
// If we started with pre-populated settings, going back goes to provider
|
|
75
|
+
if (getInitialSubStep() === "user") {
|
|
76
|
+
setSubStep("provider");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
setSubStep("port");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (subStep === "pass") {
|
|
83
|
+
// If we started with pre-populated settings, going back from pass goes to user
|
|
84
|
+
setSubStep("user");
|
|
85
|
+
}
|
|
86
|
+
else if (subStep === "from") {
|
|
87
|
+
setSubStep("pass");
|
|
88
|
+
}
|
|
89
|
+
else if (subStep === "fromName") {
|
|
90
|
+
setSubStep("from");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const handleProviderSelect = (item) => {
|
|
95
|
+
setProvider(item.value);
|
|
96
|
+
const providerConfig = SMTP_PROVIDERS[item.value];
|
|
97
|
+
if (providerConfig) {
|
|
98
|
+
setHost(providerConfig.host);
|
|
99
|
+
setPort(providerConfig.port.toString());
|
|
100
|
+
// Pre-fill user if provider has a default (like Resend)
|
|
101
|
+
if (providerConfig.user) {
|
|
102
|
+
setUser(providerConfig.user);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (item.value === "custom") {
|
|
106
|
+
setSubStep("host");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
setSubStep("user");
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const handleHostSubmit = () => {
|
|
113
|
+
if (!host) {
|
|
114
|
+
setError("SMTP host is required");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setError(null);
|
|
118
|
+
setSubStep("port");
|
|
119
|
+
};
|
|
120
|
+
const handlePortSubmit = () => {
|
|
121
|
+
const portNum = parseInt(port, 10);
|
|
122
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
123
|
+
setError("Port must be between 1 and 65535");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setError(null);
|
|
127
|
+
setSubStep("user");
|
|
128
|
+
};
|
|
129
|
+
const handleUserSubmit = () => {
|
|
130
|
+
if (!user) {
|
|
131
|
+
setError("SMTP username is required");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setError(null);
|
|
135
|
+
setSubStep("pass");
|
|
136
|
+
};
|
|
137
|
+
const handlePassSubmit = () => {
|
|
138
|
+
if (!pass) {
|
|
139
|
+
setError("SMTP password is required");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
setError(null);
|
|
143
|
+
setSubStep("from");
|
|
144
|
+
};
|
|
145
|
+
const handleFromSubmit = () => {
|
|
146
|
+
if (!from) {
|
|
147
|
+
setError("From address is required");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!isValidEmail(from)) {
|
|
151
|
+
setError("Invalid email format");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
setError(null);
|
|
155
|
+
setSubStep("fromName");
|
|
156
|
+
};
|
|
157
|
+
const handleFromNameSubmit = () => {
|
|
158
|
+
if (!fromName) {
|
|
159
|
+
setError("From name is required");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setError(null);
|
|
163
|
+
dispatch({
|
|
164
|
+
type: "SET_SMTP",
|
|
165
|
+
config: {
|
|
166
|
+
smtpHost: host,
|
|
167
|
+
smtpPort: parseInt(port, 10),
|
|
168
|
+
smtpUser: user,
|
|
169
|
+
smtpPass: pass,
|
|
170
|
+
smtpFrom: from,
|
|
171
|
+
smtpFromName: fromName,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
onComplete();
|
|
175
|
+
};
|
|
176
|
+
const getCompletedFields = () => {
|
|
177
|
+
const fields = [];
|
|
178
|
+
if (host)
|
|
179
|
+
fields.push({ label: "Host", value: host });
|
|
180
|
+
if (port && subStep !== "port")
|
|
181
|
+
fields.push({ label: "Port", value: port });
|
|
182
|
+
if (user && subStep !== "user")
|
|
183
|
+
fields.push({ label: "User", value: user });
|
|
184
|
+
if (pass && subStep !== "pass")
|
|
185
|
+
fields.push({ label: "Password", value: "••••••••" });
|
|
186
|
+
if (from && subStep !== "from")
|
|
187
|
+
fields.push({ label: "From", value: from });
|
|
188
|
+
return fields;
|
|
189
|
+
};
|
|
190
|
+
return (_jsxs(BorderBox, { title: "Email (SMTP)", children: [_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Configure SMTP for user invitations, password resets, and notifications" }) }), subStep === "provider" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Select your email provider:" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: PROVIDER_ITEMS, onSelect: handleProviderSelect, itemComponent: ({ isSelected, label }) => (_jsx(Text, { color: isSelected ? colors.accent : undefined, children: label })) }) })] })), subStep === "host" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter SMTP server hostname:" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: host, onChange: setHost, onSubmit: handleHostSubmit, placeholder: "smtp.example.com" }) })] })), subStep === "port" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter SMTP port:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Common ports: 25, 465 (SSL), 587 (TLS), 2525" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: port, onChange: setPort, onSubmit: handlePortSubmit, placeholder: "587" }) }), getCompletedFields().map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", f.label, ": ", f.value] })] }, f.label)))] })), subStep === "user" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter SMTP username:" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: user, onChange: setUser, onSubmit: handleUserSubmit, placeholder: "smtp_username" }) }), getCompletedFields().map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", f.label, ": ", f.value] })] }, f.label)))] })), subStep === "pass" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter SMTP password:" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: pass, onChange: setPass, onSubmit: handlePassSubmit, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", mask: "*" }) }), getCompletedFields().map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", f.label, ": ", f.value] })] }, f.label)))] })), subStep === "from" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter sender email address:" }), _jsx(Text, { color: "gray", dimColor: true, children: "This must be verified with your email provider" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: from, onChange: setFrom, onSubmit: handleFromSubmit, placeholder: "no-reply@yourdomain.com" }) }), getCompletedFields().map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", f.label, ": ", f.value] })] }, f.label)))] })), subStep === "fromName" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter sender display name:" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: fromName, onChange: setFromName, onSubmit: handleFromNameSubmit, placeholder: "Rulebricks" }) }), getCompletedFields().map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", f.label, ": ", f.value] })] }, f.label)))] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
|
|
191
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { useWizard } from "../WizardContext.js";
|
|
6
|
+
import { BorderBox } from "../../common/index.js";
|
|
7
|
+
import { generateSecureSecret } from "../../../lib/validation.js";
|
|
8
|
+
// Fixed JWT secret - users can edit this in the raw config if needed
|
|
9
|
+
const JWT_SECRET = "your-super-secret-jwt-token-with-at-least-32-characters-long";
|
|
10
|
+
export function SupabaseCredentialsStep({ onComplete, onBack, }) {
|
|
11
|
+
const { state, dispatch } = useWizard();
|
|
12
|
+
// Generate defaults if not already set
|
|
13
|
+
const defaultDbPass = state.supabaseDbPassword || generateSecureSecret(24);
|
|
14
|
+
const defaultDashboardPass = state.supabaseDashboardPass || generateSecureSecret(16);
|
|
15
|
+
const [subStep, setSubStep] = useState("db-password");
|
|
16
|
+
const [dbPassword, setDbPassword] = useState(defaultDbPass);
|
|
17
|
+
const [dashboardUser, setDashboardUser] = useState(state.supabaseDashboardUser || "supabase");
|
|
18
|
+
const [dashboardPass, setDashboardPass] = useState(defaultDashboardPass);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
useInput((input, key) => {
|
|
21
|
+
if (key.escape) {
|
|
22
|
+
setError(null);
|
|
23
|
+
if (subStep === "db-password") {
|
|
24
|
+
onBack();
|
|
25
|
+
}
|
|
26
|
+
else if (subStep === "dashboard-user") {
|
|
27
|
+
setSubStep("db-password");
|
|
28
|
+
}
|
|
29
|
+
else if (subStep === "dashboard-pass") {
|
|
30
|
+
setSubStep("dashboard-user");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const handleDbPasswordSubmit = () => {
|
|
35
|
+
if (!dbPassword || dbPassword.length < 8) {
|
|
36
|
+
setError("Database password must be at least 8 characters");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setError(null);
|
|
40
|
+
setSubStep("dashboard-user");
|
|
41
|
+
};
|
|
42
|
+
const handleDashboardUserSubmit = () => {
|
|
43
|
+
if (!dashboardUser) {
|
|
44
|
+
setError("Dashboard username is required");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setError(null);
|
|
48
|
+
setSubStep("dashboard-pass");
|
|
49
|
+
};
|
|
50
|
+
const handleDashboardPassSubmit = () => {
|
|
51
|
+
if (!dashboardPass || dashboardPass.length < 8) {
|
|
52
|
+
setError("Dashboard password must be at least 8 characters");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setError(null);
|
|
56
|
+
dispatch({
|
|
57
|
+
type: "SET_SUPABASE_SELF_HOSTED",
|
|
58
|
+
config: {
|
|
59
|
+
supabaseJwtSecret: JWT_SECRET,
|
|
60
|
+
supabaseDbPassword: dbPassword,
|
|
61
|
+
supabaseDashboardUser: dashboardUser,
|
|
62
|
+
supabaseDashboardPass: dashboardPass,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
onComplete();
|
|
66
|
+
};
|
|
67
|
+
const regenerateSecret = (field) => {
|
|
68
|
+
if (field === "db") {
|
|
69
|
+
setDbPassword(generateSecureSecret(24));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
setDashboardPass(generateSecureSecret(16));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return (_jsxs(BorderBox, { title: "Supabase Credentials", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "gray", dimColor: true, children: "Configure credentials for your self-hosted Supabase instance" }), _jsx(Text, { color: "yellow", dimColor: true, children: "\u26A0 Save these credentials securely - you'll need them to access Supabase" })] }), subStep === "db-password" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Database Password:" }), _jsx(Text, { color: "gray", dimColor: true, children: "PostgreSQL database password" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Default (press Enter to use): ", defaultDbPass] }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: dbPassword, onChange: setDbPassword, onSubmit: handleDbPasswordSubmit, placeholder: "Database password (min 8 chars)", mask: "*" }) })] })), subStep === "dashboard-user" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Supabase Studio Username:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Username for accessing the Supabase dashboard" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: dashboardUser, onChange: setDashboardUser, onSubmit: handleDashboardUserSubmit, placeholder: "supabase" }) }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsx(Text, { color: "gray", children: " Database password configured" })] }) })] })), subStep === "dashboard-pass" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Supabase Studio Password:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Password for accessing the Supabase dashboard" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Default (press Enter to use): ", defaultDashboardPass] }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: dashboardPass, onChange: setDashboardPass, onSubmit: handleDashboardPassSubmit, placeholder: "Dashboard password (min 8 chars)", mask: "*" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsx(Text, { color: "gray", children: " Database password configured" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" Dashboard user: ", dashboardUser] })] })] })] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
|
|
76
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { useWizard } from '../WizardContext.js';
|
|
5
|
+
import { BorderBox, useTheme } from '../../common/index.js';
|
|
6
|
+
import { TIER_CONFIGS } from '../../../types/index.js';
|
|
7
|
+
export function TierStep({ onComplete, onBack }) {
|
|
8
|
+
const { dispatch } = useWizard();
|
|
9
|
+
const { colors } = useTheme();
|
|
10
|
+
useInput((input, key) => {
|
|
11
|
+
if (key.escape) {
|
|
12
|
+
onBack();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
const items = Object.entries(TIER_CONFIGS).map(([tier, config]) => ({
|
|
16
|
+
label: tier.charAt(0).toUpperCase() + tier.slice(1),
|
|
17
|
+
value: tier,
|
|
18
|
+
config
|
|
19
|
+
}));
|
|
20
|
+
const handleSelect = (item) => {
|
|
21
|
+
dispatch({ type: 'SET_TIER', tier: item.value });
|
|
22
|
+
onComplete();
|
|
23
|
+
};
|
|
24
|
+
return (_jsxs(BorderBox, { title: "Performance Tier", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Select your deployment size:" }), _jsx(Text, { color: "gray", dimColor: true, children: "This determines the cluster resources and scaling limits" })] }), _jsx(SelectInput, { items: items, onSelect: handleSelect, indicatorComponent: () => null, itemComponent: ({ isSelected, label }) => {
|
|
25
|
+
const currentItem = items.find(i => i.label === label) || items[0];
|
|
26
|
+
const config = currentItem.config;
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: isSelected ? 1 : 0, children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, bold: isSelected, children: [isSelected ? '❯ ' : ' ', currentItem.label, _jsxs(Text, { color: "gray", children: [" - ", config.description] })] }), isSelected && (_jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [_jsxs(Text, { color: "gray", children: ["Throughput: ", config.throughput] }), _jsxs(Text, { color: "gray", children: ["Nodes: ", config.nodes.min, "-", config.nodes.max, " \u2022 ", config.resources] }), _jsxs(Text, { color: "gray", children: ["HPS Workers: ", config.hpsWorkerReplicas.min, "-", config.hpsWorkerReplicas.max] })] }))] }));
|
|
28
|
+
} }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Esc to go back \u2022 Enter to select" }) })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import SelectInput from "ink-select-input";
|
|
5
|
+
import TextInput from "ink-text-input";
|
|
6
|
+
import { useWizard } from "../WizardContext.js";
|
|
7
|
+
import { BorderBox, useTheme } from "../../common/index.js";
|
|
8
|
+
import { Spinner } from "../../common/Spinner.js";
|
|
9
|
+
import { fetchAppVersions, formatDate } from "../../../lib/versions.js";
|
|
10
|
+
import { CHANGELOG_URL } from "../../../types/index.js";
|
|
11
|
+
import { formatVersionDisplay } from "../../../lib/dockerHub.js";
|
|
12
|
+
export function VersionStep({ onComplete, onBack }) {
|
|
13
|
+
const { state, dispatch } = useWizard();
|
|
14
|
+
const { colors } = useTheme();
|
|
15
|
+
const [subStep, setSubStep] = useState("license");
|
|
16
|
+
const [licenseKey, setLicenseKey] = useState(state.licenseKey || "");
|
|
17
|
+
const [versions, setVersions] = useState([]);
|
|
18
|
+
const [loadError, setLoadError] = useState(null);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
useInput((input, key) => {
|
|
21
|
+
if (key.escape) {
|
|
22
|
+
setError(null);
|
|
23
|
+
if (subStep === "license") {
|
|
24
|
+
onBack();
|
|
25
|
+
}
|
|
26
|
+
else if (subStep === "version") {
|
|
27
|
+
setSubStep("license");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const loadVersions = async () => {
|
|
32
|
+
setSubStep("loading-versions");
|
|
33
|
+
setLoadError(null);
|
|
34
|
+
try {
|
|
35
|
+
const appVersions = await fetchAppVersions(licenseKey);
|
|
36
|
+
setVersions(appVersions);
|
|
37
|
+
if (appVersions.length === 0) {
|
|
38
|
+
setLoadError("No versions found. Using latest.");
|
|
39
|
+
}
|
|
40
|
+
setSubStep("version");
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const message = err instanceof Error ? err.message : "Failed to fetch versions";
|
|
44
|
+
// Check if it's an auth error
|
|
45
|
+
if (message.includes("authentication") ||
|
|
46
|
+
message.includes("Invalid license")) {
|
|
47
|
+
setError("Invalid license key - please check your key and try again");
|
|
48
|
+
setSubStep("license");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setLoadError(`${message}. Will use latest version.`);
|
|
52
|
+
setSubStep("version");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const handleLicenseSubmit = () => {
|
|
56
|
+
if (!licenseKey) {
|
|
57
|
+
setError("License key is required");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (licenseKey.length < 10) {
|
|
61
|
+
setError("Invalid license key format");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setError(null);
|
|
65
|
+
dispatch({ type: "SET_LICENSE_KEY", key: licenseKey });
|
|
66
|
+
loadVersions();
|
|
67
|
+
};
|
|
68
|
+
const handleVersionSelect = (item) => {
|
|
69
|
+
dispatch({
|
|
70
|
+
type: "SET_APP_VERSION",
|
|
71
|
+
appVersion: item.value,
|
|
72
|
+
hpsVersion: item.hpsVersion || item.value,
|
|
73
|
+
});
|
|
74
|
+
onComplete();
|
|
75
|
+
};
|
|
76
|
+
// Get the latest version (first numbered version from sorted list)
|
|
77
|
+
const latestVersion = versions.length > 0 ? versions[0] : null;
|
|
78
|
+
// Build version items for selection
|
|
79
|
+
// "Latest" uses the actual first numbered version, not empty string
|
|
80
|
+
const versionItems = latestVersion
|
|
81
|
+
? [
|
|
82
|
+
{
|
|
83
|
+
label: `✨ Latest (${formatVersionDisplay(latestVersion.version)})`,
|
|
84
|
+
value: latestVersion.version,
|
|
85
|
+
hpsVersion: latestVersion.hpsVersion || latestVersion.version,
|
|
86
|
+
releaseDate: latestVersion.releaseDate,
|
|
87
|
+
},
|
|
88
|
+
// Skip the first version since it's shown as "Latest"
|
|
89
|
+
...versions.slice(1, 15).map((v) => ({
|
|
90
|
+
label: formatVersionDisplay(v.version),
|
|
91
|
+
value: v.version,
|
|
92
|
+
hpsVersion: v.hpsVersion || v.version,
|
|
93
|
+
releaseDate: v.releaseDate,
|
|
94
|
+
})),
|
|
95
|
+
]
|
|
96
|
+
: [
|
|
97
|
+
{
|
|
98
|
+
label: "✨ Latest (recommended)",
|
|
99
|
+
value: "latest",
|
|
100
|
+
hpsVersion: "latest",
|
|
101
|
+
releaseDate: null,
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
return (_jsxs(BorderBox, { title: "License & Version", children: [subStep === "license" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Rulebricks license key:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Get a license at https://rulebricks.com/pricing" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: licenseKey, onChange: setLicenseKey, onSubmit: handleLicenseSubmit, placeholder: "vd67aveCHr1G..." }) })] })), subStep === "loading-versions" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Spinner, { label: "Authenticating and fetching versions..." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Connecting to Docker Hub..." }) })] })), subStep === "version" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Select app version to deploy:" }), loadError && (_jsxs(Text, { color: colors.warning, dimColor: true, children: ["\u26A0 ", loadError] })), _jsx(Box, { marginTop: 1, height: 12, flexDirection: "column", overflowY: "hidden", children: _jsx(SelectInput, { items: versionItems, onSelect: handleVersionSelect, limit: 10, indicatorComponent: () => null, itemComponent: ({ isSelected, label }) => {
|
|
105
|
+
// Find the version item from the list using the label
|
|
106
|
+
const versionItem = versionItems.find((v) => v.label === label);
|
|
107
|
+
const hasHps = versionItem?.value &&
|
|
108
|
+
versionItem?.hpsVersion &&
|
|
109
|
+
versionItem.value !== "latest";
|
|
110
|
+
const hasDate = versionItem?.releaseDate;
|
|
111
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, children: [isSelected ? "❯ " : " ", label] }), hasDate && (_jsxs(Text, { color: colors.muted, children: [" ", "(", formatDate(versionItem.releaseDate), ")"] })), hasHps && (_jsxs(Text, { color: colors.muted, children: [" ", "Solver:", " ", formatVersionDisplay(versionItem.hpsVersion)] }))] }));
|
|
112
|
+
} }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.success, children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", "License key: ", licenseKey.substring(0, 8), "..."] })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["View changelog: ", CHANGELOG_URL] }) })] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.error, children: ["\u2717 ", error] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
|
|
113
|
+
}
|