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

cameri / nostream / 24937797860

25 Apr 2026 06:35PM UTC coverage: 63.498% (-12.0%) from 75.491%
24937797860

Pull #574

github

web-flow
Merge b4c964365 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%)

16.28 hits per line

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

9.52
/src/cli/commands/setup.ts
1
import fs from 'fs'
1✔
2
import { randomBytes } from 'crypto'
1✔
3
import { intro, outro, confirm, text, isCancel, cancel } from '@clack/prompts'
1✔
4

5
import { ensureConfigBootstrap } from '../utils/bootstrap'
1✔
6
import { getProjectPath } from '../utils/paths'
1✔
7
import { runStart } from './start'
1✔
8

9
type SetupOptions = {
10
  yes?: boolean
11
  start?: boolean
12
}
13

14
const SECRET_PLACEHOLDER = 'change_me_to_something_long_and_random'
1✔
15

16
export const setupPrompts = {
1✔
17
  intro,
18
  outro,
19
  confirm,
20
  text,
21
  isCancel,
22
  cancel,
23
}
24

25
class SetupCancelledError extends Error {
26
  constructor() {
NEW
27
    super('Setup cancelled')
×
NEW
28
    this.name = 'SetupCancelledError'
×
29
  }
30
}
31

32
const readEnvSecret = (content: string): string | undefined => {
1✔
NEW
33
  for (const line of content.split(/\r?\n/)) {
×
NEW
34
    const trimmed = line.trim()
×
NEW
35
    if (!trimmed || trimmed.startsWith('#') || !trimmed.startsWith('SECRET=')) {
×
NEW
36
      continue
×
37
    }
38

NEW
39
    const [rawValue] = trimmed.slice('SECRET='.length).split('#', 1)
×
NEW
40
    return rawValue.trim()
×
41
  }
42

NEW
43
  return undefined
×
44
}
45

46
const needsSecretReplacement = (secret: string | undefined): boolean => {
1✔
NEW
47
  return !secret || secret === SECRET_PLACEHOLDER
×
48
}
49

50
const resolveSecret = async (assumeYes: boolean): Promise<string> => {
1✔
NEW
51
  if (process.env.SECRET?.trim()) {
×
NEW
52
    return process.env.SECRET.trim()
×
53
  }
54

NEW
55
  if (!assumeYes && process.stdin.isTTY) {
×
NEW
56
    const value = await setupPrompts.text({
×
57
      message: 'SECRET env var value (hex recommended)',
58
      placeholder: 'openssl rand -hex 128',
NEW
59
      validate: (input) => (input.trim() ? undefined : 'SECRET is required'),
×
60
    })
61

NEW
62
    if (setupPrompts.isCancel(value)) {
×
NEW
63
      setupPrompts.cancel('Setup cancelled')
×
NEW
64
      throw new SetupCancelledError()
×
65
    }
66

NEW
67
    return value.trim()
×
68
  }
69

NEW
70
  return randomBytes(64).toString('hex')
×
71
}
72

73
const upsertSecret = (content: string, secret: string): string => {
1✔
NEW
74
  const normalized = content.length > 0 ? content : ''
×
NEW
75
  const lines = normalized.split(/\r?\n/)
×
NEW
76
  let replaced = false
×
77

NEW
78
  const nextLines = lines.map((line) => {
×
NEW
79
    if (replaced) {
×
NEW
80
      return line
×
81
    }
82

NEW
83
    const trimmed = line.trim()
×
NEW
84
    if (!trimmed.startsWith('SECRET=') || trimmed.startsWith('#')) {
×
NEW
85
      return line
×
86
    }
87

NEW
88
    replaced = true
×
NEW
89
    const commentIndex = line.indexOf('#')
×
NEW
90
    const commentSuffix = commentIndex >= 0 ? line.slice(commentIndex).trimEnd() : ''
×
NEW
91
    return commentSuffix ? `SECRET=${secret} ${commentSuffix}` : `SECRET=${secret}`
×
92
  })
93

NEW
94
  if (!replaced) {
×
NEW
95
    if (nextLines.length > 0 && nextLines[nextLines.length - 1] !== '') {
×
NEW
96
      nextLines.push(`SECRET=${secret}`)
×
NEW
97
    } else if (nextLines.length === 0) {
×
NEW
98
      nextLines.push(`SECRET=${secret}`)
×
99
    } else {
NEW
100
      nextLines[nextLines.length - 1] = `SECRET=${secret}`
×
NEW
101
      nextLines.push('')
×
102
    }
103
  }
104

NEW
105
  return nextLines.join('\n')
×
106
}
107

108
const ensureEnvFile = async (assumeYes: boolean): Promise<boolean> => {
1✔
NEW
109
  const envPath = getProjectPath('.env')
×
NEW
110
  const envExamplePath = getProjectPath('.env.example')
×
111

NEW
112
  if (!fs.existsSync(envPath)) {
×
NEW
113
    if (fs.existsSync(envExamplePath)) {
×
NEW
114
      fs.copyFileSync(envExamplePath, envPath)
×
115
    } else {
NEW
116
      fs.writeFileSync(envPath, '', 'utf-8')
×
117
    }
118
  }
119

NEW
120
  const current = fs.readFileSync(envPath, 'utf-8')
×
121

NEW
122
  if (!needsSecretReplacement(readEnvSecret(current))) {
×
NEW
123
    return true
×
124
  }
125

126
  let secret: string
NEW
127
  try {
×
NEW
128
    secret = await resolveSecret(assumeYes)
×
129
  } catch (error) {
NEW
130
    if (error instanceof SetupCancelledError) {
×
NEW
131
      return false
×
132
    }
NEW
133
    throw error
×
134
  }
135

NEW
136
  fs.writeFileSync(envPath, upsertSecret(current, secret), 'utf-8')
×
NEW
137
  return true
×
138
}
139

140
export const runSetup = async (options: SetupOptions): Promise<number> => {
1✔
NEW
141
  setupPrompts.intro('Nostream setup')
×
142

NEW
143
  ensureConfigBootstrap()
×
NEW
144
  const shouldContinue = await ensureEnvFile(Boolean(options.yes))
×
NEW
145
  if (!shouldContinue) {
×
NEW
146
    return 1
×
147
  }
148

NEW
149
  let shouldStart = Boolean(options.start)
×
150

NEW
151
  if (!options.yes && !options.start && process.stdin.isTTY) {
×
NEW
152
    const answer = await setupPrompts.confirm({ message: 'Start relay now?', initialValue: true })
×
NEW
153
    if (setupPrompts.isCancel(answer)) {
×
NEW
154
      setupPrompts.cancel('Setup cancelled')
×
NEW
155
      return 1
×
156
    }
157

NEW
158
    shouldStart = answer
×
159
  }
160

NEW
161
  if (shouldStart) {
×
NEW
162
    const code = await runStart({}, [])
×
NEW
163
    setupPrompts.outro(code === 0 ? 'Setup complete' : 'Setup finished with errors')
×
NEW
164
    return code
×
165
  }
166

NEW
167
  setupPrompts.outro('Setup complete')
×
NEW
168
  return 0
×
169
}
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