生产形态是单机 local(DEFAULT_DEPLOY_MODE=local,env 未覆盖)⇒ K8s 分支
在 local 下本来不可达;且该形态与官方 dsh 基座、插件体系均无关
=> 整体下线,并移除该形态唯一的第三方依赖。
移除(备份在 D:/github/_dsh_shenxian_K8s后端备份_20260915/,含还原命令与
「集群化方案要复用的模板清单」):
src/supervisor/{k8s-spawner,leader,reconcile}.ts
src/fs/k8s-user-fs.ts · src/tcp-bridge.ts · src/web/file-service.ts
test/{k8s-spawner,leader}.test.mjs · scripts/smoke-file-service.mjs
改写调用方:cli.ts(5 条 import / 选主块 / file-service 与 tcp-bridge 两个
子命令 / dispatch / HELP)、web/server.ts(改为 fail-loud 守卫 + 恒用
LocalSpawner)、fs/provider.ts(只留 LocalUserFs)、package.json(测试与
smoke 入口),外加 3 处指向已删类型的悬空 JSDoc。
保留(集群化方案列为未来可选):deploy/ · poc/01-04 · Dockerfile.dsh ·
docs/k8s*.md(已加「代码已下线」状态横幅)· config.ts 的 K8s 配置字段与
DeployMode 联合类型。
验证:tsc --noEmit exit 0;npm test 36 测试 / 35 通过 / 0 失败 / 1 跳过;
npm run verify exit 0;依赖与被删符号全仓 0 命中。
180 lines
6.1 KiB
JavaScript
180 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Standalone orchestrator entry (`dshs` bin).
|
|
*
|
|
* Subcommands:
|
|
* dshs bootstrap-admin --username <u> --password <p>
|
|
* dshs [server flags]
|
|
* @module dshs/cli
|
|
*/
|
|
|
|
import { randomUUID } from 'node:crypto'
|
|
import { parseArgs } from 'node:util'
|
|
import { resolveConfig, type ConfigOverrides } from './config.js'
|
|
import { createDbAdapter } from './db/index.js'
|
|
import { createUserFs } from './fs/provider.js'
|
|
import { homeRoot, userRoot } from './fs/workspace.js'
|
|
import { hashPassword } from './web/auth.js'
|
|
import { hashUid } from './isolation.js'
|
|
import { buildServer } from './web/server.js'
|
|
|
|
const HELP = `dshs — DSH server login orchestrator
|
|
|
|
Usage:
|
|
dshs [options] start the server
|
|
dshs bootstrap-admin [options] create the first admin
|
|
|
|
Server options:
|
|
--port <n> Bind port (0 = ephemeral). Default 3080.
|
|
--host <h> Bind host. Default 127.0.0.1.
|
|
--db <path> SQLite database path.
|
|
--data-root <p> Root for per-user homes + workspaces.
|
|
--dsh-bin <cmd> Command used to launch a child DSH. Default "dsh".
|
|
--log-level <l> Pino log level. Default "info".
|
|
--secure-cookies Set the Secure flag on session cookies (behind HTTPS).
|
|
--session-ttl <s> Session lifetime in seconds. Default 604800 (7 days).
|
|
--isolation-mode <m> Isolation tier: "soft" or "account" (Linux, needs root). Default "soft".
|
|
-h, --help Show this help.
|
|
|
|
bootstrap-admin options:
|
|
--username <u> Admin username (required).
|
|
--password <p> Admin password (or DSHS_ADMIN_PASSWORD env).
|
|
--db <path> Database path.
|
|
--data-root <p> Root for per-user homes.
|
|
`
|
|
|
|
interface ParsedValues {
|
|
[key: string]: string | boolean | undefined
|
|
}
|
|
|
|
function toOverrides(values: ParsedValues): ConfigOverrides {
|
|
const str = (value: string | boolean | undefined): string | undefined =>
|
|
typeof value === 'string' ? value : undefined
|
|
const dshBin = str(values['dsh-bin'])
|
|
return {
|
|
port: str(values.port),
|
|
host: str(values.host),
|
|
dbPath: str(values.db),
|
|
dataRoot: str(values['data-root']),
|
|
dshCommand: dshBin ? [dshBin] : undefined,
|
|
logLevel: str(values['log-level']),
|
|
secureCookies: values['secure-cookies'] === true ? true : undefined,
|
|
sessionTtlSeconds: str(values['session-ttl']),
|
|
maxUploadBytes: str(values['max-upload']),
|
|
isolationMode: str(values['isolation-mode']),
|
|
}
|
|
}
|
|
|
|
async function bootstrapAdmin(args: string[]): Promise<void> {
|
|
const { values } = parseArgs({
|
|
args,
|
|
options: {
|
|
username: { type: 'string' },
|
|
password: { type: 'string' },
|
|
db: { type: 'string' },
|
|
'data-root': { type: 'string' },
|
|
},
|
|
})
|
|
const username = values.username
|
|
const password = values.password ?? process.env.DSHS_ADMIN_PASSWORD
|
|
if (username === undefined || username === '' || password === undefined || password === '') {
|
|
console.error('usage: dshs bootstrap-admin --username <u> --password <p>')
|
|
process.exit(2)
|
|
}
|
|
const config = resolveConfig({ dbPath: values.db, dataRoot: values['data-root'] })
|
|
const db = await createDbAdapter(config)
|
|
if ((await db.countAdmins()) > 0) {
|
|
console.error('an admin already exists; refusing to create a second one')
|
|
await db.close()
|
|
process.exit(1)
|
|
}
|
|
const id = randomUUID()
|
|
const homeDir = homeRoot(userRoot(config.dataRoot, id))
|
|
const passHash = await hashPassword(password)
|
|
// Create the user before initUserRoot so the per-user uid (baseUid + row_id)
|
|
// is already assigned — same reason as the register route.
|
|
const user = await db.createUser({ id, username, passHash, role: 'admin', homeDir })
|
|
await createUserFs(config).initUserRoot(id, user.uid ?? undefined)
|
|
await db.close()
|
|
console.log(`admin "${username}" created (id: ${id})`)
|
|
}
|
|
|
|
async function runServer(args: string[]): Promise<void> {
|
|
const { values } = parseArgs({
|
|
args,
|
|
options: {
|
|
port: { type: 'string' },
|
|
host: { type: 'string' },
|
|
db: { type: 'string' },
|
|
'data-root': { type: 'string' },
|
|
'dsh-bin': { type: 'string' },
|
|
'log-level': { type: 'string' },
|
|
'secure-cookies': { type: 'boolean' },
|
|
'session-ttl': { type: 'string' },
|
|
'max-upload': { type: 'string' },
|
|
'isolation-mode': { type: 'string' },
|
|
help: { type: 'boolean', short: 'h' },
|
|
},
|
|
})
|
|
|
|
if (values.help) {
|
|
process.stdout.write(HELP)
|
|
return
|
|
}
|
|
|
|
const config = resolveConfig(toOverrides(values as ParsedValues))
|
|
const app = await buildServer(config)
|
|
await app.listen({ host: config.host, port: config.port })
|
|
|
|
const address = app.server.address()
|
|
const actualPort = typeof address === 'object' && address !== null ? address.port : config.port
|
|
app.log.info(`dshs listening on http://${config.host}:${actualPort}`)
|
|
app.log.info(`data root: ${config.dataRoot}; db: ${config.dbPath}`)
|
|
|
|
const shutdown = async (signal: string): Promise<void> => {
|
|
app.log.info(`received ${signal}, shutting down`)
|
|
await app.close()
|
|
process.exit(0)
|
|
}
|
|
process.on('SIGINT', () => void shutdown('SIGINT'))
|
|
process.on('SIGTERM', () => void shutdown('SIGTERM'))
|
|
}
|
|
|
|
async function uidForUserCmd(args: string[]): Promise<void> {
|
|
const { values, positionals } = parseArgs({
|
|
args,
|
|
allowPositionals: true,
|
|
options: { 'base-uid': { type: 'string' }, db: { type: 'string' } },
|
|
})
|
|
const userId = positionals[0]
|
|
if (userId === undefined) {
|
|
console.error('usage: dshs uid-for-user <userId> [--db <path>] [--base-uid N]')
|
|
process.exit(2)
|
|
}
|
|
const dbPath = typeof values.db === 'string' ? values.db : undefined
|
|
const baseUid = typeof values['base-uid'] === 'string' ? values['base-uid'] : undefined
|
|
const config = resolveConfig({ dbPath, baseUid })
|
|
const db = await createDbAdapter(config)
|
|
const user = await db.findUserById(userId)
|
|
await db.close()
|
|
console.log(user?.uid ?? hashUid(userId, config.baseUid))
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const [first, ...rest] = process.argv.slice(2)
|
|
if (first === 'bootstrap-admin') {
|
|
await bootstrapAdmin(rest)
|
|
return
|
|
}
|
|
if (first === 'uid-for-user') {
|
|
await uidForUserCmd(rest)
|
|
return
|
|
}
|
|
await runServer(process.argv.slice(2))
|
|
}
|
|
|
|
main().catch((err: unknown) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|