Skip to content

Commit 73b0785

Browse files
committed
Add extra environment variable support in configuration
- Introduced a new optional field for extra environment variables in the configuration schema, allowing users to define custom key-value pairs. - Implemented a KeyValueEditor component for managing these extra environment variables, enabling users to add, edit, and delete variables easily. - Updated the environment file generation logic to include the new extra environment variables, ensuring they are correctly processed and included in the output. Files modified: - lib/config.tsx: Added extra environment variable schema and updated configuration. - lib/ink.tsx: Implemented KeyValueEditor for managing extra environment variables. - lib/config/env.tsx: Updated environment file generation to include extra environment variables.
1 parent 2bf439b commit 73b0785

File tree

3 files changed

+262
-2
lines changed

3 files changed

+262
-2
lines changed

lib/config.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,17 @@ hasyxConfig.variant = z.object({
657657
backLabel: '< back',
658658
descriptionTemplate: (_data: any) => 'token'
659659
}),
660+
// Extra environment variables
661+
extraEnv: z.string().optional().meta({
662+
type: 'reference-selector',
663+
data: 'extraEnv',
664+
referenceKey: 'extraEnv',
665+
title: 'Extra Environment Variables',
666+
description: 'Select extra environment variables configuration (optional). Use this to add custom variables not covered by other configs.',
667+
emptyMessage: 'No extra environment variables configurations available. Create one first.',
668+
backLabel: '< back',
669+
descriptionTemplate: (_data: any) => 'Custom env variables'
670+
}),
660671
}).meta({
661672
type: 'variant-editor',
662673
title: 'Variant Configuration',
@@ -665,7 +676,7 @@ hasyxConfig.variant = z.object({
665676
'host', 'hasura', 'files', 'telegramBot', 'telegramChannel', 'environment', 'testing',
666677
'googleOAuth', 'yandexOAuth', 'githubOAuth', 'facebookOAuth', 'vkOAuth', 'telegramLoginOAuth', 'nextAuthSecrets',
667678
'storage', 'pg', 'docker', 'dockerhub', 'github', 'vercel', 'iosSigning', 'githubTelegramBot',
668-
'resend', 'smsProvider', 'openrouter', 'npm', 'firebase', 'firebasePublic', 'dns', 'cloudflare', 'projectUser', 'githubWebhooks'
679+
'resend', 'smsProvider', 'openrouter', 'npm', 'firebase', 'firebasePublic', 'dns', 'cloudflare', 'projectUser', 'githubWebhooks', 'extraEnv'
669680
]
670681
});
671682

@@ -1274,6 +1285,27 @@ hasyxConfig.nextAuthSecretsList = nextAuthSecretsList;
12741285
},
12751286
});
12761287

1288+
// Extra Environment Variables Schema
1289+
hasyxConfig.extraEnv = z.record(
1290+
z.string(),
1291+
z.string()
1292+
).meta({
1293+
type: 'extra-env-config',
1294+
title: 'Extra Environment Variables',
1295+
description: 'Add custom environment variables that are not covered by other configurations. Each key-value pair will be added to .env file as-is. Example: { "ANTHROPIC_API_KEY": "sk-...", "MY_CUSTOM_VAR": "value" }',
1296+
});
1297+
1298+
hasyxConfig.extraEnvs = z.record(
1299+
z.string(), // extraEnv configuration name
1300+
hasyxConfig.extraEnv,
1301+
).meta({
1302+
data: 'extraEnv',
1303+
type: 'keys',
1304+
default: ['local', 'dev', 'prod'],
1305+
add: hasyxConfig.extraEnv,
1306+
descriptionTemplate: (_data: any) => 'Custom environment variables'
1307+
});
1308+
12771309
// Testing Schema (for test-data tokens)
12781310
hasyxConfig.testing = z.object({
12791311
token: z
@@ -1747,6 +1779,7 @@ hasyxConfig.file = z.object({
17471779
testing: hasyxConfig.testings,
17481780
global: hasyxConfig.global,
17491781
invites: hasyxConfig.invitesList,
1782+
extraEnv: hasyxConfig.extraEnvs, // Extra environment variables
17501783
// validation rules (optional, alternative to top-level array "validation")
17511784
validationRules: hasyxConfig.validationRules,
17521785
});

lib/config/env.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function resolveVariant(variant: string, config: any) {
2828
'googleOAuth', 'yandexOAuth', 'githubOAuth', 'facebookOAuth', 'vkOAuth', 'telegramLoginOAuth',
2929
'storage', 'files', 'pg', 'docker', 'dockerhub', 'github', 'resend', 'smsru', 'smsaero', 'openrouter', 'npm', 'firebase', 'firebasePublic',
3030
'nextAuthSecrets', 'dns', 'cloudflare', 'projectUser', 'vercel', 'githubWebhooks', 'githubTelegramBot',
31-
'testing',
31+
'testing', 'extraEnv',
3232
];
3333

3434
for (const configType of optionalConfigs) {
@@ -254,6 +254,16 @@ function generateEnvFile(config: any, variant: string): string {
254254
// Больше не дублируем NEXT_PUBLIC_JWT_AUTH здесь – оно придёт из envMapping с учётом numericBoolean
255255
}
256256

257+
// Добавляем произвольные переменные окружения из extraEnv
258+
const extraEnvConfig = resolvedConfig.extraEnv;
259+
if (extraEnvConfig && typeof extraEnvConfig === 'object') {
260+
for (const [envKey, envValue] of Object.entries(extraEnvConfig)) {
261+
if (envValue !== undefined && envValue !== null && envValue !== '') {
262+
envVars.push(`${envKey}=${envValue}`);
263+
}
264+
}
265+
}
266+
257267
return envVars.join('\n');
258268
}
259269

lib/ink.tsx

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,204 @@ export function Form({ schema, onSubmit, initialData = {}, parentConfig }: FormP
800800

801801

802802

803+
// Компонент для редактирования key-value пар (для extraEnv)
804+
function KeyValueEditor({
805+
config,
806+
onConfig,
807+
onBack,
808+
title,
809+
description
810+
}: {
811+
config: Record<string, string>;
812+
onConfig: (newConfig: Record<string, string>) => void;
813+
onBack: () => void;
814+
title?: string;
815+
description?: string;
816+
}) {
817+
const [pairs, setPairs] = useState<Array<{ key: string; value: string }>>(() => {
818+
return Object.entries(config).map(([key, value]) => ({ key, value }));
819+
});
820+
const [editingIndex, setEditingIndex] = useState<number | null>(null);
821+
const [newKey, setNewKey] = useState('');
822+
const [newValue, setNewValue] = useState('');
823+
const [mode, setMode] = useState<'list' | 'add' | 'edit'>('list');
824+
const [addField, setAddField] = useState<'key' | 'value'>('key');
825+
const [tempKey, setTempKey] = useState('');
826+
const [tempValue, setTempValue] = useState('');
827+
828+
// Синхронизируем pairs с config при изменении config извне
829+
// Но только если мы не в режиме редактирования/добавления, чтобы не потерять изменения
830+
useEffect(() => {
831+
if (mode === 'list') {
832+
const newPairs = Object.entries(config).map(([key, value]) => ({ key, value }));
833+
setPairs(newPairs);
834+
}
835+
}, [config, mode]);
836+
837+
useInput((input, key) => {
838+
if (key.escape) {
839+
if (mode === 'add' || mode === 'edit') {
840+
setMode('list');
841+
setNewKey('');
842+
setNewValue('');
843+
setTempKey('');
844+
setTempValue('');
845+
setAddField('key');
846+
setEditingIndex(null);
847+
} else {
848+
onBack();
849+
}
850+
}
851+
});
852+
853+
const savePairs = (updatedPairs: Array<{ key: string; value: string }>) => {
854+
const newConfig: Record<string, string> = {};
855+
updatedPairs.forEach(({ key, value }) => {
856+
if (key.trim()) {
857+
newConfig[key.trim()] = value;
858+
}
859+
});
860+
onConfig(newConfig);
861+
setPairs(updatedPairs);
862+
};
863+
864+
if (mode === 'add') {
865+
if (addField === 'key') {
866+
return (
867+
<Box flexDirection="column" gap={1}>
868+
<Text color="cyan">{title || 'Extra Environment Variables'}</Text>
869+
{description && <Text color="gray">{description}</Text>}
870+
<Text color="yellow">Adding new variable (Escape to cancel)</Text>
871+
<Text color="cyan">Variable name:</Text>
872+
<TextInput
873+
key={`add-key-input`}
874+
defaultValue={tempKey}
875+
onChange={setTempKey}
876+
onSubmit={(value) => {
877+
if (value.trim()) {
878+
setNewKey(value.trim());
879+
setTempKey(value.trim());
880+
setAddField('value');
881+
}
882+
}}
883+
placeholder="e.g., ANTHROPIC_API_KEY"
884+
/>
885+
<Text color="gray">Press Enter to continue to value field</Text>
886+
</Box>
887+
);
888+
}
889+
890+
return (
891+
<Box flexDirection="column" gap={1}>
892+
<Text color="cyan">{title || 'Extra Environment Variables'}</Text>
893+
<Text color="yellow">Adding: {tempKey} (Escape to cancel)</Text>
894+
<Text color="cyan">Variable value:</Text>
895+
<TextInput
896+
key={`add-value-input`}
897+
defaultValue={tempValue}
898+
onChange={setTempValue}
899+
onSubmit={(value) => {
900+
if (tempKey.trim()) {
901+
// Убеждаемся, что мы добавляем к текущим pairs, а не перезаписываем
902+
const currentPairs = pairs.length > 0 ? pairs : Object.entries(config).map(([key, val]) => ({ key, value: val }));
903+
const updated = [...currentPairs, { key: tempKey.trim(), value: value || '' }];
904+
savePairs(updated);
905+
setNewKey('');
906+
setNewValue('');
907+
setTempKey('');
908+
setTempValue('');
909+
setAddField('key');
910+
setMode('list');
911+
}
912+
}}
913+
placeholder="Enter value"
914+
/>
915+
<Text color="gray">Press Enter to save</Text>
916+
</Box>
917+
);
918+
}
919+
920+
if (mode === 'edit' && editingIndex !== null) {
921+
const pair = pairs[editingIndex];
922+
return (
923+
<Box flexDirection="column" gap={1}>
924+
<Text color="cyan">{title || 'Extra Environment Variables'}</Text>
925+
<Text color="yellow">Editing: {pair.key} (Escape to cancel)</Text>
926+
<Text color="cyan">Variable name:</Text>
927+
<TextInput
928+
key={`edit-key-${editingIndex}`}
929+
defaultValue={newKey}
930+
onChange={setNewKey}
931+
placeholder={pair.key}
932+
/>
933+
<Text color="cyan">Variable value:</Text>
934+
<TextInput
935+
key={`edit-value-${editingIndex}`}
936+
defaultValue={newValue}
937+
onChange={setNewValue}
938+
onSubmit={() => {
939+
const updated = [...pairs];
940+
updated[editingIndex] = { key: newKey.trim() || pair.key, value: newValue };
941+
savePairs(updated);
942+
setEditingIndex(null);
943+
setNewKey('');
944+
setNewValue('');
945+
setMode('list');
946+
}}
947+
placeholder={pair.value}
948+
/>
949+
</Box>
950+
);
951+
}
952+
953+
const options = [
954+
{ label: '< back', value: 'back' },
955+
...pairs.map((pair, index) => ({
956+
label: `${pair.key}=${pair.value.length > 30 ? pair.value.substring(0, 30) + '...' : pair.value}`,
957+
value: `edit-${index}`
958+
})),
959+
...(pairs.length > 0 ? pairs.map((pair, index) => ({
960+
label: `🗑️ delete ${pair.key}`,
961+
value: `delete-${index}`
962+
})) : []),
963+
{ label: '+ add variable', value: 'add' }
964+
];
965+
966+
return (
967+
<Box flexDirection="column" gap={1}>
968+
<Text color="cyan">{title || 'Extra Environment Variables'}</Text>
969+
{description && <Text color="gray">{description}</Text>}
970+
<Text color="gray">Variables: {pairs.length}</Text>
971+
<Select
972+
options={options}
973+
onChange={(value) => {
974+
if (value === 'back') {
975+
onBack();
976+
} else if (value === 'add') {
977+
setNewKey('');
978+
setNewValue('');
979+
setTempKey('');
980+
setTempValue('');
981+
setAddField('key');
982+
setMode('add');
983+
} else if (value.startsWith('edit-')) {
984+
const index = parseInt(value.split('-')[1]);
985+
const pair = pairs[index];
986+
setEditingIndex(index);
987+
setNewKey(pair.key);
988+
setNewValue(pair.value);
989+
setMode('edit');
990+
} else if (value.startsWith('delete-')) {
991+
const index = parseInt(value.split('-')[1]);
992+
const updated = pairs.filter((_, i) => i !== index);
993+
savePairs(updated);
994+
}
995+
}}
996+
/>
997+
</Box>
998+
);
999+
}
1000+
8031001
// Универсальный компонент для добавления нового ключа
8041002
function AddNewKey({ schema, defaultKeys, onAdd, onBack }: {
8051003
schema: z.ZodSchema;
@@ -980,6 +1178,25 @@ function KeysList({
9801178
);
9811179
}
9821180

1181+
// Специальная обработка для extra-env-config (key-value pairs)
1182+
const extraEnvSchemaMeta = (addSchema as any).meta ? (addSchema as any).meta() : {};
1183+
if (extraEnvSchemaMeta.type === 'extra-env-config') {
1184+
return (
1185+
<KeyValueEditor
1186+
key={`keyvalue-${selectedKey}`}
1187+
config={config[selectedKey] || {}}
1188+
onConfig={(newKeyValuePairs) => {
1189+
const updatedConfig = { ...config, [selectedKey]: newKeyValuePairs };
1190+
onConfig(updatedConfig);
1191+
setSelectedKey(null);
1192+
}}
1193+
onBack={() => setSelectedKey(null)}
1194+
title={extraEnvSchemaMeta.title || 'Extra Environment Variables'}
1195+
description={extraEnvSchemaMeta.description}
1196+
/>
1197+
);
1198+
}
1199+
9831200
// Для остальных схем используем Form
9841201
return (
9851202
<Box flexDirection="column" gap={1}>

0 commit comments

Comments
 (0)