synfull 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/scripts/synfull.cjs +116 -34
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@ synfull start
|
|
|
13
13
|
synfull open
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
During interactive `synfull setup`, you can choose whether to bind to `0.0.0.0` for access from other devices on your LAN; localhost-only (`127.0.0.1`) remains the default, and non-interactive setup stays local-only. On Linux, setup also offers to add your account to the `docker` group so the Docker integration can access the daemon. This is optional and grants root-equivalent control of the computer; decline if you do not fully trust the account with administrator-level Docker access. If Synfull's systemd service is installed, setup restarts it after group membership changes; otherwise, an interactive user session may need a logout/login before new group membership applies. LAN binding exposes the login page and server to every reachable network interface: use a trusted LAN, configure the host firewall to allow only intended devices/subnets, change the initial `admin` / `admin` password locally before signing in remotely, and do not expose Synfull directly to the public internet. npm installation alone does not register a background service or change system startup.
|
|
17
17
|
|
|
18
18
|
### Commands
|
|
19
19
|
|
|
20
|
-
- `synfull setup` prepares the isolated Python runtime and dependencies.
|
|
20
|
+
- `synfull setup` asks whether to allow LAN access and, on Linux, whether to grant the current account Docker-group access; it then prepares the isolated Python runtime and dependencies. Both choices are optional and are used by later starts/service registration. Docker group membership is root-equivalent. If an installed Synfull systemd service is present, setup restarts it after adding group access; otherwise log out and back in for a shell session to receive the new group membership.
|
|
21
21
|
- `synfull start` runs in the background; `synfull run` keeps the server in the foreground.
|
|
22
22
|
- `synfull status`, `synfull stop`, and `synfull open` manage a manually started instance.
|
|
23
23
|
- `synfull install-service` explicitly enables automatic start and restart. On Windows this registers a per-user logon scheduled task. On Linux it creates and enables a system-wide systemd service and therefore requires `sudo`; the service runs as your invoking user, not root.
|
|
@@ -29,7 +29,7 @@ Configuration:
|
|
|
29
29
|
|
|
30
30
|
- `SYNFULL_HOME` selects the CLI runtime/application directory.
|
|
31
31
|
- `SYNFULL_STATE_DIR` selects persistent database and app state.
|
|
32
|
-
- `SYNFULL_BIND_PORT` changes the
|
|
32
|
+
- `SYNFULL_BIND_PORT` changes the port (default `8000`). The bind host is selected during interactive setup and defaults to `127.0.0.1`; `SYNFULL_BIND_HOST=127.0.0.1` or `SYNFULL_BIND_HOST=0.0.0.0` can override it for a single run/service install.
|
|
33
33
|
|
|
34
34
|
## Development from source
|
|
35
35
|
|
package/package.json
CHANGED
package/scripts/synfull.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const crypto = require('node:crypto')
|
|
|
5
5
|
const fs = require('node:fs')
|
|
6
6
|
const os = require('node:os')
|
|
7
7
|
const path = require('node:path')
|
|
8
|
+
const readline = require('node:readline/promises')
|
|
8
9
|
const { spawn, spawnSync } = require('node:child_process')
|
|
9
10
|
const ROOT = path.resolve(__dirname, '..')
|
|
10
11
|
const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version
|
|
@@ -29,6 +30,7 @@ const STATE_DIR = process.env.SYNFULL_STATE_DIR || (WINDOWS
|
|
|
29
30
|
const RUNTIME = path.join(APP_DIR, '.venv', WINDOWS ? 'Scripts' : 'bin', WINDOWS ? 'python.exe' : 'python')
|
|
30
31
|
const PID_FILE = path.join(HOME, 'synfull.pid')
|
|
31
32
|
const LOG_FILE = path.join(HOME, 'synfull.log')
|
|
33
|
+
const CLI_CONFIG = path.join(HOME, 'config.json')
|
|
32
34
|
const CLI = path.resolve(__filename)
|
|
33
35
|
|
|
34
36
|
function run(command, args, options = {}) {
|
|
@@ -47,6 +49,83 @@ function pythonCommand() {
|
|
|
47
49
|
}
|
|
48
50
|
throw new Error('Python 3.10+ was not found. Install Python and ensure it is on PATH, then run `synfull setup`.')
|
|
49
51
|
}
|
|
52
|
+
function configuredBindHost() {
|
|
53
|
+
try {
|
|
54
|
+
const config = JSON.parse(fs.readFileSync(CLI_CONFIG, 'utf8'))
|
|
55
|
+
if (config.bindHost === '0.0.0.0') return '0.0.0.0'
|
|
56
|
+
} catch {}
|
|
57
|
+
return '127.0.0.1'
|
|
58
|
+
}
|
|
59
|
+
function selectedBindHost() {
|
|
60
|
+
const host = process.env.SYNFULL_BIND_HOST || configuredBindHost()
|
|
61
|
+
if (!['127.0.0.1', '0.0.0.0'].includes(host)) throw new Error('SYNFULL_BIND_HOST must be 127.0.0.1 or 0.0.0.0.')
|
|
62
|
+
return host
|
|
63
|
+
}
|
|
64
|
+
function saveBindHost(host) {
|
|
65
|
+
if (!['127.0.0.1', '0.0.0.0'].includes(host)) throw new Error('Bind host must be 127.0.0.1 or 0.0.0.0.')
|
|
66
|
+
fs.mkdirSync(HOME, { recursive: true })
|
|
67
|
+
fs.writeFileSync(CLI_CONFIG, `${JSON.stringify({ bindHost: host }, null, 2)}\n`, { mode: 0o600 })
|
|
68
|
+
}
|
|
69
|
+
async function configureBindHost(rl) {
|
|
70
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return
|
|
71
|
+
const answer = await rl.question('Allow other devices on your LAN to access Synfull by binding to 0.0.0.0? (y/N): ')
|
|
72
|
+
const host = /^y(es)?$/i.test(answer.trim()) ? '0.0.0.0' : '127.0.0.1'
|
|
73
|
+
saveBindHost(host)
|
|
74
|
+
console.log(host === '0.0.0.0'
|
|
75
|
+
? 'LAN access enabled. Change the initial admin password from this machine before signing in from another device; use a trusted LAN and configure your firewall.'
|
|
76
|
+
: 'Synfull will only accept connections from this machine.')
|
|
77
|
+
}
|
|
78
|
+
async function configureDockerAccess(rl, options = {}) {
|
|
79
|
+
const platform = options.platform || process.platform
|
|
80
|
+
const interactive = options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
81
|
+
const username = options.username || process.env.SUDO_USER || os.userInfo().username
|
|
82
|
+
const isRoot = options.isRoot ?? Boolean(process.getuid && process.getuid() === 0)
|
|
83
|
+
const spawnCommand = options.spawnSync || spawnSync
|
|
84
|
+
const runCommand = options.run || run
|
|
85
|
+
if (platform !== 'linux' || !interactive) return false
|
|
86
|
+
if (isRoot) {
|
|
87
|
+
console.log('Run `synfull setup` as your normal account (not sudo); setup will request sudo only if Docker group access is approved.')
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
const dockerGroup = spawnCommand('getent', ['group', 'docker'], { encoding: 'utf8', windowsHide: true })
|
|
91
|
+
if (dockerGroup.status !== 0) {
|
|
92
|
+
console.log('No Linux docker group was found. Install/configure Docker Engine first; Synfull did not change account permissions.')
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
console.log('Docker group membership grants root-equivalent control of this computer (including control over containers and host files).')
|
|
96
|
+
const answer = await rl.question(`Allow Synfull to use Docker by adding ${username} to the docker group? (y/N): `)
|
|
97
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
98
|
+
console.log('Docker group access was not changed. The Docker integration will remain unavailable unless your account already has socket access.')
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
const groups = spawnCommand('id', ['-nG', username], { encoding: 'utf8', windowsHide: true })
|
|
102
|
+
const alreadyMember = groups.status === 0 && groups.stdout.trim().split(/\s+/).includes('docker')
|
|
103
|
+
if (!alreadyMember) {
|
|
104
|
+
const command = process.getuid && process.getuid() === 0 ? 'usermod' : 'sudo'
|
|
105
|
+
const args = command === 'sudo' ? ['usermod', '-aG', 'docker', username] : ['-aG', 'docker', username]
|
|
106
|
+
runCommand(command, args)
|
|
107
|
+
console.log(`Added ${username} to the docker group.`)
|
|
108
|
+
} else {
|
|
109
|
+
console.log(`${username} is already in the docker group.`)
|
|
110
|
+
}
|
|
111
|
+
const serviceInstalled = spawnCommand('systemctl', ['cat', 'synfull-npm.service'], { stdio: 'ignore', windowsHide: true }).status === 0
|
|
112
|
+
if (!serviceInstalled) console.log('No Synfull systemd service is installed. Log out/in (or start a fresh session with Docker group membership) before running Synfull so it receives the new group permissions.')
|
|
113
|
+
return serviceInstalled
|
|
114
|
+
}
|
|
115
|
+
function restartSynfullService(runCommand = run) {
|
|
116
|
+
runCommand('sudo', ['systemctl', 'restart', 'synfull-npm.service'])
|
|
117
|
+
console.log('Restarted the Synfull system service so it picks up Docker group access.')
|
|
118
|
+
}
|
|
119
|
+
async function configureSetup() {
|
|
120
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false
|
|
121
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
122
|
+
try {
|
|
123
|
+
await configureBindHost(rl)
|
|
124
|
+
return await configureDockerAccess(rl)
|
|
125
|
+
} finally {
|
|
126
|
+
rl.close()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
50
129
|
function ensureRuntime() {
|
|
51
130
|
if (!fs.existsSync(path.join(ROOT, 'backend', 'app')) || !fs.existsSync(path.join(ROOT, 'frontend', 'dist', 'index.html'))) {
|
|
52
131
|
throw new Error('This npm package is missing its backend or built dashboard assets.')
|
|
@@ -64,9 +143,7 @@ function ensureRuntime() {
|
|
|
64
143
|
const pipAvailable = spawnSync(RUNTIME, ['-m', 'pip', '--version'], { stdio: 'ignore', windowsHide: true }).status === 0
|
|
65
144
|
if (!pipAvailable) {
|
|
66
145
|
const python = pythonCommand()
|
|
67
|
-
const repair = spawnSync(python.command, [...python.prefix, '-m', 'venv', '--upgrade-deps', '--upgrade', path.join(APP_DIR, '.venv')], {
|
|
68
|
-
encoding: 'utf8', windowsHide: true,
|
|
69
|
-
})
|
|
146
|
+
const repair = spawnSync(python.command, [...python.prefix, '-m', 'venv', '--upgrade-deps', '--upgrade', path.join(APP_DIR, '.venv')], { encoding: 'utf8', windowsHide: true })
|
|
70
147
|
if (repair.status !== 0) {
|
|
71
148
|
const detail = (repair.stderr || repair.stdout || '').trim()
|
|
72
149
|
if (detail) console.error(detail)
|
|
@@ -77,9 +154,7 @@ function ensureRuntime() {
|
|
|
77
154
|
}
|
|
78
155
|
}
|
|
79
156
|
const pipReady = spawnSync(RUNTIME, ['-m', 'pip', '--version'], { stdio: 'ignore', windowsHide: true }).status === 0
|
|
80
|
-
if (!pipReady)
|
|
81
|
-
throw new Error('The Python virtual environment still has no pip. Install the Python venv support package and recreate the runtime, then rerun `synfull setup`.')
|
|
82
|
-
}
|
|
157
|
+
if (!pipReady) throw new Error('The Python virtual environment still has no pip. Install the Python venv support package and recreate the runtime, then rerun `synfull setup`.')
|
|
83
158
|
const requirementsPath = path.join(APP_DIR, 'backend', 'requirements-runtime.txt')
|
|
84
159
|
const requirementsHash = crypto.createHash('sha256').update(fs.readFileSync(requirementsPath)).digest('hex')
|
|
85
160
|
const installedHashPath = path.join(APP_DIR, '.runtime-requirements-hash')
|
|
@@ -97,7 +172,7 @@ function ensureRuntime() {
|
|
|
97
172
|
function serverEnvironment() {
|
|
98
173
|
return {
|
|
99
174
|
...process.env,
|
|
100
|
-
SYNFULL_BIND_HOST:
|
|
175
|
+
SYNFULL_BIND_HOST: selectedBindHost(),
|
|
101
176
|
SYNFULL_BIND_PORT: process.env.SYNFULL_BIND_PORT || '8000',
|
|
102
177
|
SYNFULL_STATE_DIR: STATE_DIR,
|
|
103
178
|
SYNFULL_BOOTSTRAP_DEFAULT_ADMIN: '1',
|
|
@@ -106,9 +181,12 @@ function serverEnvironment() {
|
|
|
106
181
|
}
|
|
107
182
|
function startServer(foreground) {
|
|
108
183
|
ensureRuntime()
|
|
109
|
-
const
|
|
184
|
+
const env = serverEnvironment()
|
|
185
|
+
const bindHost = env.SYNFULL_BIND_HOST
|
|
186
|
+
const port = process.env.SYNFULL_BIND_PORT || '8000'
|
|
187
|
+
const args = ['-m', 'uvicorn', 'app.main:app', '--app-dir', path.join(APP_DIR, 'backend'), '--host', bindHost, '--port', port]
|
|
110
188
|
if (foreground) {
|
|
111
|
-
const child = spawn(RUNTIME, args, { cwd: APP_DIR, env
|
|
189
|
+
const child = spawn(RUNTIME, args, { cwd: APP_DIR, env, stdio: 'inherit', windowsHide: true })
|
|
112
190
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
|
|
113
191
|
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal))
|
|
114
192
|
return
|
|
@@ -118,8 +196,7 @@ function startServer(foreground) {
|
|
|
118
196
|
try { process.kill(pid, 0); console.log(`Synfull is already running (PID ${pid}).`); return } catch { fs.rmSync(PID_FILE, { force: true }) }
|
|
119
197
|
}
|
|
120
198
|
const output = fs.openSync(LOG_FILE, 'a')
|
|
121
|
-
const
|
|
122
|
-
const child = spawn(RUNTIME, args, { cwd: APP_DIR, env: serverEnvironment(), detached, stdio: ['ignore', output, output], windowsHide: true })
|
|
199
|
+
const child = spawn(RUNTIME, args, { cwd: APP_DIR, env, detached: !WINDOWS, stdio: ['ignore', output, output], windowsHide: true })
|
|
123
200
|
fs.closeSync(output)
|
|
124
201
|
child.once('error', error => {
|
|
125
202
|
fs.rmSync(PID_FILE, { force: true })
|
|
@@ -128,7 +205,8 @@ function startServer(foreground) {
|
|
|
128
205
|
})
|
|
129
206
|
child.once('spawn', () => {
|
|
130
207
|
fs.writeFileSync(PID_FILE, String(child.pid))
|
|
131
|
-
console.log(`Synfull started
|
|
208
|
+
console.log(`Synfull started on ${bindHost}:${port} (PID ${child.pid}).`)
|
|
209
|
+
if (bindHost === '0.0.0.0') console.log(`LAN clients can use http://<server-lan-ip>:${port}; change the initial admin password locally first.`)
|
|
132
210
|
})
|
|
133
211
|
child.unref()
|
|
134
212
|
console.log(`Logs: ${LOG_FILE}`)
|
|
@@ -153,43 +231,44 @@ function status() {
|
|
|
153
231
|
pid = fs.readFileSync(PID_FILE, 'utf8').trim()
|
|
154
232
|
try { process.kill(Number(pid), 0); running = true } catch { fs.rmSync(PID_FILE, { force: true }) }
|
|
155
233
|
}
|
|
234
|
+
const bindHost = selectedBindHost()
|
|
156
235
|
console.log(`Synfull ${VERSION}: ${running ? `running (PID ${pid})` : 'stopped'}`)
|
|
157
|
-
console.log(`Dashboard: http
|
|
236
|
+
console.log(`Dashboard: http://${bindHost === '0.0.0.0' ? '<server-lan-ip>' : '127.0.0.1'}:${process.env.SYNFULL_BIND_PORT || '8000'}`)
|
|
158
237
|
console.log(`Data: ${STATE_DIR}`)
|
|
159
238
|
}
|
|
160
239
|
function windowsTaskXml() {
|
|
161
|
-
const
|
|
240
|
+
const identity = spawnSync('whoami.exe', ['/user', '/fo', 'csv', '/nh'], { encoding: 'utf8', windowsHide: true })
|
|
241
|
+
const sid = (identity.stdout || '').match(/S-1-[0-9-]+/)
|
|
162
242
|
if (!sid) throw new Error('Could not determine the current Windows account SID.')
|
|
163
243
|
const exe = process.execPath.replace(/&/g, '&').replace(/</g, '<')
|
|
164
244
|
const cli = CLI.replace(/&/g, '&').replace(/</g, '<')
|
|
165
245
|
return `<?xml version="1.0" encoding="UTF-16"?>\n<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"><Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${sid[0]}</UserId></LogonTrigger></Triggers><Principals><Principal id="Author"><UserId>${sid[0]}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals><Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><Enabled>true</Enabled></Settings><Actions Context="Author"><Exec><Command>${exe}</Command><Arguments>"${cli}" run</Arguments><WorkingDirectory>${APP_DIR.replace(/&/g, '&').replace(/</g, '<')}</WorkingDirectory></Exec></Actions></Task>`
|
|
166
246
|
}
|
|
167
247
|
function installStartup() {
|
|
168
|
-
if (!WINDOWS && process.getuid && process.getuid() !== 0)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (!WINDOWS) {
|
|
172
|
-
const serviceUser = process.env.SUDO_USER || os.userInfo().username
|
|
173
|
-
if (serviceUser === 'root') throw new Error('Do not install Synfull as root. Re-run `sudo synfull install-service` from your normal account.')
|
|
174
|
-
}
|
|
248
|
+
if (!WINDOWS && process.getuid && process.getuid() !== 0) throw new Error('Installing the system-wide boot service requires administrator permission. Re-run with sudo; the service will still run as your original user.')
|
|
249
|
+
if (!WINDOWS && (process.env.SUDO_USER || os.userInfo().username) === 'root') throw new Error('Do not install Synfull as root. Re-run `sudo synfull install-service` from your normal account.')
|
|
250
|
+
saveBindHost(selectedBindHost())
|
|
175
251
|
ensureRuntime()
|
|
252
|
+
const bindHost = selectedBindHost()
|
|
253
|
+
const port = process.env.SYNFULL_BIND_PORT || '8000'
|
|
176
254
|
if (WINDOWS) {
|
|
177
255
|
const xmlPath = path.join(os.tmpdir(), `synfull-task-${process.pid}.xml`)
|
|
178
256
|
fs.writeFileSync(xmlPath, `\ufeff${windowsTaskXml()}`, 'utf16le')
|
|
179
257
|
try { run('schtasks.exe', ['/Create', '/TN', 'Synfull', '/XML', xmlPath, '/F']) } finally { fs.rmSync(xmlPath, { force: true }) }
|
|
180
|
-
console.log(
|
|
258
|
+
console.log(`Synfull will start at sign-in and restart after failures. Bind address: ${bindHost}.`)
|
|
181
259
|
} else {
|
|
182
260
|
const serviceUser = process.env.SUDO_USER || os.userInfo().username
|
|
183
261
|
fs.mkdirSync(STATE_DIR, { recursive: true })
|
|
184
262
|
run('chown', ['-R', serviceUser, HOME, STATE_DIR])
|
|
185
263
|
const escaped = value => value.replace(/\\/g, '\\\\').replace(/ /g, '\\x20').replace(/%/g, '%%')
|
|
186
|
-
const unit = `[Unit]\nDescription=Synfull
|
|
264
|
+
const unit = `[Unit]\nDescription=Synfull host monitoring\nAfter=network.target\n\n[Service]\nType=simple\nUser=${serviceUser}\nWorkingDirectory=${escaped(APP_DIR)}\nEnvironment=SYNFULL_BIND_HOST=${bindHost}\nEnvironment=SYNFULL_BIND_PORT=${port}\nEnvironment=SYNFULL_STATE_DIR=${escaped(STATE_DIR)}\nEnvironment=SYNFULL_BOOTSTRAP_DEFAULT_ADMIN=1\nEnvironment=PYTHONPATH=${escaped(path.join(APP_DIR, 'backend'))}\nExecStart=${escaped(RUNTIME)} -m uvicorn app.main:app --app-dir ${escaped(path.join(APP_DIR, 'backend'))} --host ${bindHost} --port ${port}\nRestart=always\nRestartSec=3\n\n[Install]\nWantedBy=multi-user.target\n`
|
|
187
265
|
const unitPath = '/etc/systemd/system/synfull-npm.service'
|
|
188
266
|
fs.writeFileSync(unitPath, unit, { mode: 0o644 })
|
|
189
267
|
try { run('systemctl', ['daemon-reload']); run('systemctl', ['enable', '--now', 'synfull-npm.service']) }
|
|
190
268
|
catch (error) { fs.rmSync(unitPath, { force: true }); throw error }
|
|
191
|
-
console.log(
|
|
269
|
+
console.log(`Synfull is installed as a systemd service and starts at boot. Bind address: ${bindHost}.`)
|
|
192
270
|
}
|
|
271
|
+
if (bindHost === '0.0.0.0') console.log('Allow access only from trusted LAN devices, configure your firewall, and change the initial admin password locally before remote sign-in.')
|
|
193
272
|
}
|
|
194
273
|
function uninstallStartup() {
|
|
195
274
|
if (WINDOWS) run('schtasks.exe', ['/Delete', '/TN', 'Synfull', '/F'])
|
|
@@ -197,29 +276,32 @@ function uninstallStartup() {
|
|
|
197
276
|
console.log('Synfull startup registration removed; application data was retained.')
|
|
198
277
|
}
|
|
199
278
|
function help() {
|
|
200
|
-
console.log(`Synfull ${VERSION}\n\nUsage: synfull <command>\n\nCommands:\n setup Prepare local runtime and install Python dependencies\n start Start Synfull in the background\n run Run Synfull in the foreground (for service managers)\n stop Stop a background Synfull process\n status Show status and local URLs\n open Open the local dashboard\n install-service Register start-at-login (Windows) or start-at-boot (Linux)\n uninstall-service Remove startup registration without deleting data\n\
|
|
279
|
+
console.log(`Synfull ${VERSION}\n\nUsage: synfull <command>\n\nCommands:\n setup Prepare local runtime and install Python dependencies\n start Start Synfull in the background\n run Run Synfull in the foreground (for service managers)\n stop Stop a background Synfull process\n status Show status and local URLs\n open Open the local dashboard\n install-service Register start-at-login (Windows) or start-at-boot (Linux)\n uninstall-service Remove startup registration without deleting data\n\nSetup asks whether to allow LAN connections; localhost-only is the default. Use a trusted LAN, open the port in your firewall only as needed, and change the initial admin password locally before remote sign-in.`)
|
|
201
280
|
}
|
|
202
|
-
function main() {
|
|
281
|
+
async function main() {
|
|
203
282
|
const command = process.argv[2]
|
|
204
283
|
if (command === '--version' || command === '-v') { console.log(VERSION); return }
|
|
205
284
|
if (command === '--help' || command === '-h') { help(); return }
|
|
206
285
|
if (!['win32', 'linux'].includes(process.platform)) throw new Error('Synfull npm CLI currently supports Windows and Linux.')
|
|
207
|
-
if (command === 'setup') {
|
|
286
|
+
if (command === 'setup') {
|
|
287
|
+
const restartServiceForDocker = await configureSetup()
|
|
288
|
+
ensureRuntime()
|
|
289
|
+
if (restartServiceForDocker) restartSynfullService()
|
|
290
|
+
console.log(`Runtime ready. Data directory: ${STATE_DIR}`)
|
|
291
|
+
}
|
|
208
292
|
else if (command === 'start') startServer(false)
|
|
209
293
|
else if (command === 'run') startServer(true)
|
|
210
294
|
else if (command === 'stop') stopServer()
|
|
211
295
|
else if (command === 'status') status()
|
|
212
296
|
else if (command === 'open') {
|
|
213
|
-
const
|
|
297
|
+
const host = selectedBindHost()
|
|
298
|
+
const url = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${process.env.SYNFULL_BIND_PORT || '8000'}`
|
|
214
299
|
if (WINDOWS) spawn('cmd.exe', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref()
|
|
215
|
-
else spawn(
|
|
300
|
+
else spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref()
|
|
216
301
|
}
|
|
217
302
|
else if (command === 'install-service') installStartup()
|
|
218
303
|
else if (command === 'uninstall-service') uninstallStartup()
|
|
219
304
|
else help()
|
|
220
305
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
try { main() } catch (error) { console.error(`Synfull: ${error.message}`); process.exitCode = 1 }
|
|
224
|
-
}
|
|
225
|
-
module.exports = { ensureRuntime, serverEnvironment, APP_DIR, STATE_DIR, ROOT }
|
|
306
|
+
if (require.main === module) main().catch(error => { console.error(`Synfull: ${error.message}`); process.exitCode = 1 })
|
|
307
|
+
module.exports = { ensureRuntime, serverEnvironment, configuredBindHost, selectedBindHost, saveBindHost, configureDockerAccess, restartSynfullService, APP_DIR, STATE_DIR, ROOT }
|