source-code-mgmt 1.5.0 → 1.10.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
@@ -24,6 +24,20 @@ import {
24
24
  import { join, dirname } from 'node:path'
25
25
  import { homedir } from 'node:os'
26
26
 
27
+ import { AsyncLocalStorage } from 'node:async_hooks'
28
+
29
+ // ---------- i18n: host messages follow the client’s language (?lang= query param, per request) ----------
30
+ 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
+ const requestLangStore = new AsyncLocalStorage();
32
+ function langOf(req) { try { const l = new URL(req.url ?? "", "http://localhost").searchParams.get("lang"); return l === "en" ? "en" : "zh" } catch { return "zh" } }
33
+ function lang() { return requestLangStore.getStore() ?? "zh" }
34
+ function tr(key, params) {
35
+ let s = lang() === "en" ? (HOST_EN[key] ?? key) : key;
36
+ if (params && params.length) { let i = 0; s = String(s).replace(/\$\{\}/g, () => String(params[i++] ?? "")) }
37
+ return s
38
+ }
39
+
40
+
27
41
  /** Stable cordis plugin name (also the browser bundle id). */
28
42
  export const name = 'source-code-mgmt'
29
43
 
@@ -525,7 +539,7 @@ function saveGiteeToken(token) {
525
539
  */
526
540
  function giteeApi(method, path, body, token) {
527
541
  const tok = token || readGiteeToken()
528
- if (!tok) return { ok: false, error: '尚未配置 Gitee 私人令牌(请打开 ③ 代码管理输入令牌)' }
542
+ if (!tok) return { ok: false, error: tr('尚未配置 Gitee 私人令牌(请打开 ③ 代码管理输入令牌)') }
529
543
  const args = [
530
544
  '-sS', '-L',
531
545
  '-X', String(method || 'GET').toUpperCase(),
@@ -539,7 +553,7 @@ function giteeApi(method, path, body, token) {
539
553
  args.push('https://gitee.com/api/v5/' + String(path).replace(/^\/+/, ''))
540
554
  const r = run(CURL, args, { timeout: 70_000 })
541
555
  if (!r.ok) {
542
- return { ok: false, error: (r.stderr || r.stdout || '').trim() || 'Gitee API 请求失败(需要 curl)' }
556
+ return { ok: false, error: (r.stderr || r.stdout || '').trim() || tr('Gitee API 请求失败(需要 curl)') }
543
557
  }
544
558
  let data
545
559
  try { data = JSON.parse(r.stdout || '{}') } catch { data = null }
@@ -733,7 +747,7 @@ function repoStatus(dir, provider = 'github', full = false) {
733
747
  repoOwner: ev.owner,
734
748
  visibility: ev.checked && ev.exists ? ev.visibility : undefined,
735
749
  provider,
736
- error: 'not a git repository(尚未 git init,可用「新建仓库并推送」初始化为 git 仓库并上传)',
750
+ error: tr('not a git repository(尚未 git init,可用「新建仓库并推送」初始化为 git 仓库并上传)'),
737
751
  }
738
752
  }
739
753
 
@@ -910,10 +924,10 @@ function pushFlow(dir) {
910
924
  const plan = ignorePlan(dir)
911
925
  const skipped = plan.entries.map((e) => {
912
926
  const reason = e.ignored
913
- ? `超过 GitHub 100MB 单文件限制(${fmtMB(e.bytes)}),已被 .gitignore 的 "${e.path}" 排除`
927
+ ? tr("超过 GitHub 100MB 单文件限制(${}),已被 .gitignore 的 \"${}\" 排除", [fmtMB(e.bytes), e.path])
914
928
  : e.kind === 'dir'
915
- ? `超过 GitHub 100MB 单文件限制(${fmtMB(e.bytes)}),${e.source} 所在的一级目录「${e.path}」整体忽略(该文件夹为一整体)`
916
- : `超过 GitHub 100MB 单文件限制(${fmtMB(e.bytes)}),已忽略单个文件「${e.path}」`
929
+ ? tr("超过 GitHub 100MB 单文件限制(${}),${} 所在的一级目录「${}」整体忽略(该文件夹为一整体)", [fmtMB(e.bytes), e.source, e.path])
930
+ : tr("超过 GitHub 100MB 单文件限制(${}),已忽略单个文件「${}」", [fmtMB(e.bytes), e.path])
917
931
  return { path: e.kind === 'dir' ? e.path + '/' : e.path, reason }
918
932
  })
919
933
 
@@ -965,7 +979,7 @@ function pushFlow(dir) {
965
979
  return {
966
980
  ok: false, needsRemote: true, branch: branchName,
967
981
  committed, commitHash, skipped,
968
- error: '尚未配置远程仓库 origin,请使用「新建仓库」创建远程仓库。',
982
+ error: tr('尚未配置远程仓库 origin,请使用「新建仓库」创建远程仓库。'),
969
983
  }
970
984
  }
971
985
 
@@ -994,11 +1008,11 @@ function pushFlow(dir) {
994
1008
  function pullFlow(dir) {
995
1009
  if (!dir || !existsSync(dir)) return { ok: false, error: 'folder does not exist' }
996
1010
  if (!existsSync(join(dir, '.git'))) {
997
- return { ok: false, error: 'not a git repository(尚未 git init,无法拉取)' }
1011
+ return { ok: false, error: tr('not a git repository(尚未 git init,无法拉取)') }
998
1012
  }
999
1013
  const hasRemote = run(GIT, ['-C', dir, 'remote', 'get-url', 'origin']).ok
1000
1014
  if (!hasRemote) {
1001
- return { ok: false, error: '尚未配置远程仓库 origin,无法拉取。' }
1015
+ return { ok: false, error: tr('尚未配置远程仓库 origin,无法拉取。') }
1002
1016
  }
1003
1017
 
1004
1018
  // Was already up to date? First cheap check via ls-remote comparison.
@@ -1028,8 +1042,8 @@ function pullFlow(dir) {
1028
1042
  conflict,
1029
1043
  branch: branchName,
1030
1044
  error: conflict
1031
- ? '拉取存在冲突:本地有未合并改动或与远程冲突,请手动 git pull 处理合并。'
1032
- : (pull.stderr || pull.stdout).trim() || 'git pull 失败',
1045
+ ? tr('拉取存在冲突:本地有未合并改动或与远程冲突,请手动 git pull 处理合并。')
1046
+ : (pull.stderr || pull.stdout).trim() || tr('git pull 失败'),
1033
1047
  detail: blob,
1034
1048
  }
1035
1049
  }
@@ -1043,7 +1057,7 @@ function pullFlow(dir) {
1043
1057
  function mergePushFlow(dir) {
1044
1058
  if (!dir || !existsSync(dir)) return { ok: false, error: 'folder does not exist' }
1045
1059
  if (!existsSync(join(dir, '.git'))) {
1046
- return { ok: false, error: 'not a git repository(尚未 git init)' }
1060
+ return { ok: false, error: tr('not a git repository(尚未 git init)') }
1047
1061
  }
1048
1062
  const branch = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
1049
1063
  const branchName = branch.ok ? branch.stdout.trim() : 'main'
@@ -1057,8 +1071,8 @@ function mergePushFlow(dir) {
1057
1071
  return {
1058
1072
  ok: false, conflict, branch: branchName,
1059
1073
  error: conflict
1060
- ? '拉取并推送失败:合并存在冲突,请手动解决后重试。'
1061
- : (rebase.stderr || rebase.stdout).trim() || 'pull --rebase 失败',
1074
+ ? tr('拉取并推送失败:合并存在冲突,请手动解决后重试。')
1075
+ : (rebase.stderr || rebase.stdout).trim() || tr('pull --rebase 失败'),
1062
1076
  detail: blob,
1063
1077
  }
1064
1078
  }
@@ -1069,7 +1083,7 @@ function mergePushFlow(dir) {
1069
1083
  if (!push.ok) {
1070
1084
  return {
1071
1085
  ok: false, branch: branchName, detail: (push.stdout + '\n' + push.stderr).trim(),
1072
- error: (push.stderr || push.stdout).trim() || 'push 失败',
1086
+ error: (push.stderr || push.stdout).trim() || tr('push 失败'),
1073
1087
  }
1074
1088
  }
1075
1089
  return { ok: true, branch: branchName, rebased: true, pushed: true }
@@ -1084,7 +1098,7 @@ function mergePushFlow(dir) {
1084
1098
  function forcePushFlow(dir) {
1085
1099
  if (!dir || !existsSync(dir)) return { ok: false, error: 'folder does not exist' }
1086
1100
  if (!existsSync(join(dir, '.git'))) {
1087
- return { ok: false, error: 'not a git repository(尚未 git init)' }
1101
+ return { ok: false, error: tr('not a git repository(尚未 git init)') }
1088
1102
  }
1089
1103
  const branch = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
1090
1104
  const branchName = branch.ok ? branch.stdout.trim() : 'main'
@@ -1094,7 +1108,7 @@ function forcePushFlow(dir) {
1094
1108
  const blob = (push.stdout + '\n' + push.stderr).trim()
1095
1109
  return {
1096
1110
  ok: push.ok, branch: branchName, forced: push.ok,
1097
- error: push.ok ? undefined : (push.stderr || push.stdout).trim() || 'push --force 失败',
1111
+ error: push.ok ? undefined : (push.stderr || push.stdout).trim() || tr('push --force 失败'),
1098
1112
  detail: blob,
1099
1113
  }
1100
1114
  }
@@ -1109,7 +1123,7 @@ function forcePushFlow(dir) {
1109
1123
  function forcePullFlow(dir) {
1110
1124
  if (!dir || !existsSync(dir)) return { ok: false, error: 'folder does not exist' }
1111
1125
  if (!existsSync(join(dir, '.git'))) {
1112
- return { ok: false, error: 'not a git repository(尚未 git init)' }
1126
+ return { ok: false, error: tr('not a git repository(尚未 git init)') }
1113
1127
  }
1114
1128
  const branch = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
1115
1129
  const branchName = branch.ok ? branch.stdout.trim() : 'main'
@@ -1122,8 +1136,8 @@ function forcePullFlow(dir) {
1122
1136
  ok: pull.ok, conflict, branch: branchName,
1123
1137
  error: !pull.ok
1124
1138
  ? (conflict
1125
- ? '强制拉取存在冲突,请手动处理。'
1126
- : (pull.stderr || pull.stdout).trim() || 'git pull --force 失败')
1139
+ ? tr('强制拉取存在冲突,请手动处理。')
1140
+ : (pull.stderr || pull.stdout).trim() || tr('git pull --force 失败'))
1127
1141
  : undefined,
1128
1142
  detail: blob,
1129
1143
  }
@@ -1144,9 +1158,9 @@ function forcePullFlow(dir) {
1144
1158
  function createRepoFlow(dir, name, visibility, provider = 'github') {
1145
1159
  if (!dir || !existsSync(dir)) return { ok: false, error: 'folder does not exist' }
1146
1160
  const repoName = String(name || '').trim()
1147
- if (!repoName) return { ok: false, error: '缺少仓库名称' }
1161
+ if (!repoName) return { ok: false, error: tr('缺少仓库名称') }
1148
1162
  if (!/^[A-Za-z0-9._-]+$/.test(repoName) || repoName === '.') {
1149
- return { ok: false, error: '仓库名称包含非法字符(仅允许字母、数字、点、下划线、横线)' }
1163
+ return { ok: false, error: tr('仓库名称包含非法字符(仅允许字母、数字、点、下划线、横线)') }
1150
1164
  }
1151
1165
  if (!existsSync(join(dir, '.git'))) {
1152
1166
  const init = run(GIT, ['-C', dir, 'init'])
@@ -1167,7 +1181,7 @@ function createRepoFlow(dir, name, visibility, provider = 'github') {
1167
1181
  if (!head.ok) {
1168
1182
  const stagedCount = run(GIT, ['-C', dir, 'add', '-A'])
1169
1183
  if (!stagedCount.ok) {
1170
- return { ok: false, error: `git add 失败:${(stagedCount.stderr || '')}` }
1184
+ return { ok: false, error: tr("git add 失败:${}", [(stagedCount.stderr || '')]) }
1171
1185
  }
1172
1186
  const commit = run(GIT, ['-C', dir, 'commit', '-m', 'init: initial commit from DSH source-code-mgmt'])
1173
1187
  if (!commit.ok) {
@@ -1182,10 +1196,10 @@ function createRepoFlow(dir, name, visibility, provider = 'github') {
1182
1196
  // 1) Create the empty remote repo via the Gitee OpenAPI.
1183
1197
  const owner = giteeOwner()
1184
1198
  const token = readGiteeToken()
1185
- if (!owner || !token) return { ok: false, error: '需要先配置 Gitee 私人令牌' }
1199
+ if (!owner || !token) return { ok: false, error: tr('需要先配置 Gitee 私人令牌') }
1186
1200
  const api = giteeApi('POST', 'user/repos', { name: repoName, private: vis === 'private' }, token)
1187
1201
  if (!api.ok || giteeApiFailed(api.data)) {
1188
- return { ok: false, error: (api.error || (api.data && api.data.message) || 'Gitee 创建仓库失败'), detail: api.data }
1202
+ return { ok: false, error: (api.error || (api.data && api.data.message) || tr('Gitee 创建仓库失败')), detail: api.data }
1189
1203
  }
1190
1204
  // 2) Point origin at the SSH remote and push (SSH per user's choice).
1191
1205
  const sshUrl = 'git@gitee.com:' + owner + '/' + repoName + '.git'
@@ -1244,7 +1258,7 @@ function repoDiffFlow(dir, path) {
1244
1258
  return { ok: false, error: 'not a git repository' }
1245
1259
  }
1246
1260
  const p = String(path || '').trim()
1247
- if (!p) return { ok: false, error: '缺少文件路径' }
1261
+ if (!p) return { ok: false, error: tr('缺少文件路径') }
1248
1262
  // Untracked files have no tracked diff.
1249
1263
  const status = run(GIT, ['-C', dir, 'status', '--porcelain', '--', p])
1250
1264
  const isUntracked = status.ok && /^\?\?/.test(status.stdout.trim().slice(0, 2))
@@ -1266,23 +1280,23 @@ function repoDiffFlow(dir, path) {
1266
1280
  function alignFlow(dir) {
1267
1281
  if (!dir || !existsSync(dir)) return { ok: false, error: 'folder does not exist' }
1268
1282
  if (!existsSync(join(dir, '.git'))) {
1269
- return { ok: false, error: 'not a git repository(尚未 git init)' }
1283
+ return { ok: false, error: tr('not a git repository(尚未 git init)') }
1270
1284
  }
1271
1285
  const branch = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
1272
1286
  const branchName = branch.ok ? branch.stdout.trim() : undefined
1273
- if (!branchName) return { ok: false, error: '无法识别当前分支' }
1287
+ if (!branchName) return { ok: false, error: tr('无法识别当前分支') }
1274
1288
  const hasRemote = run(GIT, ['-C', dir, 'remote', 'get-url', 'origin']).ok
1275
- if (!hasRemote) return { ok: false, error: '尚未配置远程仓库 origin,无法对齐。' }
1289
+ if (!hasRemote) return { ok: false, error: tr('尚未配置远程仓库 origin,无法对齐。') }
1276
1290
 
1277
1291
  const fetch = run(GIT, ['-C', dir, 'fetch', 'origin'], { timeout: 120_000 })
1278
1292
  if (!fetch.ok) {
1279
- return { ok: false, error: (fetch.stderr || fetch.stdout).trim() || 'git fetch 失败', detail: (fetch.stdout + '\n' + fetch.stderr).trim() }
1293
+ return { ok: false, error: (fetch.stderr || fetch.stdout).trim() || tr('git fetch 失败'), detail: (fetch.stdout + '\n' + fetch.stderr).trim() }
1280
1294
  }
1281
1295
  const reset = run(GIT, ['-C', dir, 'reset', '--hard', 'origin/' + branchName], { timeout: 60_000 })
1282
1296
  return {
1283
1297
  ok: reset.ok,
1284
1298
  branch: branchName,
1285
- error: reset.ok ? undefined : (reset.stderr || reset.stdout).trim() || 'git reset --hard 失败',
1299
+ error: reset.ok ? undefined : (reset.stderr || reset.stdout).trim() || tr('git reset --hard 失败'),
1286
1300
  detail: (fetch.stdout + '\n' + reset.stdout).trim(),
1287
1301
  }
1288
1302
  }
@@ -1299,7 +1313,7 @@ function initGitFlow(dir) {
1299
1313
  return { ok: true, alreadyRepo: true, dir }
1300
1314
  }
1301
1315
  const init = run(GIT, ['-C', dir, 'init'])
1302
- if (!init.ok) return { ok: false, error: (init.stderr || init.stdout).trim() || 'git init 失败' }
1316
+ if (!init.ok) return { ok: false, error: (init.stderr || init.stdout).trim() || tr('git init 失败') }
1303
1317
  const ident = run(GIT, ['-C', dir, 'config', 'user.email'])
1304
1318
  if (!ident.ok) {
1305
1319
  run(GIT, ['-C', dir, 'config', 'user.name', 'DSH User'])
@@ -1316,7 +1330,7 @@ function initGitFlow(dir) {
1316
1330
  /** Basic guard: the folder must exist and be a git repo; returns an error object or null. */
1317
1331
  function requireGitRepo(dir) {
1318
1332
  if (!dir || !existsSync(dir)) return { error: 'folder does not exist' }
1319
- if (!existsSync(join(dir, '.git'))) return { error: 'not a git repository(尚未 git init)' }
1333
+ if (!existsSync(join(dir, '.git'))) return { error: tr('not a git repository(尚未 git init)') }
1320
1334
  return null
1321
1335
  }
1322
1336
 
@@ -1331,7 +1345,7 @@ function stageFlow(dir, path) {
1331
1345
  const args = ['-C', dir, 'add', '-A']
1332
1346
  if (path && String(path).trim() !== '') args.push('--', String(path).trim())
1333
1347
  const r = run(GIT, args)
1334
- if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || 'git add 失败' }
1348
+ if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || tr('git add 失败') }
1335
1349
  return { ok: true, path: path || null }
1336
1350
  }
1337
1351
 
@@ -1346,7 +1360,7 @@ function unstageFlow(dir, path) {
1346
1360
  const args = ['-C', dir, 'reset', '-q']
1347
1361
  if (path && String(path).trim() !== '') args.push('--', String(path).trim())
1348
1362
  const r = run(GIT, args)
1349
- if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || 'git reset 失败' }
1363
+ if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || tr('git reset 失败') }
1350
1364
  return { ok: true, path: path || null }
1351
1365
  }
1352
1366
 
@@ -1364,7 +1378,7 @@ function commitFlow(dir, message, paths) {
1364
1378
  const guard = requireGitRepo(dir)
1365
1379
  if (guard) return { ok: false, error: guard.error }
1366
1380
  const msg = String(message || '').trim()
1367
- if (msg === '') return { ok: false, error: '提交信息不能为空' }
1381
+ if (msg === '') return { ok: false, error: tr('提交信息不能为空') }
1368
1382
 
1369
1383
  const ident = run(GIT, ['-C', dir, 'config', 'user.email'])
1370
1384
  if (!ident.ok) {
@@ -1377,7 +1391,7 @@ function commitFlow(dir, message, paths) {
1377
1391
  if (list.length > 0) {
1378
1392
  // 只提交指定文件:先把这些文件 add 进暂存区。
1379
1393
  const add = run(GIT, ['-C', dir, 'add', '--', ...list])
1380
- if (!add.ok) return { ok: false, error: (add.stderr || add.stdout).trim() || 'git add 失败' }
1394
+ if (!add.ok) return { ok: false, error: (add.stderr || add.stdout).trim() || tr('git add 失败') }
1381
1395
  stagedPaths = list
1382
1396
  } else {
1383
1397
  // 提交整个暂存区:列出已暂存文件用于提示。
@@ -1387,10 +1401,10 @@ function commitFlow(dir, message, paths) {
1387
1401
 
1388
1402
  // 无可提交内容时避免报错。
1389
1403
  const hasChanges = run(GIT, ['-C', dir, 'diff', '--cached', '--quiet']).code !== 0
1390
- if (!hasChanges) return { ok: false, error: '没有已暂存(staged)的改动可提交', staged: 0 }
1404
+ if (!hasChanges) return { ok: false, error: tr('没有已暂存(staged)的改动可提交'), staged: 0 }
1391
1405
 
1392
1406
  const commit = run(GIT, ['-C', dir, 'commit', '-m', msg])
1393
- if (!commit.ok) return { ok: false, error: (commit.stderr || commit.stdout).trim() || 'git commit 失败', staged: stagedPaths.length }
1407
+ if (!commit.ok) return { ok: false, error: (commit.stderr || commit.stdout).trim() || tr('git commit 失败'), staged: stagedPaths.length }
1394
1408
 
1395
1409
  const rev = run(GIT, ['-C', dir, 'rev-parse', '--short', 'HEAD'])
1396
1410
  return {
@@ -1402,6 +1416,62 @@ function commitFlow(dir, message, paths) {
1402
1416
  }
1403
1417
  }
1404
1418
 
1419
+ /**
1420
+ * Push STAGED changes only: commit whatever is currently staged (using the
1421
+ * given message, or an autogenerated one when empty), then push to origin.
1422
+ * Unlike {@link pushFlow} it does NOT go on to stage untracked/unstaged files
1423
+ * itself and does NOT overwrite the user's own commit message. When nothing is
1424
+ * newly staged yet (e.g. the local branch already has commits ahead), it simply
1425
+ * pushes those existing commits.
1426
+ * @param {string} dir - local folder.
1427
+ * @param {string} [message] - commit message used only when something is staged
1428
+ * and the caller supplies one; empty falls back to an autogenerated message.
1429
+ */
1430
+ function pushStagedFlow(dir, message) {
1431
+ const guard = requireGitRepo(dir)
1432
+ if (guard) return { ok: false, error: guard.error }
1433
+
1434
+ // Remote must exist before we can push.
1435
+ const hasRemote = run(GIT, ['-C', dir, 'remote', 'get-url', 'origin']).ok
1436
+ if (!hasRemote) {
1437
+ return { ok: false, needsRemote: true, error: tr('尚未配置远程仓库 origin,请使用「新建仓库」创建远程仓库。') }
1438
+ }
1439
+
1440
+ const msg = String(message || '').trim()
1441
+ let committed = false
1442
+ let commitHash
1443
+ // Commit staged content only if there is anything staged right now.
1444
+ const hasStaged = run(GIT, ['-C', dir, 'diff', '--cached', '--quiet']).code !== 0
1445
+ if (hasStaged) {
1446
+ const ident = run(GIT, ['-C', dir, 'config', 'user.email'])
1447
+ if (!ident.ok) {
1448
+ run(GIT, ['-C', dir, 'config', 'user.name', 'DSH User'])
1449
+ run(GIT, ['-C', dir, 'config', 'user.email', 'dsh@localhost'])
1450
+ }
1451
+ const useMsg = msg || ('chore: update ' + baseName(dir))
1452
+ const commit = run(GIT, ['-C', dir, 'commit', '-m', useMsg])
1453
+ if (!commit.ok) return { ok: false, error: (commit.stderr || commit.stdout).trim() || tr('git commit 失败') }
1454
+ committed = true
1455
+ const rev = run(GIT, ['-C', dir, 'rev-parse', '--short', 'HEAD'])
1456
+ commitHash = rev.ok ? rev.stdout.trim() : undefined
1457
+ }
1458
+
1459
+ const curBranch = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
1460
+ const branchName = curBranch.ok ? curBranch.stdout.trim() : 'main'
1461
+ const push = run(GIT, ['-C', dir, 'push', '-u', 'origin', branchName], { timeout: 120_000 })
1462
+ const pushed = push.ok
1463
+ return {
1464
+ ok: pushed,
1465
+ needsRemote: false,
1466
+ committed,
1467
+ commitHash,
1468
+ message: msg || (committed ? ('chore: update ' + baseName(dir)) : undefined),
1469
+ branch: branchName,
1470
+ pushed,
1471
+ pushError: pushed ? undefined : (push.stderr || push.stdout).trim(),
1472
+ }
1473
+ }
1474
+
1405
1475
  /**
1406
1476
  * List branches (current first) using `git for-each-ref`.
1407
1477
  * @param {string} dir - local folder.
@@ -1426,9 +1496,9 @@ function checkoutFlow(dir, branch) {
1426
1496
  const guard = requireGitRepo(dir)
1427
1497
  if (guard) return { ok: false, error: guard.error }
1428
1498
  const b = String(branch || '').trim()
1429
- if (b === '') return { ok: false, error: '缺少分支名' }
1499
+ if (b === '') return { ok: false, error: tr('缺少分支名') }
1430
1500
  const r = run(GIT, ['-C', dir, 'checkout', b])
1431
- if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || '切换分支失败' }
1501
+ if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || tr('切换分支失败') }
1432
1502
  return { ok: true, branch: b }
1433
1503
  }
1434
1504
 
@@ -1449,7 +1519,7 @@ function logFlow(dir, count) {
1449
1519
  if (/does not have any commits|bad revision|your current branch/i.test((r.stderr || r.stdout))) {
1450
1520
  return { ok: true, commits: [] }
1451
1521
  }
1452
- return { ok: false, error: (r.stderr || r.stdout).trim() || 'git log 失败' }
1522
+ return { ok: false, error: (r.stderr || r.stdout).trim() || tr('git log 失败') }
1453
1523
  }
1454
1524
  const commits = r.stdout.split(/\r?\n/).filter((l) => l.trim() !== '').map((line) => {
1455
1525
  const [short, subject, author, date, full, refs] = line.split('\x1f')
@@ -1474,9 +1544,9 @@ function revertFlow(dir, hash) {
1474
1544
  const guard = requireGitRepo(dir)
1475
1545
  if (guard) return { ok: false, error: guard.error }
1476
1546
  const h = String(hash || '').trim()
1477
- if (h === '') return { ok: false, error: '缺少 commit hash' }
1547
+ if (h === '') return { ok: false, error: tr('缺少 commit hash') }
1478
1548
  const r = run(GIT, ['-C', dir, 'revert', '--no-edit', h], { timeout: 60_000 })
1479
- if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || 'revert 失败(可能有冲突,请手动处理)' }
1549
+ if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || tr('revert 失败(可能有冲突,请手动处理)') }
1480
1550
  return { ok: true, hash: h }
1481
1551
  }
1482
1552
 
@@ -1489,9 +1559,9 @@ function cherryPickFlow(dir, hash) {
1489
1559
  const guard = requireGitRepo(dir)
1490
1560
  if (guard) return { ok: false, error: guard.error }
1491
1561
  const h = String(hash || '').trim()
1492
- if (h === '') return { ok: false, error: '缺少 commit hash' }
1562
+ if (h === '') return { ok: false, error: tr('缺少 commit hash') }
1493
1563
  const r = run(GIT, ['-C', dir, 'cherry-pick', h], { timeout: 60_000 })
1494
- if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || 'cherry-pick 失败(可能有冲突,请手动处理)' }
1564
+ if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || tr('cherry-pick 失败(可能有冲突,请手动处理)') }
1495
1565
  return { ok: true, hash: h }
1496
1566
  }
1497
1567
 
@@ -1505,9 +1575,9 @@ function commitDiffFlow(dir, hash) {
1505
1575
  const guard = requireGitRepo(dir)
1506
1576
  if (guard) return { ok: false, error: guard.error }
1507
1577
  const h = String(hash || '').trim()
1508
- if (h === '') return { ok: false, error: '缺少 commit hash' }
1578
+ if (h === '') return { ok: false, error: tr('缺少 commit hash') }
1509
1579
  const r = run(GIT, ['-C', dir, 'show', '--no-ext-diff', '--no-color', '--format=', '-m', '--first-parent', h])
1510
- if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || '读取提交 diff 失败' }
1580
+ if (!r.ok) return { ok: false, error: (r.stderr || r.stdout).trim() || tr('读取提交 diff 失败') }
1511
1581
  return { ok: true, hash: h, diff: r.stdout }
1512
1582
  }
1513
1583
 
@@ -1577,7 +1647,7 @@ function installCommand(tool) {
1577
1647
  */
1578
1648
  function installToolFlow(tool) {
1579
1649
  const t = String(tool || '').trim().toLowerCase()
1580
- if (t !== 'git' && t !== 'gh' && t !== 'ssh') return { ok: false, error: '未知工具:' + tool }
1650
+ if (t !== 'git' && t !== 'gh' && t !== 'ssh') return { ok: false, error: tr('未知工具:') + tool }
1581
1651
  // 已安装就直接返回,不重复安装。
1582
1652
  const present = t === 'git' ? (GIT && GIT !== 'git')
1583
1653
  : t === 'gh' ? (GH && GH !== 'gh')
@@ -1586,7 +1656,7 @@ function installToolFlow(tool) {
1586
1656
 
1587
1657
  const cmd = installCommand(t)
1588
1658
  if (!cmd) {
1589
- return { ok: false, error: '未找到可用的包管理器(' + (IS_WIN ? 'winget / choco / scoop' : process.platform === 'darwin' ? 'brew' : 'apt-get / dnf / pacman') + ')。请手动安装。', tool: t }
1659
+ return { ok: false, error: tr('未找到可用的包管理器(') + (IS_WIN ? 'winget / choco / scoop' : process.platform === 'darwin' ? 'brew' : 'apt-get / dnf / pacman') + tr(')。请手动安装。'), tool: t }
1590
1660
  }
1591
1661
  const r = run(cmd.bin, cmd.args, { timeout: 300_000 })
1592
1662
  const output = (r.stdout + '\n' + r.stderr).trim()
@@ -1596,7 +1666,7 @@ function installToolFlow(tool) {
1596
1666
  ok,
1597
1667
  tool: t,
1598
1668
  command: cmd.label,
1599
- output: output || (ok ? '安装成功' : '安装失败'),
1669
+ output: output || (ok ? tr('安装成功') : tr('安装失败')),
1600
1670
  needElevation: IS_WIN && t === 'ssh',
1601
1671
  }
1602
1672
  }
@@ -1755,7 +1825,7 @@ function pickDirDialog(initialDir) {
1755
1825
  const script =
1756
1826
  'Add-Type -AssemblyName System.Windows.Forms;' +
1757
1827
  '$dlg = New-Object System.Windows.Forms.FolderBrowserDialog;' +
1758
- '$dlg.Description = \'选择本地目录\';' +
1828
+ tr('$dlg.Description = \'选择本地目录\';') +
1759
1829
  (initial ? `$dlg.SelectedPath = '${initial}';` : '') +
1760
1830
  'if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $dlg.SelectedPath }'
1761
1831
  const r = run('powershell', ['-NoProfile', '-STA', '-Command', script], { timeout: 180_000 })
@@ -1882,15 +1952,15 @@ function setVisibilityFlow(name, target, provider = 'github') {
1882
1952
  const vis = target === 'public' ? 'public' : 'private'
1883
1953
  if (provider === 'gitee') {
1884
1954
  const owner = giteeOwner()
1885
- if (!owner || !name) return { ok: false, error: '需要先配置 Gitee 私人令牌' }
1955
+ if (!owner || !name) return { ok: false, error: tr('需要先配置 Gitee 私人令牌') }
1886
1956
  const api = giteeApi('PATCH', 'repos/' + owner + '/' + name, { private: vis === 'private' })
1887
1957
  if (!api.ok || giteeApiFailed(api.data)) {
1888
- return { ok: false, error: (api.error || (api.data && api.data.message) || 'Gitee 修改可见性失败') }
1958
+ return { ok: false, error: (api.error || (api.data && api.data.message) || tr('Gitee 修改可见性失败')) }
1889
1959
  }
1890
1960
  return { ok: true, name, visibility: vis, provider: 'gitee' }
1891
1961
  }
1892
1962
  const owner = ghOwner()
1893
- if (!owner || !name) return { ok: false, error: '需要先登录 GitHub CLI(gh)' }
1963
+ if (!owner || !name) return { ok: false, error: tr('需要先登录 GitHub CLI(gh)') }
1894
1964
  const r = run(GH, [
1895
1965
  'repo', 'edit', owner + '/' + name,
1896
1966
  '--visibility', vis,
@@ -1899,7 +1969,7 @@ function setVisibilityFlow(name, target, provider = 'github') {
1899
1969
  if (!r.ok) {
1900
1970
  return {
1901
1971
  ok: false,
1902
- error: `修改可见性失败:${(r.stderr || r.stdout || '').trim() || 'gh repo edit 失败'}`,
1972
+ error: tr("修改可见性失败:${}", [(r.stderr || r.stdout || '').trim() || tr('gh repo edit 失败')]),
1903
1973
  }
1904
1974
  }
1905
1975
  return { ok: true, name, visibility: vis, provider: 'github' }
@@ -1914,11 +1984,13 @@ function baseName(p) {
1914
1984
 
1915
1985
  export function apply(ctx) {
1916
1986
  const fallbackDir = resolveDefaultDir(process.cwd())
1917
- const handle = async (req, res, fn) => {
1987
+ // 每个请求按 ?lang= 参数跑在独立语言上下文里(AsyncLocalStorage),
1988
+ // 所有 flow 函数内的 tr() 都能读到当前请求的语言,并发请求互不干扰。
1989
+ const handle = async (req, res, fn) => requestLangStore.run(langOf(req), async () => {
1918
1990
  if (!isLoopbackRequest(req)) return forbidden(res)
1919
1991
  const payload = await fn(req)
1920
1992
  json(res, 200, payload)
1921
- }
1993
+ })
1922
1994
 
1923
1995
  const routes = {
1924
1996
  '/env': (req, res) => handle(req, res, async () => ({ ok: true, ...checkEnv() })),
@@ -1966,7 +2038,7 @@ export function apply(ctx) {
1966
2038
  await handle(req, res, async (req) => {
1967
2039
  const body = JSON.parse((await readBody(req)) || '{}')
1968
2040
  const dir = String(body.dir || '').trim()
1969
- if (!dir) return { ok: false, error: '缺少目录路径' }
2041
+ if (!dir) return { ok: false, error: tr('缺少目录路径') }
1970
2042
  removeCustomDir(dir)
1971
2043
  return { ok: true, removed: dir, customDirs: readCustomDirs(), workspaces: listWorkspaces() }
1972
2044
  })
@@ -1990,7 +2062,7 @@ export function apply(ctx) {
1990
2062
  await handle(req, res, async (req) => {
1991
2063
  const body = JSON.parse((await readBody(req)) || '{}')
1992
2064
  const picked = pickDirDialog(body.initial || fallbackDir)
1993
- if (!picked) return { ok: false, cancelled: true, error: '未选择目录' }
2065
+ if (!picked) return { ok: false, cancelled: true, error: tr('未选择目录') }
1994
2066
  return { ok: true, dir: picked, name: baseName(picked) }
1995
2067
  })
1996
2068
  },
@@ -1999,9 +2071,9 @@ export function apply(ctx) {
1999
2071
  await handle(req, res, async (req) => {
2000
2072
  const body = JSON.parse((await readBody(req)) || '{}')
2001
2073
  const dir = String(body.dir || '').trim()
2002
- if (!dir) return { ok: false, error: '缺少目录路径' }
2074
+ if (!dir) return { ok: false, error: tr('缺少目录路径') }
2003
2075
  if (!existsSync(dir) || !statSync(dir).isDirectory()) {
2004
- return { ok: false, error: '目录不存在或不是文件夹:' + dir }
2076
+ return { ok: false, error: tr('目录不存在或不是文件夹:') + dir }
2005
2077
  }
2006
2078
  saveCustomDir(dir)
2007
2079
  return { ok: true, dir, name: baseName(dir), workspaces: listWorkspaces(), customDirs: readCustomDirs() }
@@ -2099,6 +2171,13 @@ export function apply(ctx) {
2099
2171
  return pushFlow(body.dir || fallbackDir)
2100
2172
  })
2101
2173
  },
2174
+ '/push-staged': async (req, res) => {
2175
+ if (req.method !== 'POST') return methodNotAllowed(res, req.method)
2176
+ await handle(req, res, async (req) => {
2177
+ const body = JSON.parse((await readBody(req)) || '{}')
2178
+ return pushStagedFlow(body.dir || fallbackDir, body.message)
2179
+ })
2180
+ },
2102
2181
  '/pull': async (req, res) => {
2103
2182
  if (req.method !== 'POST') return methodNotAllowed(res, req.method)
2104
2183
  await handle(req, res, async (req) => {
@@ -2158,13 +2237,13 @@ export function apply(ctx) {
2158
2237
  return { ok: true, configured: false, cleared: true }
2159
2238
  }
2160
2239
  const token = String(body.token || '').trim()
2161
- if (!token) return { ok: false, error: '令牌不能为空' }
2240
+ if (!token) return { ok: false, error: tr('令牌不能为空') }
2162
2241
  saveGiteeToken(token)
2163
2242
  const owner = giteeOwner()
2164
2243
  if (!owner) {
2165
2244
  // 令牌无效则清掉,避免留有坏令牌。
2166
2245
  saveGiteeToken('')
2167
- return { ok: false, error: 'Gitee 令牌无效,请检查(需有 projects 权限)' }
2246
+ return { ok: false, error: tr('Gitee 令牌无效,请检查(需有 projects 权限)') }
2168
2247
  }
2169
2248
  return { ok: true, configured: true, owner }
2170
2249
  })
package/package.json CHANGED
@@ -1,38 +1,41 @@
1
- {
2
- "name": "source-code-mgmt",
3
- "version": "1.5.0",
4
- "description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a top-right floating button + right drawer — 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).",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": {
9
- "default": "./lib/index.js"
10
- },
11
- "./client": {
12
- "default": "./lib/client.js"
13
- },
14
- "./package.json": "./package.json"
15
- },
16
- "dsh": {
17
- "bundle": "./lib/index.js",
18
- "client": {
19
- "inject": [
20
- "@deepseek-ai/dsh-client-runtime"
21
- ],
22
- "platform": "web"
23
- }
24
- },
25
- "peerDependencies": {
26
- "react": "^18.2.0",
27
- "dsh-better-sidebar": ">=0.4.0"
28
- },
29
- "peerDependenciesMeta": {
30
- "dsh-better-sidebar": {
31
- "optional": true
32
- }
33
- },
34
- "files": [
35
- "lib/**/*.js"
36
- ],
37
- "license": "MIT"
38
- }
1
+ {
2
+ "name": "source-code-mgmt",
3
+ "version": "1.10.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).",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./lib/index.js"
10
+ },
11
+ "./client": {
12
+ "default": "./lib/client.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "bundle": {
18
+ "patch": "./cordis.patch.yml"
19
+ },
20
+ "client": {
21
+ "inject": [
22
+ "@deepseek-ai/dsh-client-runtime"
23
+ ],
24
+ "platform": "web"
25
+ }
26
+ },
27
+ "peerDependencies": {
28
+ "react": "^18.2.0",
29
+ "dsh-better-sidebar": ">=0.4.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "dsh-better-sidebar": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "files": [
37
+ "lib/**/*.js",
38
+ "cordis.patch.yml"
39
+ ],
40
+ "license": "MIT"
41
+ }