Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shared-settings-config-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostream": minor
---

refactor: extract shared settings-config module and guided schema for admin settings editor foundation
20 changes: 3 additions & 17 deletions src/cli/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import yaml from 'js-yaml'

import {
getByPath,
loadDefaults,
loadMergedSettings,
loadUserSettings,
parseTypedValue,
saveSettings,
setByPath,
toCategoryLabel,
validatePathAgainstDefaults,
validateSettings,
} from '../utils/config'
} from '../../utils/settings-config'
import {
isSecretEnvKey,
isSupportedEnvKey,
Expand Down Expand Up @@ -57,14 +57,6 @@ const serialize = (value: unknown): string => {
return yaml.dump(value, { lineWidth: 120 }).trimEnd()
}

const formatLabel = (key: string): string => {
return key
.split(/[_\-.]/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}

const restartRelay = async (): Promise<number> => {
const spinner = ora('Restarting relay...').start()

Expand Down Expand Up @@ -181,7 +173,6 @@ export const runConfigValidate = async (): Promise<number> => {

return 1
}

export const runConfigEnvList = async (options: { showSecrets?: boolean } = {}): Promise<number> => {
const values = readEnvValues()
const entries = Object.entries(values).sort(([a], [b]) => a.localeCompare(b))
Expand Down Expand Up @@ -250,13 +241,8 @@ export const runConfigEnvValidate = async (): Promise<number> => {

logError('Environment validation failed:')
for (const issue of issues) {
logError(`- ${formatLabel(issue.path)} (${issue.path}): ${issue.message}`)
logError(`- ${toCategoryLabel(issue.path)} (${issue.path}): ${issue.message}`)
}

return 1
}

export const getConfigTopLevelCategories = (): string[] => {
const defaults = loadDefaults() as unknown as Record<string, unknown>
return Object.keys(defaults)
}
133 changes: 9 additions & 124 deletions src/cli/tui/menus/configure.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import {
getConfigTopLevelCategories,
runConfigGet,
runConfigList,
runConfigSet,
runConfigValidate,
} from '../../commands/config'
import { getByPath, loadMergedSettings } from '../../utils/config'
import { getByPath, getTopLevelSettingCategories, loadMergedSettings, toCategoryLabel } from '../../../utils/settings-config'
import {
type GuidedSettingField,
guidedSettingCategories,
} from '../../../utils/settings-guided-schema'
import { tuiPrompts } from '../prompts'

const toCategoryLabel = (key: string): string => {
return key
.split(/[_\-.]/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}

const getCategoryOptions = () => {
const categories = getConfigTopLevelCategories().sort((a, b) => a.localeCompare(b))
const categories = getTopLevelSettingCategories().sort((a, b) => a.localeCompare(b))

return [
...categories.map((category) => ({
Expand All @@ -28,116 +23,6 @@ const getCategoryOptions = () => {
]
}

type GuidedSetting = {
label: string
path: string
type: 'boolean' | 'number' | 'string' | 'select' | 'stringArray'
options?: string[]
placeholder?: string
validate?: (value: string) => string | undefined
}

type GuidedCategory = {
value: string
label: string
settings: GuidedSetting[]
}

const requireNonEmpty = (value: string): string | undefined => {
return value.trim() ? undefined : 'Value is required'
}

const requireSafeNonNegativeInteger = (value: string): string | undefined => {
const trimmed = value.trim()
if (!/^\d+$/.test(trimmed)) {
return 'Value must be a non-negative integer'
}

const parsed = Number(trimmed)
if (!Number.isSafeInteger(parsed)) {
return 'Value must be a safe integer'
}

return undefined
}

const guidedCategories: GuidedCategory[] = [
{
value: 'payments',
label: 'Payments',
settings: [
{ label: 'Enable payments', path: 'payments.enabled', type: 'boolean' },
{
label: 'Payment processor',
path: 'payments.processor',
type: 'select',
options: ['zebedee', 'lnbits', 'lnurl', 'nodeless', 'opennode'],
},
{
label: 'Admission fee enabled',
path: 'payments.feeSchedules.admission[0].enabled',
type: 'boolean',
},
{
label: 'Admission fee amount (msats)',
path: 'payments.feeSchedules.admission[0].amount',
type: 'number',
validate: requireSafeNonNegativeInteger,
},
],
},
{
value: 'network',
label: 'Network',
settings: [
{
label: 'Relay URL',
path: 'info.relay_url',
type: 'string',
placeholder: 'wss://relay.example.com',
validate: requireNonEmpty,
},
{
label: 'Relay name',
path: 'info.name',
type: 'string',
placeholder: 'relay.example.com',
validate: requireNonEmpty,
},
{
label: 'Max payload size',
path: 'network.maxPayloadSize',
type: 'number',
validate: requireSafeNonNegativeInteger,
},
],
},
{
value: 'limits',
label: 'Limits',
settings: [
{
label: 'Rate limiter strategy',
path: 'limits.rateLimiter.strategy',
type: 'select',
options: ['ewma', 'sliding_window'],
},
{
label: 'Primary event content max length',
path: 'limits.event.content[0].maxLength',
type: 'number',
validate: requireSafeNonNegativeInteger,
},
{
label: 'Minimum pubkey balance',
path: 'limits.event.pubkey.minBalance',
type: 'number',
validate: requireSafeNonNegativeInteger,
},
],
},
]

const formatCurrentValue = (value: unknown): string => {
if (Array.isArray(value)) {
return value.length === 0 ? '[]' : value.join(', ')
Expand All @@ -162,7 +47,7 @@ const formatCurrentValue = (value: unknown): string => {
return String(value)
}

const getGuidedSettingValue = async (setting: GuidedSetting, currentValue: unknown) => {
const getGuidedSettingValue = async (setting: GuidedSettingField, currentValue: unknown) => {
switch (setting.type) {
case 'boolean': {
const answer = await tuiPrompts.confirm({
Expand Down Expand Up @@ -248,7 +133,7 @@ const getGuidedSettingValue = async (setting: GuidedSetting, currentValue: unkno
const runGuidedConfigureMenu = async (): Promise<number> => {
const category = await tuiPrompts.select({
message: 'Configuration category',
options: [...guidedCategories.map(({ value, label }) => ({ value, label })), { value: 'back', label: 'Back' }],
options: [...guidedSettingCategories.map(({ value, label }) => ({ value, label })), { value: 'back', label: 'Back' }],
})

if (tuiPrompts.isCancel(category)) {
Expand All @@ -259,7 +144,7 @@ const runGuidedConfigureMenu = async (): Promise<number> => {
return 0
}

const selectedCategory = guidedCategories.find((entry) => entry.value === category)
const selectedCategory = guidedSettingCategories.find((entry) => entry.value === category)
if (!selectedCategory) {
tuiPrompts.cancel('Unknown category')
return 1
Expand Down
Loading
Loading