• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cameri / nostream / 24938720143

25 Apr 2026 07:23PM UTC coverage: 63.498% (-12.0%) from 75.491%
24938720143

Pull #574

github

web-flow
Merge 73575abe4 into c0c1c35b8
Pull Request #574: feat: migrate nostream scripts to unified CLI/TUI

1619 of 2880 branches covered (56.22%)

Branch coverage included in aggregate %.

735 of 1701 new or added lines in 29 files covered. (43.21%)

3838 of 5714 relevant lines covered (67.17%)

22.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.08
/src/cli/utils/config.ts
1
import fs from 'fs'
2✔
2
import yaml from 'js-yaml'
2✔
3
import { mergeDeepRight } from 'ramda'
2✔
4

5
import { Settings } from '../../@types/settings'
6
import { getConfigBaseDir, getDefaultSettingsFilePath, getSettingsFilePath } from './paths'
2✔
7

8
export type ValidationIssue = {
9
  path: string
10
  message: string
11
}
12

13
type PathToken =
14
  | {
15
      type: 'key'
16
      key: string
17
    }
18
  | {
19
      type: 'index'
20
      index: number
21
    }
22

23
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
2✔
24
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
58✔
25
}
26

27
const parsePath = (path: string): PathToken[] => {
2✔
28
  const input = path.trim()
19✔
29

30
  if (!input) {
19!
NEW
31
    throw new Error('Path is required')
×
32
  }
33

34
  const tokens: PathToken[] = []
19✔
35
  const segments = input.split('.').map((part) => part.trim())
53✔
36

37
  for (const segment of segments) {
19✔
38
    if (!segment) {
53!
NEW
39
      throw new Error(`Invalid path segment in: ${path}`)
×
40
    }
41

42
    const match = segment.match(/^([A-Za-z_][A-Za-z0-9_]*)(\[(\d+)\])*$/)
53✔
43
    if (!match) {
53✔
44
      throw new Error(`Invalid path segment: ${segment}`)
1✔
45
    }
46

47
    tokens.push({ type: 'key', key: match[1] })
52✔
48

49
    const indexes = [...segment.matchAll(/\[(\d+)\]/g)]
52✔
50
    for (const entry of indexes) {
52✔
51
      tokens.push({
7✔
52
        type: 'index',
53
        index: Number(entry[1]),
54
      })
55
    }
56
  }
57

58
  return tokens
18✔
59
}
60

61
const formatPathTokens = (tokens: PathToken[]): string => {
2✔
62
  let out = ''
1✔
63

64
  for (const token of tokens) {
1✔
NEW
65
    if (token.type === 'key') {
×
NEW
66
      out = out ? `${out}.${token.key}` : token.key
×
NEW
67
      continue
×
68
    }
69

NEW
70
    out = `${out}[${token.index}]`
×
71
  }
72

73
  return out
1✔
74
}
75

76
export const parseValue = (raw: string): unknown => {
2✔
77
  const trimmed = raw.trim()
6✔
78

79
  if (trimmed === 'true') {
6✔
80
    return true
1✔
81
  }
82

83
  if (trimmed === 'false') {
5✔
84
    return false
1✔
85
  }
86

87
  if (trimmed === 'null') {
4✔
88
    return null
1✔
89
  }
90

91
  if (/^-?\d+$/.test(trimmed)) {
3✔
92
    const asNumber = Number(trimmed)
1✔
93
    if (Number.isSafeInteger(asNumber)) {
1!
94
      return asNumber
1✔
95
    }
96
  }
97

98
  if (/^-?\d+n$/.test(trimmed)) {
2✔
99
    return BigInt(trimmed.slice(0, -1))
1✔
100
  }
101

102
  if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
1!
NEW
103
    try {
×
NEW
104
      return JSON.parse(trimmed)
×
105
    } catch {
NEW
106
      return raw
×
107
    }
108
  }
109

110
  return raw
1✔
111
}
112

113
export const parseTypedValue = (raw: string, type: 'inferred' | 'json' = 'inferred'): unknown => {
2!
114
  if (type === 'json') {
3!
115
    try {
3✔
116
      return JSON.parse(raw)
3✔
117
    } catch (error) {
118
      const message = error instanceof Error ? error.message : String(error)
1!
119
      throw new Error(`Invalid JSON value: ${message}`)
1✔
120
    }
121
  }
122

NEW
123
  return parseValue(raw)
×
124
}
125

126
const toSerializable = (value: unknown): unknown => {
2✔
NEW
127
  if (typeof value === 'bigint') {
×
NEW
128
    return value.toString()
×
129
  }
130

NEW
131
  if (Array.isArray(value)) {
×
NEW
132
    return value.map((entry) => toSerializable(entry))
×
133
  }
134

NEW
135
  if (isPlainObject(value)) {
×
NEW
136
    return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, toSerializable(entry)]))
×
137
  }
138

NEW
139
  return value
×
140
}
141

142
const validateShape = (schema: unknown, candidate: unknown, path: PathToken[], issues: ValidationIssue[]): void => {
2✔
143
  if (schema === undefined || candidate === undefined) {
10✔
144
    return
9✔
145
  }
146

147
  const renderedPath = formatPathTokens(path) || '$'
1✔
148

149
  if (Array.isArray(schema)) {
1!
NEW
150
    if (!Array.isArray(candidate)) {
×
NEW
151
      issues.push({
×
152
        path: renderedPath,
153
        message: `Expected array, got ${typeof candidate}`,
154
      })
NEW
155
      return
×
156
    }
157

NEW
158
    if (schema.length === 0) {
×
NEW
159
      return
×
160
    }
161

NEW
162
    candidate.forEach((entry, index) => {
×
NEW
163
      const matchesAny = schema.some((schemaEntry) => {
×
NEW
164
        const localIssues: ValidationIssue[] = []
×
NEW
165
        validateShape(schemaEntry, entry, [...path, { type: 'index', index }], localIssues)
×
NEW
166
        return localIssues.length === 0
×
167
      })
168

NEW
169
      if (!matchesAny) {
×
NEW
170
        issues.push({
×
171
          path: formatPathTokens([...path, { type: 'index', index }]),
172
          message: 'Array element does not match expected schema shape',
173
        })
174
      }
175
    })
NEW
176
    return
×
177
  }
178

179
  if (isPlainObject(schema)) {
1!
180
    if (!isPlainObject(candidate)) {
1!
NEW
181
      issues.push({
×
182
        path: renderedPath,
183
        message: `Expected object, got ${typeof candidate}`,
184
      })
NEW
185
      return
×
186
    }
187

188
    for (const key of Object.keys(candidate)) {
1✔
NEW
189
      if (!(key in schema)) {
×
NEW
190
        issues.push({
×
191
          path: formatPathTokens([...path, { type: 'key', key }]),
192
          message: 'Unknown setting key',
193
        })
194
      }
195
    }
196

197
    for (const key of Object.keys(schema)) {
1✔
198
      validateShape((schema as Record<string, unknown>)[key], (candidate as Record<string, unknown>)[key], [...path, { type: 'key', key }], issues)
9✔
199
    }
200

201
    return
1✔
202
  }
203

NEW
204
  if (candidate === null && schema !== null) {
×
NEW
205
    issues.push({
×
206
      path: renderedPath,
207
      message: `Expected ${typeof schema}, got null`,
208
    })
NEW
209
    return
×
210
  }
211

NEW
212
  if (schema !== null && typeof schema !== typeof candidate) {
×
NEW
213
    issues.push({
×
214
      path: renderedPath,
215
      message: `Expected ${typeof schema}, got ${typeof candidate}`,
216
    })
217
  }
218
}
219

220
const pathExistsInSchema = (schema: unknown, tokens: PathToken[]): boolean => {
2✔
221
  let current: unknown = schema
3✔
222

223
  for (const token of tokens) {
3✔
224
    if (token.type === 'key') {
9✔
225
      if (!isPlainObject(current) || !(token.key in current)) {
8✔
226
        return false
1✔
227
      }
228
      current = (current as Record<string, unknown>)[token.key]
7✔
229
      continue
7✔
230
    }
231

232
    if (!Array.isArray(current)) {
1!
NEW
233
      return false
×
234
    }
235

236
    current = current[0]
1✔
237
  }
238

239
  return true
2✔
240
}
241

242
export const ensureSettingsExists = (): void => {
2✔
243
  const configDir = getConfigBaseDir()
2✔
244
  const settingsPath = getSettingsFilePath()
2✔
245
  const defaultsPath = getDefaultSettingsFilePath()
2✔
246

247
  if (!fs.existsSync(configDir)) {
2!
NEW
248
    fs.mkdirSync(configDir, { recursive: true })
×
249
  }
250

251
  if (!fs.existsSync(settingsPath)) {
2!
NEW
252
    fs.copyFileSync(defaultsPath, settingsPath)
×
253
  }
254
}
255

256
export const loadDefaults = (): Settings => {
2✔
257
  const defaultsRaw = fs.readFileSync(getDefaultSettingsFilePath(), 'utf-8')
7✔
258
  return yaml.load(defaultsRaw) as Settings
7✔
259
}
260

261
export const loadUserSettings = (): Settings => {
2✔
262
  ensureSettingsExists()
2✔
263
  const raw = fs.readFileSync(getSettingsFilePath(), 'utf-8')
2✔
264
  return (yaml.load(raw) as Settings) ?? ({} as Settings)
2!
265
}
266

267
export const loadMergedSettings = (): Settings => {
2✔
268
  return mergeDeepRight(loadDefaults(), loadUserSettings()) as Settings
2✔
269
}
270

271
export const saveSettings = (settings: Settings): void => {
2✔
NEW
272
  ensureSettingsExists()
×
NEW
273
  const serialized = yaml.dump(toSerializable(settings), { lineWidth: 120 })
×
NEW
274
  fs.writeFileSync(getSettingsFilePath(), serialized, 'utf-8')
×
275
}
276

277
export const getByPath = (settings: unknown, path: string): unknown => {
2✔
278
  const tokens = parsePath(path)
13✔
279
  let current: unknown = settings
13✔
280

281
  for (const token of tokens) {
13✔
282
    if (token.type === 'key') {
43✔
283
      if (!isPlainObject(current)) {
38!
NEW
284
        return undefined
×
285
      }
286
      current = current[token.key]
38✔
287
      continue
38✔
288
    }
289

290
    if (!Array.isArray(current)) {
5!
NEW
291
      return undefined
×
292
    }
293

294
    current = current[token.index]
5✔
295
  }
296

297
  return current
13✔
298
}
299

300
const ensureArrayLength = (target: unknown[], minimumLength: number): void => {
2✔
301
  while (target.length <= minimumLength) {
1✔
NEW
302
    target.push(undefined)
×
303
  }
304
}
305

306
export const setByPath = (settings: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => {
2✔
307
  const tokens = parsePath(path)
3✔
308
  const clone: Record<string, unknown> = structuredClone(settings)
2✔
309

310
  if (tokens.length === 0) {
2!
NEW
311
    throw new Error('Path is required')
×
312
  }
313

314
  let current: unknown = clone
2✔
315

316
  for (let i = 0; i < tokens.length - 1; i++) {
2✔
317
    const token = tokens[i]
5✔
318
    const nextToken = tokens[i + 1]
5✔
319

320
    if (token.type === 'key') {
5✔
321
      if (!isPlainObject(current)) {
4!
NEW
322
        throw new Error(`Cannot set key ${token.key} on non-object path`) 
×
323
      }
324

325
      const existing = current[token.key]
4✔
326
      if (existing === undefined) {
4!
NEW
327
        current[token.key] = nextToken.type === 'index' ? [] : {}
×
328
      } else if (nextToken.type === 'index' && !Array.isArray(existing)) {
4!
NEW
329
        current[token.key] = []
×
330
      } else if (nextToken.type === 'key' && !isPlainObject(existing)) {
4!
NEW
331
        current[token.key] = {}
×
332
      }
333

334
      current = current[token.key]
4✔
335
      continue
4✔
336
    }
337

338
    if (!Array.isArray(current)) {
1!
NEW
339
      throw new Error(`Cannot index non-array path at [${token.index}]`)
×
340
    }
341

342
    ensureArrayLength(current, token.index)
1✔
343

344
    const existing = current[token.index]
1✔
345
    if (existing === undefined) {
1!
NEW
346
      current[token.index] = nextToken.type === 'index' ? [] : {}
×
347
    } else if (nextToken.type === 'index' && !Array.isArray(existing)) {
1!
NEW
348
      current[token.index] = []
×
349
    } else if (nextToken.type === 'key' && !isPlainObject(existing)) {
1!
NEW
350
      current[token.index] = {}
×
351
    }
352

353
    current = current[token.index]
1✔
354
  }
355

356
  const last = tokens[tokens.length - 1]
2✔
357

358
  if (last.type === 'key') {
2!
359
    if (!isPlainObject(current)) {
2!
NEW
360
      throw new Error(`Cannot set key ${last.key} on non-object path`)
×
361
    }
362

363
    current[last.key] = value
2✔
364
    return clone
2✔
365
  }
366

NEW
367
  if (!Array.isArray(current)) {
×
NEW
368
    throw new Error(`Cannot index non-array path at [${last.index}]`)
×
369
  }
370

NEW
371
  ensureArrayLength(current, last.index)
×
NEW
372
  current[last.index] = value
×
373

NEW
374
  return clone
×
375
}
376

377
export const validatePathAgainstDefaults = (path: string): ValidationIssue[] => {
2✔
378
  const defaults = loadDefaults() as unknown
3✔
379
  const tokens = parsePath(path)
3✔
380

381
  if (pathExistsInSchema(defaults, tokens)) {
3✔
382
    return []
2✔
383
  }
384

385
  return [
1✔
386
    {
387
      path,
388
      message: 'Path does not exist in default settings schema',
389
    },
390
  ]
391
}
392

393
export const validateSettings = (settings: Settings): ValidationIssue[] => {
2✔
394
  const issues: ValidationIssue[] = []
1✔
395

396
  if (!settings.info?.relay_url) {
1!
397
    issues.push({ path: 'info.relay_url', message: 'relay_url is required' })
1✔
398
  }
399

400
  if (!settings.info?.name) {
1!
401
    issues.push({ path: 'info.name', message: 'name is required' })
1✔
402
  }
403

404
  if (!settings.network) {
1!
405
    issues.push({ path: 'network', message: 'network section is required' })
1✔
406
  }
407

408
  if (settings.payments?.enabled && !settings.payments.processor) {
1!
NEW
409
    issues.push({ path: 'payments.processor', message: 'processor is required when payments are enabled' })
×
410
  }
411

412
  const strategy = settings.limits?.rateLimiter?.strategy
1✔
413
  if (strategy && strategy !== 'ewma' && strategy !== 'sliding_window') {
1!
NEW
414
    issues.push({ path: 'limits.rateLimiter.strategy', message: 'strategy must be ewma or sliding_window' })
×
415
  }
416

417
  validateShape(loadDefaults(), settings, [], issues)
1✔
418

419
  return issues
1✔
420
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc