Skip to content

Commit cf434b4

Browse files
committed
feat(cli): prompt for password on first launch
1 parent 31abc9c commit cf434b4

6 files changed

Lines changed: 277 additions & 30 deletions

File tree

docs/deployment/README.en.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ Why this is recommended:
3535

3636
After the first launch, the app creates `auth.json` inside the app data directory.
3737

38+
If you use the CLI defaults, the first launch starts with these values automatically:
39+
40+
- `publicMode`: `true`
41+
- `rootPath`: `~/coder-studio-workspaces`
42+
- `bindHost`: `127.0.0.1`
43+
- `bindPort`: `41033`
44+
- `sessionIdleMinutes`: `15`
45+
- `sessionMaxHours`: `12`
46+
47+
That means you do not need to preconfigure `rootPath`, `bindHost`, or `bindPort` just to get started. In the default case, setting the password is enough.
48+
3849
Typical locations:
3950

4051
- Linux: `~/.local/share/com.spencerkit.coderstudio/auth.json`
@@ -135,25 +146,35 @@ server {
135146
## Deployment Steps
136147

137148
1. Build the app: `pnpm tauri build`
138-
2. Start it once on the target machine so it generates `auth.json`
139-
3. Edit `auth.json`, or use the CLI, and set at least:
140-
- `password`
141-
- `rootPath`
142-
- `bindHost`
143-
- `bindPort`
144-
4. Restart the app
145-
5. Configure an HTTPS reverse proxy to `bindHost:bindPort`
146-
6. Open your domain and verify the login screen appears first
147-
148-
Recommended CLI flow:
149+
2. Install the CLI on the target machine: `npm install -g @spencer-kit/coder-studio`
150+
3. Set the access passphrase
151+
4. Override `rootPath`, `bindHost`, or `bindPort` only if you need non-default values
152+
5. Start or restart the app
153+
6. Configure an HTTPS reverse proxy to `bindHost:bindPort`
154+
7. Open your domain and verify the login screen appears first
155+
156+
Minimal setup:
157+
158+
```bash
159+
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
160+
coder-studio start
161+
```
162+
163+
Notes:
164+
165+
- if you accept the default workspace directory and bind address, the two commands above are enough
166+
- on an interactive terminal, the first `coder-studio start`, `coder-studio restart`, or `coder-studio open` also prompts for a passphrase if none is configured yet, then continues startup
167+
- non-interactive environments do not enter the prompt flow, so configure the passphrase first with `coder-studio config password set --stdin`
168+
169+
Use this flow only when you want custom paths or bind settings:
149170

150171
```bash
151172
coder-studio config root set /srv/coder-studio/workspaces
152173
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
153174
coder-studio config auth public-mode on
154175
coder-studio config set server.host 127.0.0.1
155176
coder-studio config set server.port 41033
156-
coder-studio restart
177+
coder-studio start
157178
```
158179

159180
## Verification Checklist

docs/deployment/README.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535

3636
应用首次启动后,会在 app data 目录生成 `auth.json`
3737

38+
如果你直接使用 CLI 默认配置,首次启动会自动带上这些默认值:
39+
40+
- `publicMode`: `true`
41+
- `rootPath`: `~/coder-studio-workspaces`
42+
- `bindHost`: `127.0.0.1`
43+
- `bindPort`: `41033`
44+
- `sessionIdleMinutes`: `15`
45+
- `sessionMaxHours`: `12`
46+
47+
也就是说,默认情况下你不需要先手动设置 `rootPath``bindHost``bindPort`;只要把密码设好,就可以先启动。
48+
3849
常见位置:
3950

4051
- Linux:`~/.local/share/com.spencerkit.coderstudio/auth.json`
@@ -135,25 +146,35 @@ server {
135146
## 部署步骤
136147

137148
1. 构建应用:`pnpm tauri build`
138-
2. 在目标机器上先启动一次应用,让它生成 `auth.json`
139-
3. 编辑 `auth.json`,或者直接使用 CLI,至少设置:
140-
- `password`
141-
- `rootPath`
142-
- `bindHost`
143-
- `bindPort`
144-
4. 重启应用
145-
5. 配置 HTTPS 反向代理到 `bindHost:bindPort`
146-
6. 打开你的域名,确认先出现登录页
147-
148-
推荐直接使用 CLI:
149+
2. 在目标机器上安装 CLI:`npm install -g @spencer-kit/coder-studio`
150+
3. 设置访问口令
151+
4. 如有需要,再覆盖默认的 `rootPath``bindHost``bindPort`
152+
5. 启动或重启应用
153+
6. 配置 HTTPS 反向代理到 `bindHost:bindPort`
154+
7. 打开你的域名,确认先出现登录页
155+
156+
最小可用配置:
157+
158+
```bash
159+
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
160+
coder-studio start
161+
```
162+
163+
说明:
164+
165+
- 如果你接受默认目录和默认监听地址,上面两条命令就够了
166+
- 交互式终端里首次执行 `coder-studio start``coder-studio restart``coder-studio open` 时,如果还没设置密码,CLI 也会先提示你输入并确认密码,然后再继续启动
167+
- 非交互环境下不会进入提示流程,这时应先执行 `coder-studio config password set --stdin`
168+
169+
需要自定义目录或监听地址时,再使用:
149170

150171
```bash
151172
coder-studio config root set /srv/coder-studio/workspaces
152173
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
153174
coder-studio config auth public-mode on
154175
coder-studio config set server.host 127.0.0.1
155176
coder-studio config set server.port 41033
156-
coder-studio restart
177+
coder-studio start
157178
```
158179

159180
## 验证清单

docs/development/cli.en.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,19 +342,33 @@ The CLI exposes one config view, but it persists values into two files:
342342
- `root.path` is written into `auth.json` as the single-root `rootPath` field
343343
- legacy `allowedRoots` values are still read for compatibility, but CLI writes back only `rootPath`
344344
- `auth.password` is hidden in `show` and `get`
345+
- with the default configuration, the CLI uses:
346+
- `server.host = 127.0.0.1`
347+
- `server.port = 41033`
348+
- `root.path = ~/coder-studio-workspaces`
349+
- `auth.publicMode = true`
345350
- while the runtime is already running:
346351
- `root.path`, `auth.publicMode`, `auth.password`, and session lifetime settings are applied immediately
347352
- `server.host` and `server.port` are persisted immediately, but require `restart` before the bind address changes
348353
- changing `auth.password` or `auth.publicMode` clears current authenticated sessions
354+
- in an interactive terminal, if this is the first launch and `auth.password` is still missing, `start`, `restart`, and `open` prompt for a new passphrase and confirmation before continuing startup
355+
- in `--json`, CI, or other non-interactive environments, the prompt flow is skipped; configure the passphrase first with `coder-studio config password set --stdin`
349356

350357
### Common Examples
351358

352-
Minimal public-mode setup:
359+
Start with the default configuration:
360+
361+
```bash
362+
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
363+
coder-studio start
364+
```
365+
366+
Minimal setup when you want a custom workspace root:
353367

354368
```bash
355369
coder-studio config root set /srv/coder-studio/workspaces
356370
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
357-
coder-studio config auth public-mode on
371+
coder-studio start
358372
```
359373

360374
Change the port and restart:

docs/development/cli.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,19 +342,33 @@ CLI 对外只暴露一套配置视图,但底层分成两个文件:
342342
- `root.path` 会以单根目录模型写入 `auth.json``rootPath`
343343
- 旧版本 `allowedRoots` 配置仍可读取,但 CLI 写回时只会写 `rootPath`
344344
- `auth.password``show` / `get` 中不会返回明文
345+
- 默认配置下,CLI 会使用:
346+
- `server.host = 127.0.0.1`
347+
- `server.port = 41033`
348+
- `root.path = ~/coder-studio-workspaces`
349+
- `auth.publicMode = true`
345350
- 运行时已经启动时:
346351
- `root.path``auth.publicMode``auth.password`、会话时长配置会即时写入并立即生效
347352
- `server.host``server.port` 会写入配置,但需要 `restart` 后才会真正改变监听地址
348353
- 修改 `auth.password``auth.publicMode` 时,当前活跃登录会话会被清空
354+
- 在交互式终端中,如果这是首次启动且 `auth.password` 仍未配置,`start``restart``open` 会先提示输入并确认密码,然后再继续启动
355+
-`--json`、CI 或其他非交互环境中,不会进入提示流程;首次启动前请先执行 `coder-studio config password set --stdin`
349356

350357
### 常用示例
351358

352-
设置公网模式的最小配置:
359+
使用默认配置直接启动:
360+
361+
```bash
362+
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
363+
coder-studio start
364+
```
365+
366+
设置自定义目录时的最小配置:
353367

354368
```bash
355369
coder-studio config root set /srv/coder-studio/workspaces
356370
printf '%s' 'replace-this-passphrase' | coder-studio config password set --stdin
357-
coder-studio config auth public-mode on
371+
coder-studio start
358372
```
359373

360374
切换监听端口并重启:

packages/cli/src/lib/cli.mts

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// @ts-nocheck
22
import fs from 'node:fs/promises';
3+
import path from 'node:path';
4+
import { emitKeypressEvents } from 'node:readline';
35
import {
46
generateCompletionScript,
57
installCompletionScript,
@@ -41,6 +43,7 @@ import { readPackageVersion } from './state.mjs';
4143
const EXIT_SUCCESS = 0;
4244
const EXIT_FAILURE = 1;
4345
const EXIT_USAGE = 2;
46+
const RUNTIME_DB_FILENAME = 'coder-studio.db';
4447

4548
class CliError extends Error {
4649
constructor(message, { exitCode = EXIT_FAILURE, helpTopic = null } = {}) {
@@ -575,6 +578,127 @@ async function readSecretFromStdin() {
575578
return Buffer.concat(chunks).toString('utf8').trimEnd();
576579
}
577580

581+
async function pathExists(filePath) {
582+
try {
583+
await fs.access(filePath);
584+
return true;
585+
} catch {
586+
return false;
587+
}
588+
}
589+
590+
async function promptHiddenInput(label) {
591+
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== 'function') {
592+
throw new CliError('interactive password setup requires a TTY', { exitCode: EXIT_FAILURE });
593+
}
594+
595+
const stdin = process.stdin;
596+
const stdout = process.stdout;
597+
const wasRaw = Boolean(stdin.isRaw);
598+
599+
emitKeypressEvents(stdin);
600+
stdin.resume();
601+
if (!wasRaw) {
602+
stdin.setRawMode(true);
603+
}
604+
605+
stdout.write(label);
606+
607+
return await new Promise((resolve, reject) => {
608+
let value = '';
609+
610+
const cleanup = () => {
611+
stdin.off('keypress', onKeypress);
612+
if (!wasRaw) {
613+
stdin.setRawMode(false);
614+
}
615+
stdout.write('\n');
616+
};
617+
618+
const finish = (callback) => {
619+
cleanup();
620+
callback();
621+
};
622+
623+
const onKeypress = (chunk, key = {}) => {
624+
if (key.ctrl && (key.name === 'c' || key.name === 'd')) {
625+
finish(() => reject(new CliError('initial password setup cancelled', { exitCode: EXIT_FAILURE })));
626+
return;
627+
}
628+
629+
if (key.name === 'return' || key.name === 'enter') {
630+
finish(() => resolve(value));
631+
return;
632+
}
633+
634+
if (key.name === 'backspace' || key.name === 'delete') {
635+
value = value.slice(0, -1);
636+
return;
637+
}
638+
639+
if (typeof chunk === 'string' && chunk.length > 0 && !key.ctrl && !key.meta) {
640+
value += chunk;
641+
}
642+
};
643+
644+
stdin.on('keypress', onKeypress);
645+
});
646+
}
647+
648+
async function ensureInitialPasswordConfigured(context, flags) {
649+
const status = await getStatus(context.options);
650+
if (runtimeIsActive(status)) {
651+
return context;
652+
}
653+
654+
const needsPassword = context.config.values.auth.publicMode && !context.config.values.auth.passwordConfigured;
655+
if (!needsPassword) {
656+
return context;
657+
}
658+
659+
const dbPath = path.join(context.config.paths.dataDir, RUNTIME_DB_FILENAME);
660+
if (await pathExists(dbPath)) {
661+
return context;
662+
}
663+
664+
if (flags.json || !process.stdin.isTTY || !process.stdout.isTTY) {
665+
throw new CliError(
666+
'first launch requires configuring auth.password before start; run `coder-studio config password set --stdin` and retry',
667+
{ exitCode: EXIT_FAILURE },
668+
);
669+
}
670+
671+
console.log('First launch detected. Set an access password before starting Coder Studio.');
672+
673+
while (true) {
674+
const password = await promptHiddenInput('New password: ');
675+
if (!password.trim()) {
676+
console.log('Password cannot be empty.');
677+
continue;
678+
}
679+
680+
const confirmation = await promptHiddenInput('Confirm password: ');
681+
if (password !== confirmation) {
682+
console.log('Passwords do not match. Try again.');
683+
continue;
684+
}
685+
686+
await updateLocalConfig(
687+
{ stateDir: context.config.paths.stateDir, dataDir: context.config.paths.dataDir },
688+
{ 'auth.password': password },
689+
);
690+
691+
console.log('Password saved. Starting Coder Studio...');
692+
return {
693+
...context,
694+
config: await loadLocalConfig({
695+
stateDir: context.config.paths.stateDir,
696+
dataDir: context.config.paths.dataDir,
697+
}),
698+
};
699+
}
700+
}
701+
578702
async function applyConfigUpdate(context, key, rawValue, { unset = false } = {}) {
579703
const status = await getStatus(context.options);
580704
if (runtimeIsActive(status) && status.managed && isRuntimeConfigKey(key)) {
@@ -1049,10 +1173,12 @@ export async function runCli(argv = process.argv.slice(2)) {
10491173
return await handleAuthCommand(positionals, flags, context);
10501174
}
10511175

1052-
const context = await resolveCommandContext(flags);
1053-
const options = context.options;
1176+
let context = await resolveCommandContext(flags);
1177+
let options = context.options;
10541178

10551179
if (command === 'start') {
1180+
context = await ensureInitialPasswordConfigured(context, flags);
1181+
options = context.options;
10561182
const result = await startRuntime({
10571183
...options,
10581184
foreground: Boolean(flags.foreground),
@@ -1084,6 +1210,8 @@ export async function runCli(argv = process.argv.slice(2)) {
10841210
}
10851211

10861212
if (command === 'restart') {
1213+
context = await ensureInitialPasswordConfigured(context, flags);
1214+
options = context.options;
10871215
const result = await restartRuntime(options);
10881216
if (flags.json) printJson(result);
10891217
else {
@@ -1112,6 +1240,8 @@ export async function runCli(argv = process.argv.slice(2)) {
11121240
}
11131241

11141242
if (command === 'open') {
1243+
context = await ensureInitialPasswordConfigured(context, flags);
1244+
options = context.options;
11151245
const result = await openRuntime(options);
11161246
if (flags.json) printJson(result);
11171247
else console.log(`opened: ${result.endpoint}`);

0 commit comments

Comments
 (0)