@rulebricks/cli 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +62 -0
  2. package/dist/commands/clone.d.ts +6 -0
  3. package/dist/commands/clone.js +60 -0
  4. package/dist/commands/deploy.d.ts +8 -0
  5. package/dist/commands/deploy.js +409 -0
  6. package/dist/commands/destroy.d.ts +8 -0
  7. package/dist/commands/destroy.js +298 -0
  8. package/dist/commands/init.d.ts +7 -0
  9. package/dist/commands/init.js +201 -0
  10. package/dist/commands/logs.d.ts +9 -0
  11. package/dist/commands/logs.js +222 -0
  12. package/dist/commands/open.d.ts +7 -0
  13. package/dist/commands/open.js +139 -0
  14. package/dist/commands/status.d.ts +5 -0
  15. package/dist/commands/status.js +125 -0
  16. package/dist/commands/upgrade.d.ts +7 -0
  17. package/dist/commands/upgrade.js +239 -0
  18. package/dist/components/DNSWaitScreen.d.ts +9 -0
  19. package/dist/components/DNSWaitScreen.js +73 -0
  20. package/dist/components/Wizard/WizardContext.d.ts +176 -0
  21. package/dist/components/Wizard/WizardContext.js +346 -0
  22. package/dist/components/Wizard/index.d.ts +2 -0
  23. package/dist/components/Wizard/index.js +2 -0
  24. package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
  25. package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
  26. package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
  27. package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
  28. package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
  29. package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
  30. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
  31. package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
  32. package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
  33. package/dist/components/Wizard/steps/DomainStep.js +126 -0
  34. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
  35. package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
  36. package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
  38. package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
  39. package/dist/components/Wizard/steps/ReviewStep.js +56 -0
  40. package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
  41. package/dist/components/Wizard/steps/SMTPStep.js +191 -0
  42. package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
  43. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
  44. package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
  45. package/dist/components/Wizard/steps/TierStep.js +29 -0
  46. package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
  47. package/dist/components/Wizard/steps/VersionStep.js +113 -0
  48. package/dist/components/Wizard/steps/index.d.ts +12 -0
  49. package/dist/components/Wizard/steps/index.js +12 -0
  50. package/dist/components/common/AppShell.d.ts +31 -0
  51. package/dist/components/common/AppShell.js +31 -0
  52. package/dist/components/common/Box.d.ts +20 -0
  53. package/dist/components/common/Box.js +20 -0
  54. package/dist/components/common/Logo.d.ts +7 -0
  55. package/dist/components/common/Logo.js +22 -0
  56. package/dist/components/common/Spinner.d.ts +12 -0
  57. package/dist/components/common/Spinner.js +28 -0
  58. package/dist/components/common/index.d.ts +6 -0
  59. package/dist/components/common/index.js +5 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +202 -0
  62. package/dist/lib/cloudCli.d.ts +156 -0
  63. package/dist/lib/cloudCli.js +691 -0
  64. package/dist/lib/config.d.ts +91 -0
  65. package/dist/lib/config.js +278 -0
  66. package/dist/lib/dns.d.ts +41 -0
  67. package/dist/lib/dns.js +235 -0
  68. package/dist/lib/dockerHub.d.ts +57 -0
  69. package/dist/lib/dockerHub.js +128 -0
  70. package/dist/lib/helm.d.ts +53 -0
  71. package/dist/lib/helm.js +209 -0
  72. package/dist/lib/helmValues.d.ts +17 -0
  73. package/dist/lib/helmValues.js +693 -0
  74. package/dist/lib/kubernetes.d.ts +161 -0
  75. package/dist/lib/kubernetes.js +755 -0
  76. package/dist/lib/terraform.d.ts +44 -0
  77. package/dist/lib/terraform.js +230 -0
  78. package/dist/lib/theme.d.ts +81 -0
  79. package/dist/lib/theme.js +115 -0
  80. package/dist/lib/validation.d.ts +47 -0
  81. package/dist/lib/validation.js +164 -0
  82. package/dist/lib/versions.d.ts +69 -0
  83. package/dist/lib/versions.js +139 -0
  84. package/dist/types/index.d.ts +718 -0
  85. package/dist/types/index.js +556 -0
  86. package/email-templates/email_change.html +325 -0
  87. package/email-templates/invite.html +383 -0
  88. package/email-templates/password_change.html +414 -0
  89. package/email-templates/verify.html +396 -0
  90. package/package.json +78 -0
  91. package/terraform/aws/main.tf +327 -0
  92. package/terraform/azure/main.tf +326 -0
  93. package/terraform/gcp/main.tf +369 -0
@@ -0,0 +1,6 @@
1
+ interface FeaturesStepProps {
2
+ onComplete: () => void;
3
+ onBack: () => void;
4
+ }
5
+ export declare function FeaturesStep({ onComplete, onBack }: FeaturesStepProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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,6 @@
1
+ interface ReviewStepProps {
2
+ onComplete: () => void;
3
+ onBack: () => void;
4
+ }
5
+ export declare function ReviewStep({ onComplete, onBack }: ReviewStepProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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,6 @@
1
+ interface SMTPStepProps {
2
+ onComplete: () => void;
3
+ onBack: () => void;
4
+ }
5
+ export declare function SMTPStep({ onComplete, onBack }: SMTPStepProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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,6 @@
1
+ interface SupabaseCredentialsStepProps {
2
+ onComplete: () => void;
3
+ onBack: () => void;
4
+ }
5
+ export declare function SupabaseCredentialsStep({ onComplete, onBack, }: SupabaseCredentialsStepProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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,6 @@
1
+ interface TierStepProps {
2
+ onComplete: () => void;
3
+ onBack: () => void;
4
+ }
5
+ export declare function TierStep({ onComplete, onBack }: TierStepProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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,6 @@
1
+ interface VersionStepProps {
2
+ onComplete: () => void;
3
+ onBack: () => void;
4
+ }
5
+ export declare function VersionStep({ onComplete, onBack }: VersionStepProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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
+ }