Skip to content
Open
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
16 changes: 5 additions & 11 deletions src/cli/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import ora from 'ora'
import yaml from 'js-yaml'

import {
formatSettingCategoryLabel,
getByPath,
loadDefaults,
getTopLevelSettingCategories,
loadMergedSettings,
loadUserSettings,
parseTypedValue,
saveSettings,
setByPath,
validatePathAgainstDefaults,
validateSettings,
} from '../utils/config'
} from '../../utils/settings-config'
import {
isSecretEnvKey,
isSupportedEnvKey,
Expand Down Expand Up @@ -57,13 +58,7 @@ 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 formatLabel = formatSettingCategoryLabel

const restartRelay = async (): Promise<number> => {
const spinner = ora('Restarting relay...').start()
Expand Down Expand Up @@ -257,6 +252,5 @@ export const runConfigEnvValidate = async (): Promise<number> => {
}

export const getConfigTopLevelCategories = (): string[] => {
const defaults = loadDefaults() as unknown as Record<string, unknown>
return Object.keys(defaults)
return getTopLevelSettingCategories()
}
130 changes: 9 additions & 121 deletions src/cli/tui/menus/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@ import {
runConfigSet,
runConfigValidate,
} from '../../commands/config'
import { getByPath, loadMergedSettings } from '../../utils/config'
import { formatSettingCategoryLabel, getByPath, loadMergedSettings } 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 toCategoryLabel = formatSettingCategoryLabel

const getCategoryOptions = () => {
const categories = getConfigTopLevelCategories().sort((a, b) => a.localeCompare(b))
Expand All @@ -28,116 +26,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 +50,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 +136,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 +147,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