source-code-mgmt 1.11.0 → 1.14.0

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/lib/index.js CHANGED
@@ -25,10 +25,62 @@ import { join, dirname } from 'node:path'
25
25
  import { homedir } from 'node:os'
26
26
 
27
27
  import { AsyncLocalStorage } from 'node:async_hooks'
28
+ import { fileURLToPath } from 'node:url'
28
29
 
29
30
  // ---------- i18n: host messages follow the client’s language (?lang= query param, per request) ----------
30
31
  const HOST_EN = {"尚未配置 Gitee 私人令牌(请打开 ③ 代码管理输入令牌)":"No Gitee personal token configured (enter one in ③ Code Management)","Gitee API 请求失败(需要 curl)":"Gitee API request failed (curl required)","not a git repository(尚未 git init,可用「新建仓库并推送」初始化为 git 仓库并上传)":"not a git repository (run \"Create repo & push\" to init one and upload)","超过 GitHub 100MB 单文件限制(${}),已被 .gitignore 的 \"${}\" 排除":"Exceeds GitHub's 100MB file limit (${}) — already excluded by .gitignore entry \"${}\"","超过 GitHub 100MB 单文件限制(${}),${} 所在的一级目录「${}」整体忽略(该文件夹为一整体)":"Exceeds GitHub's 100MB file limit (${}) — the whole first-level folder \"${}\" of ${} is ignored (the folder is one unit)","超过 GitHub 100MB 单文件限制(${}),已忽略单个文件「${}」":"Exceeds GitHub's 100MB file limit (${}) — the file \"${}\" was ignored individually","尚未配置远程仓库 origin,请使用「新建仓库」创建远程仓库。":"No origin remote configured — use \"Create repo\" to create a remote repo.","not a git repository(尚未 git init,无法拉取)":"not a git repository (no git init yet — cannot pull)","尚未配置远程仓库 origin,无法拉取。":"No origin remote configured — cannot pull.","拉取存在冲突:本地有未合并改动或与远程冲突,请手动 git pull 处理合并。":"Pull conflicts: you have unmerged local changes or conflicts with the remote — resolve them manually with git pull.","git pull 失败":"git pull failed","not a git repository(尚未 git init)":"not a git repository (run git init first)","拉取并推送失败:合并存在冲突,请手动解决后重试。":"Pull-and-push failed: merge conflicts — resolve them manually and retry.","pull --rebase 失败":"pull --rebase failed","push 失败":"push failed","push --force 失败":"push --force failed","强制拉取存在冲突,请手动处理。":"Force-pull conflicts — handle them manually.","git pull --force 失败":"git pull --force failed","缺少仓库名称":"Repo name required","仓库名称包含非法字符(仅允许字母、数字、点、下划线、横线)":"Repo name contains invalid characters (only letters, digits, dots, underscores, dashes)","git add 失败:${}":"git add failed: ${}","需要先配置 Gitee 私人令牌":"Configure the Gitee personal token first","Gitee 创建仓库失败":"Failed to create the Gitee repo","缺少文件路径":"File path required","无法识别当前分支":"Cannot determine the current branch","尚未配置远程仓库 origin,无法对齐。":"No origin remote configured — cannot align.","git fetch 失败":"git fetch failed","git reset --hard 失败":"git reset --hard failed","git init 失败":"git init failed","git add 失败":"git add failed","git reset 失败":"git reset failed","提交信息不能为空":"Commit message cannot be empty","没有已暂存(staged)的改动可提交":"Nothing staged to commit","git commit 失败":"git commit failed","缺少分支名":"Branch name required","切换分支失败":"Failed to switch branch","git log 失败":"git log failed","缺少 commit hash":"Commit hash required","revert 失败(可能有冲突,请手动处理)":"revert failed (possible conflicts — handle manually)","cherry-pick 失败(可能有冲突,请手动处理)":"cherry-pick failed (possible conflicts — handle manually)","读取提交 diff 失败":"Failed to read the commit diff","未知工具:":"Unknown tool: ","未找到可用的包管理器(":"No usable package manager found (",")。请手动安装。":"). Please install manually.","安装成功":"Installed successfully","安装失败":"Installation failed","$dlg.Description = '选择本地目录';":"$dlg.Description = 'Choose a local folder';","Gitee 修改可见性失败":"Failed to change Gitee visibility","需要先登录 GitHub CLI(gh)":"Log in to the GitHub CLI (gh) first","修改可见性失败:${}":"Failed to change visibility: ${}","gh repo edit 失败":"gh repo edit failed","缺少目录路径":"Folder path required","未选择目录":"No folder selected","目录不存在或不是文件夹:":"Folder does not exist or is not a directory: ","令牌不能为空":"Token cannot be empty","Gitee 令牌无效,请检查(需有 projects 权限)":"Invalid Gitee token — check it (needs projects permission)"};
31
32
  const requestLangStore = new AsyncLocalStorage();
33
+ // 新增板块(④克隆 / ⑤npm)的 host 英文消息,追加进 HOST_EN。
34
+ Object.assign(HOST_EN, {
35
+ "LLM 服务不可用(未加载 llm 服务)": "LLM service unavailable (llm service not loaded)",
36
+ "未找到默认模型配置(agent-default-model)": "No default model config found (agent-default-model)",
37
+ "目标目录不存在:": "Target directory does not exist: ",
38
+ "已存在同名目录:": "A directory with that name already exists: ",
39
+ "git clone 失败": "git clone failed",
40
+ "无法识别仓库地址:": "Cannot parse repo URL: ",
41
+ "没有可用的 PR 步骤(请先让 AI 分析规则)": "No PR steps available (run AI rule analysis first)",
42
+ "确保 fork": "Ensure fork",
43
+ "已 fork 或 fork 已存在": "Forked already, or fork exists",
44
+ "克隆 fork": "Clone fork",
45
+ "已存在本地副本:": "Local copy already exists: ",
46
+ "已克隆到 ": "Cloned to ",
47
+ "克隆 fork 失败": "Failed to clone fork",
48
+ "添加上游": "Add upstream",
49
+ "拉取上游": "Fetch upstream",
50
+ "拉取失败": "Fetch failed",
51
+ "新建分支": "Create branch",
52
+ "创建分支失败": "Failed to create branch",
53
+ "写入条目": "Write entry",
54
+ "缺少条目文件路径": "Missing entry file path",
55
+ "重生成 README": "Regenerate README",
56
+ "无命令,跳过": "No command, skipped",
57
+ "执行失败": "Command failed",
58
+ "提交": "Commit",
59
+ "推送": "Push",
60
+ "创建 PR": "Create PR",
61
+ "创建 PR 失败": "Failed to create PR",
62
+ "未知步骤": "Unknown step",
63
+ "未知步骤类型": "Unknown step type",
64
+ "缺少仓库地址": "Repo URL required",
65
+ "未登录 npm": "Not logged in to npm",
66
+ "npm publish 已执行": "npm publish executed",
67
+ "npm publish 失败": "npm publish failed",
68
+ "已提交": "Committed",
69
+ "已创建 PR": "PR created",
70
+ "推送上游失败": "Failed to push",
71
+ "AI 未输出有效规则": "AI produced no valid rules",
72
+ "AI 输出无法解析": "AI output could not be parsed",
73
+ "推送失败": "Push failed",
74
+ "条目文件路径不安全(已拦截)": "Entry file path unsafe (blocked)",
75
+ "重生成命令不安全(已拦截)": "Regenerate command unsafe (blocked)",
76
+ "创建分支失败(上游分支可能不存在,请检查规则 baseBranch)": "Failed to create branch (upstream base may not exist — check the rule's baseBranch)",
77
+ "已打开终端窗口,请完成 npm login 后回到面板点「重新检查」": "A terminal window has been opened — complete npm login, then click \"Re-check\" in the panel",
78
+ "未找到可用终端,请手动在目标目录运行:": "No usable terminal found — run this manually in the target directory: ",
79
+ "规则文件获取失败:": "Failed to fetch rules file: ",
80
+ "规则文件读取失败:": "Failed to read rules file: ",
81
+ "找不到规则文件:": "Rules file not found: ",
82
+ "未获取到规则内容(文件为空或无法读取)": "Could not read any rule content (file empty or unreadable)",
83
+ })
32
84
  function langOf(req) { try { const l = new URL(req.url ?? "", "http://localhost").searchParams.get("lang"); return l === "en" ? "en" : "zh" } catch { return "zh" } }
33
85
  function lang() { return requestLangStore.getStore() ?? "zh" }
34
86
  function tr(key, params) {
@@ -83,6 +135,8 @@ const SSH = resolveBin('ssh', 'DSH_SCM_SSH')
83
135
  const SSH_KEYGEN = resolveBin('ssh-keygen', 'DSH_SCM_SSH_KEYGEN')
84
136
  /** Resolved path to curl (used to call the Gitee OpenAPI). */
85
137
  const CURL = resolveBin('curl', 'DSH_SCM_CURL')
138
+ /** Resolved path to npm (used by the ⑤ publish-npm section). */
139
+ const NPM = resolveBin('npm', 'DSH_SCM_NPM')
86
140
 
87
141
  /**
88
142
  * Preferred ssh for git remote operations (injected via GIT_SSH).
@@ -305,8 +359,13 @@ function run(cmd, args, opts = {}) {
305
359
  // `-c core.quotepath=false` 让 git 直接输出原始 UTF-8 路径,Node 以 utf8
306
360
  // 解码后即得到正确的中文文件名(git diff / git show 头部路径也一并修复)。
307
361
  const finalArgs = cmd === GIT ? ['-c', 'core.quotepath=false', ...args] : args
362
+ // Windows + shell 模式:execFileSync 把 cmd 与参数拼成一条命令行交给 cmd.exe。
363
+ // 若可执行文件路径含空格(如 "C:\Program Files\nodejs\npm.cmd"),不引号包裹会
364
+ // 被截断成 "'C:\Program' 不是内部命令"。这里只包裹可执行文件本身(参数保持数组,
365
+ // 由 execFileSync 逐个安全拼接),不触碰用户输入,无注入面。
366
+ const execCmd = (opts.shell && IS_WIN && /\s/.test(cmd)) ? '"' + cmd + '"' : cmd
308
367
  try {
309
- const out = execFileSync(cmd, finalArgs, {
368
+ const out = execFileSync(execCmd, finalArgs, {
310
369
  encoding: 'utf8',
311
370
  stdio: ['ignore', 'pipe', 'pipe'],
312
371
  timeout: opts.timeout ?? 30_000,
@@ -1799,6 +1858,326 @@ function installToolFlow(tool) {
1799
1858
  }
1800
1859
  }
1801
1860
 
1861
+ // ---------------------------------------------------------------------------
1862
+ // ④ 克隆远程仓库 / ⑤ 发布 npm 包 —— host 逻辑
1863
+ // ---------------------------------------------------------------------------
1864
+
1865
+ // ---------- LLM 辅助:用 DSH 的 llm 服务 + 默认模型(当前对话/默认模型) ----------
1866
+
1867
+ /**
1868
+ * 读取 DSH 默认模型配置(agent-default-model)。优先走 settings 服务(ctx.settings
1869
+ * 在 host 侧同进程可用),拿不到时兜底解析 ~/.dsh/settings.yaml 的该段。
1870
+ * @returns {{provider: string, model: string}|undefined}
1871
+ */
1872
+ function resolveDefaultModel(ctx) {
1873
+ try {
1874
+ if (ctx && typeof ctx.get === 'function') {
1875
+ const settings = ctx.get('settings')
1876
+ if (settings && typeof settings.get === 'function') {
1877
+ const v = settings.get('agent-default-model')
1878
+ if (v && typeof v === 'object' && typeof v.provider === 'string' && v.provider && typeof v.model === 'string' && v.model) {
1879
+ return { provider: v.provider, model: v.model }
1880
+ }
1881
+ }
1882
+ }
1883
+ } catch { /* fall through */ }
1884
+ try {
1885
+ const file = join(homedir(), '.dsh', 'settings.yaml')
1886
+ if (existsSync(file)) {
1887
+ const raw = readFileSync(file, 'utf8')
1888
+ // 捕获 `agent-default-model:` 后所有缩进(开头空白)的连续行,直到下一个顶格键。
1889
+ const m = /^\s*agent-default-model\s*:\s*\n((?:[ \t]+[^\n]*(?:\n|$))*)/m.exec(raw)
1890
+ if (m) {
1891
+ const block = m[1]
1892
+ const prov = /^\s*provider\s*:\s*"?([^\s"#]+)"?/m.exec(block)
1893
+ const mod = /^\s*model\s*:\s*"?([^\s"#]+)"?/m.exec(block)
1894
+ if (prov && mod) return { provider: prov[1], model: mod[1] }
1895
+ }
1896
+ }
1897
+ } catch { /* ignore */ }
1898
+ return undefined
1899
+ }
1900
+
1901
+ /**
1902
+ * 调用 DSH 的 LLM(默认模型),收集完整文本。messages 传 { role: 'user'|'assistant', text } 简写。
1903
+ * @param {*} ctx - cordis context(apply 注入)。
1904
+ * @param {{system?: string, messages: Array<{role:string,text:string}>, maxTokens?: number, temperature?: number}} opts
1905
+ * @returns {Promise<{ok:boolean, text?:string, provider?:string, model?:string, error?:string}>}
1906
+ */
1907
+ async function llmComplete(ctx, opts) {
1908
+ let llm = null
1909
+ try {
1910
+ if (ctx && typeof ctx.get === 'function') llm = ctx.get('llm')
1911
+ if (!llm && ctx && ctx.llm) llm = ctx.llm
1912
+ } catch { llm = null }
1913
+ if (!llm || typeof llm.stream !== 'function') {
1914
+ return { ok: false, error: tr('LLM 服务不可用(未加载 llm 服务)') }
1915
+ }
1916
+ const cfg = resolveDefaultModel(ctx)
1917
+ if (!cfg) return { ok: false, error: tr('未找到默认模型配置(agent-default-model)') }
1918
+ const blocks = (Array.isArray(opts.messages) ? opts.messages : []).map((m) => ({
1919
+ role: m.role === 'assistant' ? 'assistant' : 'user',
1920
+ content: [{ type: 'text', text: String(m.text ?? '') }],
1921
+ }))
1922
+ if (blocks.length === 0) blocks.push({ role: 'user', content: [{ type: 'text', text: '' }] })
1923
+ const chunks = []
1924
+ try {
1925
+ const stream = llm.stream({
1926
+ provider: cfg.provider,
1927
+ model: cfg.model,
1928
+ system: opts.system ? String(opts.system) : undefined,
1929
+ messages: blocks,
1930
+ maxTokens: opts.maxTokens ?? 2048,
1931
+ temperature: opts.temperature ?? 0.6,
1932
+ })
1933
+ for await (const chunk of stream) {
1934
+ if (chunk && chunk.type === 'text-delta') chunks.push(chunk.text)
1935
+ }
1936
+ } catch (e) {
1937
+ return { ok: false, error: String((e && e.message) || e) }
1938
+ }
1939
+ return { ok: true, text: chunks.join(''), provider: cfg.provider, model: cfg.model }
1940
+ }
1941
+
1942
+ // ---------- ④ 克隆远程仓库(Clone) ----------
1943
+
1944
+ /** 用户 home 目录(④ 克隆默认位置:Windows=C:\Users\xxx,Linux/macOS=/home/xxx)。 */
1945
+ function cloneHomeDir() {
1946
+ return homedir()
1947
+ }
1948
+
1949
+ /** 默认克隆位置(⑤ 等板块的工作目录兜底):DSH 默认工作区。 */
1950
+ function cloneDefaultDir() {
1951
+ return resolveDefaultDir(process.cwd())
1952
+ }
1953
+
1954
+
1955
+ /** 某仓库名在 baseDir 下是否已有同名 git 仓库(含 baseDir 本身就是同名仓库的情形,
1956
+ * 如工作区 C:\Users\xxx\workspace 即对应仓库 workspace)。 */
1957
+ function localRepoExists(baseDir, name) {
1958
+ try {
1959
+ if (!baseDir || !name) return false
1960
+ // baseDir/<name>/.git 存在
1961
+ if (existsSync(join(baseDir, name)) && existsSync(join(baseDir, name, '.git'))) return true
1962
+ // baseDir 本身就是同名仓库
1963
+ if (baseName(baseDir) === name && existsSync(join(baseDir, '.git'))) return true
1964
+ return false
1965
+ } catch { return false }
1966
+ }
1967
+
1968
+ /**
1969
+ * 判断仓库在「当前选中的克隆目录」或「任何已登记的工作区/自定义目录」下是否已存在。
1970
+ * 用于 ④ 克隆仓库的「本地已有」标记:不遍历全盘,只查 ③代码管理登记过的目录——
1971
+ * 用户若把克隆目录选到其他(已登记)位置,那里的同名仓库同样算「本地已有」。
1972
+ * @param {string} name - 仓库名
1973
+ * @param {string} [chosenDir] - 当前下拉选中的克隆目标目录(可为空)
1974
+ */
1975
+ function localRepoExistsAnywhere(name, chosenDir) {
1976
+ try {
1977
+ if (!name) return false
1978
+ if (localRepoExists(String(chosenDir || '').trim(), name)) return true
1979
+ for (const dir of listWorkspaces()) {
1980
+ if (localRepoExists(dir, name)) return true
1981
+ }
1982
+ return false
1983
+ } catch { return false }
1984
+ }
1985
+
1986
+ /**
1987
+ * 列出已登录账号(GitHub / Gitee)的所有远程仓库,并标记本地是否已存在。
1988
+ * @param {string} [provider] - 'github' (default) | 'gitee'
1989
+ * @param {string} [baseDir] - 用于「本地是否已有」判定的目录(默认克隆位置)。
1990
+ */
1991
+ async function listRemoteRepos(provider, baseDir) {
1992
+ const p = provider === 'gitee' ? 'gitee' : 'github'
1993
+ const base = baseDir || cloneDefaultDir()
1994
+ if (p === 'gitee') {
1995
+ const owner = giteeOwner()
1996
+ if (!owner) return { ok: true, provider: 'gitee', owner: undefined, repos: [], error: tr('需要先配置 Gitee 私人令牌') }
1997
+ // 分页拉取账号下所有仓库(每页 100,避免 20/页的默认截断遗漏)。
1998
+ const allRepos = []
1999
+ let page = 1
2000
+ while (page <= 10) {
2001
+ const api = giteeApi('GET', 'user/repos?per_page=100&page=' + page)
2002
+ if (!api.ok) {
2003
+ if (allRepos.length === 0) {
2004
+ return { ok: true, provider: 'gitee', owner, repos: [], error: api.error || tr('Gitee API 请求失败(需要 curl)') }
2005
+ }
2006
+ break
2007
+ }
2008
+ if (!Array.isArray(api.data)) break
2009
+ allRepos.push(...api.data)
2010
+ if (api.data.length < 100) break
2011
+ page++
2012
+ }
2013
+ const repos = allRepos.map((r) => ({
2014
+ name: String(r.name || ''),
2015
+ fullName: String(r.full_name || ''),
2016
+ owner,
2017
+ visibility: r.private === true ? 'private' : (r.private === false ? 'public' : undefined),
2018
+ description: typeof r.description === 'string' ? r.description : undefined,
2019
+ sshUrl: typeof r.ssh_url === 'string' ? r.ssh_url : undefined,
2020
+ htmlUrl: typeof r.html_url === 'string' ? r.html_url : undefined,
2021
+ local: localRepoExistsAnywhere(String(r.name || ''), base),
2022
+ }))
2023
+ return { ok: true, provider: 'gitee', owner, repos }
2024
+ }
2025
+ const owner = ghOwner()
2026
+ if (!owner) return { ok: true, provider: 'github', owner: undefined, repos: [], error: tr('需要先登录 GitHub CLI(gh)') }
2027
+ const r = run(GH, ['repo', 'list', owner, '--limit', '1000', '--json', 'name,owner,visibility,description,updatedAt,sshUrl'], { timeout: 60_000 })
2028
+ let parsed = []
2029
+ if (r.ok) {
2030
+ try {
2031
+ const arr = JSON.parse(r.stdout)
2032
+ if (Array.isArray(arr)) {
2033
+ parsed = arr.map((x) => ({
2034
+ name: String(x.name || ''),
2035
+ fullName: String((x.owner && x.owner.login ? x.owner.login + '/' : '') + (x.name || '')),
2036
+ owner: x.owner && x.owner.login,
2037
+ visibility: String(x.visibility || 'private').toLowerCase(),
2038
+ description: typeof x.description === 'string' ? x.description : undefined,
2039
+ sshUrl: typeof x.sshUrl === 'string' ? x.sshUrl : `git@github.com:${owner}/${x.name}.git`,
2040
+ htmlUrl: `https://github.com/${owner}/${x.name}`,
2041
+ updatedAt: typeof x.updatedAt === 'string' ? x.updatedAt : undefined,
2042
+ }))
2043
+ }
2044
+ } catch { /* ignore */ }
2045
+ }
2046
+ return { ok: true, provider: 'github', owner, repos: parsed.map((x) => ({ ...x, local: localRepoExistsAnywhere(x.name, base) })) }
2047
+ }
2048
+
2049
+ /**
2050
+ * 执行 git clone:克隆到 dest(默认=默认工作区)下的 <name> 目录。成功后加入自定义目录列表。
2051
+ * @param {string} url - 远程仓库 URL(SSH 或 HTTPS)。
2052
+ * @param {string} [dest] - 目标父目录,空=默认克隆位置。
2053
+ * @param {string} [name] - 本地目录名,空=从 URL 推导。
2054
+ */
2055
+ function cloneFlow(url, dest, name) {
2056
+ const target = String(dest || '').trim() || cloneDefaultDir()
2057
+ const repoName = String(name || '').trim()
2058
+ const full = repoName ? join(target, repoName) : ''
2059
+ if (!repoName) return { ok: false, error: tr('缺少仓库名称') }
2060
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
2061
+ return { ok: false, error: tr('目标目录不存在:') + target }
2062
+ }
2063
+ if (existsSync(full)) {
2064
+ return { ok: false, error: tr('已存在同名目录:') + full }
2065
+ }
2066
+ const r = run(GIT, ['clone', String(url || '').trim(), full], { timeout: 600_000, env: { GIT_SSH: WORKING_SSH } })
2067
+ if (!r.ok) {
2068
+ return { ok: false, error: (r.stderr || r.stdout || '').trim() || tr('git clone 失败') }
2069
+ }
2070
+ saveCustomDir(full)
2071
+ return { ok: true, dir: full, name: repoName }
2072
+ }
2073
+
2074
+
2075
+ // ---------- ⑤ 发布 npm 包(npm publish) ----------
2076
+
2077
+ /** npm 状态:是否安装、registry、whoami、目标目录是否存在 package.json。 */
2078
+ function npmStatus(dir) {
2079
+ const npm = NPM
2080
+ const cwd = String(dir || '').trim() || cloneDefaultDir()
2081
+ const installed = npm !== 'npm' && npm !== 'npm.cmd'
2082
+ const registry = run(npm, ['config', 'get', 'registry'], { shell: IS_WIN })
2083
+ const whoami = run(npm, ['whoami'], { shell: IS_WIN })
2084
+ return {
2085
+ ok: true,
2086
+ installed,
2087
+ npmPath: npm,
2088
+ dir: cwd,
2089
+ dirExists: existsSync(cwd),
2090
+ hasPackageJson: existsSync(join(cwd, 'package.json')),
2091
+ registry: registry.ok ? registry.stdout.trim() : undefined,
2092
+ whoami: whoami.ok ? whoami.stdout.trim() : undefined,
2093
+ whoamiError: whoami.ok ? undefined : ((whoami.stderr || whoami.stdout || '').trim() || tr('未登录 npm')),
2094
+ }
2095
+ }
2096
+
2097
+ /**
2098
+ * 打开一个终端窗口,在目标目录运行 `npm login`(交互式登录无法非交互执行,
2099
+ * 需要用户在弹出的终端里完成)。Windows 用 `start cmd /k`;macOS/Linux 探测常见终端。
2100
+ * @param {string} [dir] - 目标目录。
2101
+ * @returns {{ok:boolean, opened:boolean, command:string, detail:string}}
2102
+ */
2103
+ function npmLoginTerminal(dir) {
2104
+ const cwd = String(dir || '').trim() || cloneDefaultDir()
2105
+ const command = 'npm login'
2106
+ const cdCmd = IS_WIN
2107
+ ? `cd /d "${cwd}" && ${command}`
2108
+ : `cd "${cwd}" && ${command}`
2109
+ try {
2110
+ if (IS_WIN) {
2111
+ // start cmd /k 打开独立窗口(第一引号段是窗口标题);detached 不阻塞宿主。
2112
+ const child = spawn('cmd', ['/c', 'start', '"npm-login"', 'cmd', '/k', cdCmd], { detached: true, stdio: 'ignore', windowsHide: false })
2113
+ child.unref()
2114
+ return { ok: true, opened: true, command, detail: tr('已打开终端窗口,请完成 npm login 后回到面板点「重新检查」') }
2115
+ }
2116
+ const terms = [
2117
+ ['x-terminal-emulator', ['-e', 'sh', '-c', cdCmd]],
2118
+ ['gnome-terminal', ['--', 'sh', '-c', cdCmd]],
2119
+ ['konsole', ['-e', 'sh', '-c', cdCmd]],
2120
+ ['xterm', ['-e', 'sh', '-c', cdCmd]],
2121
+ ]
2122
+ for (const [bin, args] of terms) {
2123
+ try {
2124
+ const child = spawn(bin, args, { detached: true, stdio: 'ignore' })
2125
+ child.unref()
2126
+ return { ok: true, opened: true, command, detail: tr('已打开终端窗口,请完成 npm login 后回到面板点「重新检查」') }
2127
+ } catch { /* try next terminal */ }
2128
+ }
2129
+ return { ok: false, opened: false, command, detail: tr('未找到可用终端,请手动在目标目录运行:') + command }
2130
+ } catch (e) {
2131
+ return { ok: false, opened: false, command, detail: String((e && e.message) || e) }
2132
+ }
2133
+ }
2134
+
2135
+ /** 脱敏 npm config list 输出:隐藏 token / auth 等敏感值。 */
2136
+ function redactNpmList(output) {
2137
+ return String(output || '').split(/\r?\n/).map((line) => {
2138
+ if (/(token|_auth|authtoken|password|\bauth\b)/i.test(line)) {
2139
+ return line.replace(/=.*$/, '=[REDACTED]')
2140
+ }
2141
+ return line
2142
+ }).join('\n')
2143
+ }
2144
+
2145
+ /**
2146
+ * 执行 npm 流程的一步(registry / list / whoami / pack / publish)。
2147
+ * @param {string} [dir] - 目标目录(package.json 所在处)。
2148
+ * @param {string} step - 'registry'|'list'|'whoami'|'pack'|'publish'。
2149
+ */
2150
+ function npmRunStep(dir, step) {
2151
+ const npm = NPM
2152
+ const cwd = String(dir || '').trim() || cloneDefaultDir()
2153
+ const runNpm = (args, timeout) => run(npm, args, { shell: IS_WIN, cwd, timeout: timeout ?? 60_000 })
2154
+ switch (step) {
2155
+ case 'registry': {
2156
+ const r = runNpm(['config', 'get', 'registry'])
2157
+ return { ok: true, step, output: r.ok ? r.stdout.trim() : ((r.stderr || '') + (r.stdout || '')).trim() }
2158
+ }
2159
+ case 'list': {
2160
+ const r = runNpm(['config', 'list'])
2161
+ return { ok: true, step, output: redactNpmList((r.stdout || '') + (r.stderr || '')) }
2162
+ }
2163
+ case 'whoami': {
2164
+ const r = runNpm(['whoami'])
2165
+ return { ok: true, step, output: r.ok ? r.stdout.trim() : ((r.stderr || r.stdout || '').trim() || tr('未登录 npm')), loggedIn: r.ok }
2166
+ }
2167
+ case 'pack': {
2168
+ const r = runNpm(['pack', '--dry-run'], 120_000)
2169
+ return { ok: true, step, output: (r.stdout || '') + (r.stderr || '') }
2170
+ }
2171
+ case 'publish': {
2172
+ const r = runNpm(['publish'], 180_000)
2173
+ const blob = ((r.stdout || '') + (r.stderr || '')).trim()
2174
+ return { ok: r.ok, step, output: blob || tr('npm publish 已执行'), error: r.ok ? undefined : (blob || tr('npm publish 失败')) }
2175
+ }
2176
+ default:
2177
+ return { ok: false, step, output: tr('未知步骤') }
2178
+ }
2179
+ }
2180
+
1802
2181
  // ---------- route handlers ----------
1803
2182
 
1804
2183
  function json(res, status, payload) {
@@ -2379,6 +2758,57 @@ export function apply(ctx) {
2379
2758
  return methodNotAllowed(res, req.method)
2380
2759
  }
2381
2760
  },
2761
+
2762
+ // ---- ④ 克隆远程仓库 ----
2763
+ // 默认工作区(⑤ 等板块兜底)。
2764
+ '/clone/default-dir': (req, res) => handle(req, res, async () => {
2765
+ const dir = cloneDefaultDir()
2766
+ return { ok: true, dir, name: baseName(dir) }
2767
+ }),
2768
+ // home 目录(④ 克隆默认:Windows C:\Users\xxx,Linux/macOS /home/xxx)。
2769
+ '/clone/home': (req, res) => handle(req, res, async () => {
2770
+ const dir = cloneHomeDir()
2771
+ return { ok: true, dir, name: baseName(dir) }
2772
+ }),
2773
+ '/clone/repos': async (req, res) => {
2774
+ if (req.method !== 'GET') return methodNotAllowed(res, req.method)
2775
+ await handle(req, res, async (req) => {
2776
+ const q = new URL(req.url ?? '', 'http://localhost')
2777
+ const provider = q.searchParams.get('provider') || 'github'
2778
+ const baseDir = q.searchParams.get('dir') || cloneHomeDir()
2779
+ return listRemoteRepos(provider, baseDir)
2780
+ })
2781
+ },
2782
+ '/clone/run': async (req, res) => {
2783
+ if (req.method !== 'POST') return methodNotAllowed(res, req.method)
2784
+ await handle(req, res, async (req) => {
2785
+ const body = JSON.parse((await readBody(req)) || '{}')
2786
+ return cloneFlow(body.url, body.dest, body.name)
2787
+ })
2788
+ },
2789
+
2790
+ // ---- ⑤ 发布 npm 包 ----
2791
+ '/npm/status': async (req, res) => {
2792
+ if (req.method !== 'GET') return methodNotAllowed(res, req.method)
2793
+ await handle(req, res, async (req) => {
2794
+ const q = new URL(req.url ?? '', 'http://localhost')
2795
+ return npmStatus(q.searchParams.get('dir') || '')
2796
+ })
2797
+ },
2798
+ '/npm/login': async (req, res) => {
2799
+ if (req.method !== 'POST') return methodNotAllowed(res, req.method)
2800
+ await handle(req, res, async (req) => {
2801
+ const body = JSON.parse((await readBody(req)) || '{}')
2802
+ return npmLoginTerminal(body.dir)
2803
+ })
2804
+ },
2805
+ '/npm/step': async (req, res) => {
2806
+ if (req.method !== 'POST') return methodNotAllowed(res, req.method)
2807
+ await handle(req, res, async (req) => {
2808
+ const body = JSON.parse((await readBody(req)) || '{}')
2809
+ return npmRunStep(body.dir, body.step)
2810
+ })
2811
+ },
2382
2812
  }
2383
2813
 
2384
2814
  ctx.effect(() => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "source-code-mgmt",
3
- "version": "1.11.0",
4
- "description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a button in the DSH session header's right-aligned utilities (beside \"Session log\") opening a right-side integrated panel that pushes the main content — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, and auto-ignore files over 100MB with per-file skipped reasons. Cross-platform (Windows / Linux / macOS).",
3
+ "version": "1.14.0",
4
+ "description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a button in the DSH session header's right-aligned utilities (beside \"Session log\") opening a right-side integrated panel that pushes the main content — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, auto-ignore files over 100MB with per-file skipped reasons, a clone section (remote repos from your account with local-exists detection, plus clone any repo by URL into any location), and a five-step npm publish wizard. Cross-platform (Windows / Linux / macOS).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {