#!/usr/bin/env node /** * ensure-workspace-picker-patch.cjs —— 把「工作区目录选择器收敛」平台段幂等写入各用户 profile patch。 * * 背景(档案 18 v3,2026-09-11 实测): * · 官方 dsh-web-app 的 `directory-picker` 行(@…-auto) 在启动时**连带挂载客户端对话框** * (@deepseek-ai/dsh-client-ui-directory-picker-browse);把它 disabled 会让对话框消失(点不动)。 * · 该 client 包自带 dsh.client 元数据,可**单独作为一行**插入 → 保留官方对话框 UI。 * · host 面(seam) 改由自建 @dsh-local/workspace-scoped-picker 提供:根固定为 /ws, * 越界(/etc、..、他人目录、符号链接逃逸)一律拒绝 → 路径框里手输也出不去。 * · 另由该插件的 client 面注入 CSS,隐藏「改路径」入口(crumbEditZone/Glyph)。 * * 用法: * node ensure-workspace-picker-patch.cjs # 全部用户(幂等) * node ensure-workspace-picker-patch.cjs --dry-run # 只打印计划 * node ensure-workspace-picker-patch.cjs --restart # 写后 kill 实例(由崩溃自愈拉起,读新 patch) * node ensure-workspace-picker-patch.cjs admin guest # 指定用户 * * 幂等:以 BEGIN/END 标记包裹平台段;已存在即跳过(不改内容)。 * 注意:本脚本**只追加平台段**,不触碰既有内容(角色 patch 等原样保留)。 */ const { execFileSync } = require('node:child_process') const { existsSync, readFileSync, writeFileSync } = require('node:fs') const { join } = require('node:path') const Database = require('/opt/dshs/node_modules/better-sqlite3') const DB_PATH = '/var/lib/dshs/dshs.db' const PROFILE = 'web' const BEGIN = '# >>> platform: workspace-scoped-picker (managed by ensure-workspace-picker-patch.cjs)' const END = '# <<< platform: workspace-scoped-picker' const DRY = process.argv.includes('--dry-run') const RESTART = process.argv.includes('--restart') const only = process.argv.slice(2).filter((a) => !a.startsWith('--')) const BLOCK = [ BEGIN, '# 目录选择器收敛:官方对话框 UI 保留(单独插入 client 面),host 面换成受限实现(根=自有 ws)', '- insert:', ' - id: workspace-scoped-picker', ' name: "@dsh-local/workspace-scoped-picker"', ' - id: ui-directory-picker-browse', ' name: "@deepseek-ai/dsh-client-ui-directory-picker-browse"', '- id: directory-picker', ' name: "@deepseek-ai/dsh-host-directory-picker-auto"', ' disabled: true', END, '', ].join('\n') const db = new Database(DB_PATH, { readonly: true }) const users = db .prepare('SELECT username, uid, home_dir FROM users') .all() .filter((u) => only.length === 0 || only.includes(u.username)) for (const user of users) { const patchPath = join(user.home_dir, 'profiles', PROFILE, 'cordis.patch.yml') if (!existsSync(patchPath)) { console.log(` ${user.username}: NO_PROFILE(用户未首登 spawn,先登录一次)`) continue } const current = readFileSync(patchPath, 'utf8') if (current.includes(BEGIN)) { console.log(` ${user.username}: skip(平台段已存在)`) continue } const body = current.trim() === '' || current.trim() === '[]' ? '' : current.trimEnd() + '\n\n' const next = body + BLOCK if (DRY) { console.log(` ${user.username}: [dry-run] 将写入 ${patchPath}`) continue } writeFileSync(patchPath, next, 'utf8') try { execFileSync('chown', [`${user.uid}:${user.uid}`, patchPath]) } catch {} console.log(` ${user.username}: wrote(已追加平台段)${RESTART ? ' + 重启实例' : ''}`) if (RESTART) { try { const out = execFileSync('ps', ['-eo', 'pid,user', '--no-headers'], { encoding: 'utf8' }) for (const line of out.split('\n')) { const [pid, uname] = line.trim().split(/\s+/) if (pid && uname === `dsh-${user.uid}`) { try { process.kill(Number(pid)) } catch {} } } } catch {} } } db.close() console.log('done')