Files
dsh_ai1net_server/src/cli.ts
T
admin cf8b7b1f5c chore(k8s): 下线 K8s 后端形态,移除依赖 @kubernetes/client-node
生产形态是单机 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 命中。
2026-09-15 06:36:20 +08:00

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)
})