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/README.en.md +54 -3
- package/README.md +55 -4
- package/cordis.patch.yml +15 -15
- package/lib/client.js +513 -2
- package/lib/index.js +431 -1
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -180,6 +180,90 @@ window.__ModuleLoader__.load({
|
|
|
180
180
|
|
|
181
181
|
// ---------- i18n: follows DSH’s language setting (Settings → General → Language), live ----------
|
|
182
182
|
const EN_DICT = {"展开":"Expand","折叠":"Collapse","展开该部分":"Expand this section","折叠该部分":"Collapse this section","已安装":"Installed","❌ 未安装":"❌ Not installed","复制安装命令":"Copy install command","一键安装(会自动选包管理器)":"Install automatically (picks a package manager)","安装中…":"Installing…","安装":"Install"," 执行成功":" executed successfully","(如未生效需以管理员身份重试)":" (retry as administrator if it did not take effect)","安装失败":"Install failed","① 环境检查":"① Environment","✅ 均存在":"✅ All present","操作系统":"OS","已找到":"Found","检测中":"Checking","失败: ":"Failed: ","未找到的工具可用上方「安装」按钮一键安装(":"Missing tools can be installed with the \"Install\" button above (","Windows 用 winget / 内置功能":"winget / built-in features on Windows","用系统包管理器":"your system package manager",",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。":"; admin rights may be required). You can also copy the install command and run it manually, then click \"Re-check\".","重新检查":"Re-check",":已存在,无需重复操作":": already exists, no need to repeat","成功":" succeeded","失败":" failed","失败:":"Failed: ","平台":"Platform","选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换":"Choose the code hosting platform: GitHub or Gitee (GitHub default); step ③ follows this switch","GitHub(默认)":"GitHub (default)","② SSH 密钥与连接":"② SSH Key & Connection","密钥":"Key"," 已生成":" generated","⚠️ 未生成":"⚠️ Not generated","GH 登录":"GH login","已登录":"Logged in","⚠️ 未登录":"⚠️ Not logged in","SSH 配置":"SSH config"," 已配置(443)":" configured (443)","⚠️ 未配置/限制 22 端口需配置":"⚠️ Not configured / port 22 blocked — use 443","生成密钥":"Generate key","配置 SSH(config)":"Configure SSH config","配置 SSH":"Configure SSH","测试连接":"Test connection","SSH 连接成功(":"SSH connection succeeded (","已认证":"authenticated","连接失败,请确认密钥已上传到 ":"Connection failed. Make sure the key is uploaded to "," 或已登录 gh":" or that gh is logged in","测试失败:":"Test failed: ","公钥(复制上传到 ":"Public key (copy and upload to ","Gitee → 设置 → SSH 公钥":"Gitee → Settings → SSH keys",",或运行 gh auth login 自动上传):":", or run gh auth login to upload it automatically):","该目录不是 git 仓库":"This folder is not a git repository","读取令牌状态失败:":"Failed to read token status: ","已保存 Gitee 令牌(账号:":"Gitee token saved (account: ","保存令牌失败":"Failed to save token","保存令牌失败:":"Failed to save token: ","已清除 Gitee 令牌":"Gitee token cleared","清除失败":"Clear failed","清除令牌失败:":"Failed to clear token: ","已推送":"Pushed","推送失败":"Push failed","推送失败:":"Push failed: ","已拉取远程更新(本地已对齐到远程最新状态)":"Pulled remote updates (local is now aligned with the latest remote state)","拉取失败":"Pull failed","拉取失败:":"Pull failed: ","已拉取远程更新并推送更改":"Pulled remote updates and pushed changes","合并推送失败":"Merge-push failed","合并推送失败:":"Merge-push failed: ","强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?":"Force-push overwrites the remote repo with your local version; remote-only changes will be lost. Continue?","已强制推送,远程已更新为本地状态":"Force-pushed; the remote is now your local state","强制推送失败":"Force-push failed","强制推送失败:":"Force-push failed: ","强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?":"Force-pull merges remote updates into local; anything you don't want kept may conflict or be lost. Continue?","已强制拉取远程更新":"Force-pulled remote updates","强制拉取失败":"Force-pull failed","强制拉取失败:":"Force-pull failed: ","请先加载一个有效的文件夹":"Load a valid folder first","确定把仓库 ":"Change repo "," 改为「":" visibility to「","公开":"Public","私有":"Private","」?修改可见性可能影响 Star、关注者等。":"」? Changing visibility may affect stars, watchers, etc.","已把仓库设置为「":"Repo set to「","修改可见性失败":"Failed to change visibility","修改可见性失败:":"Failed to change visibility: ","仓库已创建并推送:":"Repo created and pushed: ","创建失败":"Create failed","创建失败:":"Create failed: ","未选择目录":"No folder selected","选择失败:":"Selection failed: ","请输入或选择目录路径":"Enter or pick a folder path","添加目录失败":"Failed to add folder","添加目录失败:":"Failed to add folder: ","删除失败":"Delete failed","删除失败:":"Delete failed: ","强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?":"Force-align overwrites local with the remote branch (drops unpushed local changes and commits). Continue?","已强制对齐到远程分支":"Force-aligned to the remote branch","强制对齐失败":"Force-align failed","强制对齐失败:":"Force-align failed: ","已是 git 仓库":"Already a git repository","已创建 git 仓库,请自行拉取或推送":"Git repository created — pull or push as you wish","创建 git 失败":"Failed to init git","创建 git 失败:":"Failed to init git: ","暂存失败":"Stage failed","暂存失败:":"Stage failed: ","取消暂存失败":"Unstage failed","取消暂存失败:":"Unstage failed: ","已提交 ":"Committed "," 个文件":" file(s)","提交失败":"Commit failed","提交失败:":"Commit failed: ","已提交「":"Committed「","(自动生成)":" (auto-generated)","」并推送":"」 and pushed","已推送本地已有的提交":"Pushed existing local commits","读取分支失败":"Failed to load branches","读取分支失败:":"Failed to load branches: ","切换到分支「":"Switch to branch「","」?(若当前有未提交改动,git 会拒绝切换)":"」? (git will refuse if you have uncommitted changes)","已切换到分支 ":"Switched to branch ","切换分支失败":"Failed to switch branch","切换分支失败:":"Failed to switch branch: ","读取历史失败":"Failed to load history","读取历史失败:":"Failed to load history: ","revert 提交「":"Revert commit「","」——会生成一个反向提交,期间可能产生冲突,确定继续?":"」 — this creates a reverse commit and may cause conflicts. Continue?","已 revert 提交 ":"Reverted commit ","revert 失败":"Revert failed","revert 失败:":"Revert failed: ","cherry-pick 提交「":"Cherry-pick commit「","」到当前分支——期间可能产生冲突,确定继续?":"」 onto the current branch — this may cause conflicts. Continue?","已 cherry-pick 提交 ":"Cherry-picked commit ","cherry-pick 失败":"Cherry-pick failed","cherry-pick 失败:":"Cherry-pick failed: ","读取失败":"Failed to load","已复制":"Copied","短哈希":"short hash","完整哈希":"full hash","提交信息":"commit message","复制失败:请手动复制":"Copy failed — copy it manually","③ 代码管理":"③ Code Management","Gitee 私人令牌(OpenAPI,需 projects 权限)":"Gitee personal token (OpenAPI, requires projects permission)","✅ 已配置":"✅ Configured","(账号 ":" (account ","清除令牌":"Clear token","粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)":"Paste the Gitee personal token (https://gitee.com/personal_access_tokens)","保存令牌":"Save token","令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。":"The token is stored only on this machine at ~/.dsh/storages (0600), never in the plugin directory; the personal token must have projects permission. Your public key must be uploaded to Gitee (checked in ②).","选择工作区":"Workspace","选择 DSH 已登记的工作区文件夹":"Choose a workspace folder registered in DSH","(暂无可选工作区)":"(no workspaces available)","— 请选择 —":"— Select —","选择目录":"Choose folder","自定义目录:":"Custom folder: ","删除该目录记录":"Remove this folder entry","删除该目录记录(不删除实际文件夹)":"Remove this folder entry (does not delete the actual folder)","目录":"Folder","分支":"Branch","(无)":"(none)","切换分支":"Switch branch","切换":"Switch","查看提交历史":"View commit history","历史":"History","远程":"Remote","改动":"Changes","无":"None","查看改动的文件列表":"View the list of changed files","查看":"View","同步":"Sync","本地领先 ":"Ahead by "," 提交":" commit(s)","、落后 ":", behind by ","落后 ":"behind by ","与远程一致":"Up to date with remote","查看本地与远程的提交差异":"View commit differences between local and remote","⚠️ 以下 ":"⚠️ The following "," 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):":" item(s) >100MB (over GitHub's limit — auto-ignored on push):","/(整个文件夹)":"/ (entire folder)","创建 Git":"Init Git","仅初始化 git 仓库,不拉取不推送,由你决定下一步":"Initializes a git repo only — no pull/push; you decide the next step","本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态":"You have local changes AND remote updates — use \"Force align\" to reset local to the remote state","强制对齐":"Force align","本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况":"Fully resets local to the remote branch (drops local differences); fixes \"files identical but sync still shows a difference\"","推送更改":"Push changes","拉取更新":"Pull updates","✓ 已是最新":"✓ Up to date","⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…":"⟳ Refreshing status — syncing the latest GitHub/Gitee data…","⟳ 切换平台,正在重新检测「":"⟳ Switching platform, re-checking「","」仓库状态…":"」 repo status…","推送中…":"Pushing…","推送暂存":"Push staged","把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动":"Commits only what is staged (with your message, or auto-generated) and pushes it; does not auto-stage other changes","刷新中…":"Refreshing…","刷新状态":"Refresh status","提交信息(留空则用默认)":"Commit message (default if empty)","填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)":"Type a message then click \"Commit\"; leave empty to auto-generate (chore: update <folder>)","提交":"Commit","仓库名称自动取文件夹名(不可修改)":"Repo name is taken from the folder name (read-only)","(未加载文件夹)":"(no folder loaded)","仓库可见性:私有 / 公开":"Repo visibility: private / public","(修改当前仓库的可见性)":" (changes the current repo's visibility)","新建仓库并推送":"Create repo & push","修改仓库状态":"Change repo status","✓ 仓库已是「":"✓ Repo is already「","」状态,如需修改请调整左侧可见性选择。":"」 — to change it, adjust the visibility dropdown.","将把仓库从「":"Will change the repo from「","」改为「":"」 to「","」,点击「修改仓库状态」执行。":"」 — click \"Change repo status\" to apply.","⚠️ 同名仓库已经创建(无法读取当前可见性,可能未":"⚠️ A repo with this name already exists (couldn't read its visibility — maybe not ","配置 Gitee 令牌":"Gitee token configured","登录 gh":"logged into gh","✓ 同名仓库 ":"✓ No repo named "," 不存在,可在 ":" exists — you can create it on ","「新建仓库并推送」创建(可选私有/公开)。":" with \"Create repo & push\" (private or public).","已忽略未上传(":"Skipped uploads ("," 项 >100MB):":" item(s) >100MB):","已提交":"Committed"," 并推送":" and pushed","(推送省略)":" (push skipped)","查看详情":"View details","改动文件":"Changed files","提交历史":"Commit history","与远程同步差异":"Sync differences vs remote","关闭":"Close","已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理面板」查看。":"dsh-better-sidebar is installed — this lists changed files only; see the actual changes in its \"Source Control panel\".","点击收起":"Click to collapse","点击查看改动内容":"Click to view the diff","新文件(untracked)暂无内容 diff":"New (untracked) file — no diff yet","已暂存":"Staged","未暂存":"Unstaged","取消暂存":"Unstage","暂存":"Stage","加载中…":"Loading…","▾ 收起":"▾ Collapse","▸ 查看":"▸ View","正在加载改动内容…":"Loading changes…","(无内容差异)":"(no diff)","当前没有改动。":"No changes.","正在读取分支…":"Loading branches…","(无分支)":"(no branches)","(当前)":"(current)","正在读取历史…":"Loading history…","(暂无提交)":"(no commits)","更多操作(右键也可打开)":"More actions (right-click also works)","查看提交差异":"View commit diff","复制短哈希":"Copy short hash","复制完整哈希":"Copy full hash","复制提交信息":"Copy commit message","还原此提交":"Revert this commit","拾取此提交":"Cherry-pick this commit","正在加载提交 diff…":"Loading commit diff…","本地落后 ":"Behind by "," 个提交(远程有而本地没有):":" commit(s) (on remote, not local):"," 个提交(本地有而远程没有):":" commit(s) (on local, not remote):","✓ 已与远程同步,无差异。":"✓ In sync with remote — no differences.","选择代码目录":"Choose a code folder","可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。":"Paste an absolute folder path, or click \"Browse…\" for the native picker. Confirmed folders are added to the dropdown and remembered.","C:\\Users\\你的用户名\\项目目录":"C:\\Users\\your-name\\project","浏览…":"Browse…","取消":"Cancel","确定":"OK","新增":"Added","删除":"Deleted","重命名":"Renamed","修改":"Modified","旧版本":"Old","新版本":"New","源代码管理":"Source Control","按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(":"Work through in order: ① Environment → ② SSH Key & Connection → ③ Code Management. Files over 100MB are auto-ignored on push ("," 限制)并说明原因。":" limit) with the reason shown.","代码管理":"Code Management","拖动调整面板宽度":"Drag to resize the panel","二进制文件,无法查看文本内容":"Binary file — text preview unavailable","(空文件)":"(empty file)"};
|
|
183
|
+
Object.assign(EN_DICT, {
|
|
184
|
+
// ④ 克隆仓库
|
|
185
|
+
"④ 克隆仓库": "④ Clone repos",
|
|
186
|
+
"刷新仓库列表": "Refresh repo list",
|
|
187
|
+
"克隆到目录": "Clone into",
|
|
188
|
+
"远程仓库列表": "Remote repos",
|
|
189
|
+
"(暂无仓库)": "(no repos)",
|
|
190
|
+
"本地已有": "Local",
|
|
191
|
+
"可克隆": "Cloneable",
|
|
192
|
+
"克隆": "Clone",
|
|
193
|
+
"克隆中…": "Cloning…",
|
|
194
|
+
"已克隆到 ": "Cloned to ",
|
|
195
|
+
"克隆失败": "Clone failed",
|
|
196
|
+
"克隆失败:": "Clone failed: ",
|
|
197
|
+
"加载失败": "Load failed",
|
|
198
|
+
"加载失败:": "Load failed: ",
|
|
199
|
+
"加载中…": "Loading…",
|
|
200
|
+
"(无)": "(none)",
|
|
201
|
+
"已记录到本地": "Saved locally",
|
|
202
|
+
"保存失败:": "Save failed: ",
|
|
203
|
+
"生成中…": "Generating…",
|
|
204
|
+
"如 https://github.com/owner/repo 或 owner/repo": "e.g. https://github.com/owner/repo or owner/repo",
|
|
205
|
+
"目标目录不存在:": "Target directory does not exist: ",
|
|
206
|
+
"已存在同名目录:": "A directory with the same name already exists: ",
|
|
207
|
+
"请先登录 GitHub CLI(gh)": "Log in to GitHub CLI (gh) first",
|
|
208
|
+
"需要先配置 Gitee 私人令牌": "Configure the Gitee personal token first",
|
|
209
|
+
"克隆一个账号下的远程仓库到本地": "Clone a remote repo from your account locally",
|
|
210
|
+
"删除": "Remove",
|
|
211
|
+
// ⑤ 发布 npm 包
|
|
212
|
+
"⑤ 发布 npm 包": "⑤ Publish npm package",
|
|
213
|
+
"目标目录(含 package.json)": "Target dir (contains package.json)",
|
|
214
|
+
"① 确认源(registry)": "① Check registry",
|
|
215
|
+
"② 查看认证配置(config list)": "② Auth config (config list)",
|
|
216
|
+
"③ 验证身份(whoami)": "③ Verify identity (whoami)",
|
|
217
|
+
"④ 预览打包文件(pack --dry-run)": "④ Preview packed files (pack --dry-run)",
|
|
218
|
+
"⑤ 发布(publish)": "⑤ Publish",
|
|
219
|
+
"执行": "Run",
|
|
220
|
+
"执行中…": "Running…",
|
|
221
|
+
"npm 未安装(请先在 ① 环境检查安装)": "npm is not installed (install it in ① Environment first)",
|
|
222
|
+
"已登录:": "Logged in as: ",
|
|
223
|
+
"未登录 npm": "Not logged in to npm",
|
|
224
|
+
"npm 状态": "npm status",
|
|
225
|
+
"registry:": "registry: ",
|
|
226
|
+
"我已核对以上内容,确认发布到 npm registry": "I've reviewed the above — confirm publishing to the npm registry",
|
|
227
|
+
"发布完成": "Publish complete",
|
|
228
|
+
"发布失败": "Publish failed",
|
|
229
|
+
"发布失败:": "Publish failed: ",
|
|
230
|
+
"读取 npm 状态失败:": "Failed to read npm status: ",
|
|
231
|
+
"发布 npm 包五步向导:确认源 → 认证配置 → 身份 → 打包预览 → 发布": "Five-step npm publish guide: registry → auth config → identity → pack preview → publish",
|
|
232
|
+
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理,以及 ④克隆仓库 / ⑤发布 npm 包。推送会自动忽略 >100MB 的文件(": "In order: ①Environment → ②SSH key & connection → ③Code management, plus ④Clone repos / ⑤Publish npm. Pushing auto-ignores >100MB files (",
|
|
233
|
+
" 限制)并说明原因。": " limit) with a reason.",
|
|
234
|
+
// ④ 目录选择(复用 ③ 的「选择目录」交互)
|
|
235
|
+
"克隆到目录(默认=用户主目录 home)": "Clone into (default = user home directory)",
|
|
236
|
+
"选择目录…": "Choose directory…",
|
|
237
|
+
"记住到常用目录": "Remember as a favorite dir",
|
|
238
|
+
"已记住目录": "Directory remembered",
|
|
239
|
+
"记住目录失败:": "Failed to remember dir: ",
|
|
240
|
+
"目录不存在或不是文件夹:": "Directory does not exist or is not a folder: ",
|
|
241
|
+
"可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到常用目录并记住,下次打开无需重新选择。": "Type/paste an absolute path, or click \"Browse…\" to open the native folder picker. Confirming adds it to your favorite dirs so you won't re-pick it next time.",
|
|
242
|
+
// ⑤ npm 登录与目录
|
|
243
|
+
"未登录 npm,发布前需先登录": "Not logged in to npm — log in before publishing",
|
|
244
|
+
"打开终端执行 npm login": "Open a terminal to run npm login",
|
|
245
|
+
"复制登录命令": "Copy login command",
|
|
246
|
+
"登录窗口已打开,请在弹出的终端完成登录后点「重新检查」": "A login terminal has been opened — complete login there, then click \"Re-check\"",
|
|
247
|
+
"该目录没有 package.json(请选择含 package.json 的包目录)": "This directory has no package.json (choose a package directory with one)",
|
|
248
|
+
"(当前目录不存在)": " (directory does not exist)",
|
|
249
|
+
"打开终端失败": "Failed to open a terminal",
|
|
250
|
+
"请手动在终端运行:": "Run this manually in a terminal: ",
|
|
251
|
+
"已复制:": "Copied: ",
|
|
252
|
+
"目录不存在:": "Directory does not exist: ",
|
|
253
|
+
"提交": "Commit",
|
|
254
|
+
"取消": "Cancel",
|
|
255
|
+
// ④ 按地址克隆
|
|
256
|
+
"也可以按仓库地址克隆到任意位置": "You can also clone any repo by URL into any location",
|
|
257
|
+
"仓库地址": "Repo URL",
|
|
258
|
+
"克隆到": "Clone into",
|
|
259
|
+
"请输入仓库地址": "Enter a repo URL",
|
|
260
|
+
"请选择克隆到目录": "Choose a destination directory first",
|
|
261
|
+
"无法从地址识别仓库名称": "Cannot derive a repo name from this URL",
|
|
262
|
+
// ⑤ 目录默认留空,需手动选择
|
|
263
|
+
"请先选择目标目录": "Choose a target directory first",
|
|
264
|
+
"请先选择目标目录(当前留空,不会自动使用默认目录)": "Choose a target directory first (left empty on purpose — it will NOT auto-use the default dir)",
|
|
265
|
+
});
|
|
266
|
+
|
|
183
267
|
let scmLang = (() => { try { return String(navigator.language || "zh").toLowerCase().split("-")[0] === "en" ? "en" : "zh" } catch { return "zh" } })();
|
|
184
268
|
let scmLocaleFace = null; // ctx.locale (the DSH LocaleRuntime), set in apply()
|
|
185
269
|
let scmSessionList = null; // ctx.get('sessions').list (ObservableSnapshot), set in apply() — 用于判断「无会话空态」
|
|
@@ -1472,6 +1556,16 @@ function useSession() {
|
|
|
1472
1556
|
};
|
|
1473
1557
|
}
|
|
1474
1558
|
|
|
1559
|
+
/** Shared text input style for the new sections (④⑤). */
|
|
1560
|
+
function inputStyle() {
|
|
1561
|
+
return {
|
|
1562
|
+
flex: 1, minWidth: 0, font: "inherit", fontSize: 12,
|
|
1563
|
+
padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8,
|
|
1564
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,.14))",
|
|
1565
|
+
color: T.label, outline: "none",
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1475
1569
|
/** 提交行「更多操作」下拉面板样式(右对齐、向上弹出,避免在弹窗底部被裁)。 */
|
|
1476
1570
|
function menuPanelStyle() {
|
|
1477
1571
|
return {
|
|
@@ -1605,6 +1699,419 @@ function useSession() {
|
|
|
1605
1699
|
);
|
|
1606
1700
|
}
|
|
1607
1701
|
|
|
1702
|
+
// ---------- section 4: 克隆远程仓库 ----------
|
|
1703
|
+
function CloneSection({ provider, setProvider }) {
|
|
1704
|
+
// 跟随 ② 的平台(GitHub / Gitee),标题行也能切。
|
|
1705
|
+
const prov = provider || "github";
|
|
1706
|
+
const setProv = setProvider || (() => {});
|
|
1707
|
+
const [dir, setDir] = useState("");
|
|
1708
|
+
const [dirs, setDirs] = useState([]); // 可选克隆目标目录(home + 工作区 + 自定义)
|
|
1709
|
+
const [repos, setRepos] = useState([]); // 远程仓库列表
|
|
1710
|
+
const [loading, setLoading] = useState(false);
|
|
1711
|
+
const [busyName, setBusyName] = useState(null); // 正在克隆的仓库名
|
|
1712
|
+
const [msg, setMsg] = useState(null);
|
|
1713
|
+
const [err, setErr] = useState(null);
|
|
1714
|
+
// 按仓库地址克隆:地址输入 + 目标目录(默认空,用户选择)
|
|
1715
|
+
const [cloneUrl, setCloneUrl] = useState("");
|
|
1716
|
+
const [cloneBusy, setCloneBusy] = useState(false);
|
|
1717
|
+
// 目录选择器(同 ③:输入 / 浏览… 原生选择器 / 记住到自定义目录)
|
|
1718
|
+
const [pickOpen, setPickOpen] = useState(false);
|
|
1719
|
+
const [pickPath, setPickPath] = useState("");
|
|
1720
|
+
const [pickErr, setPickErr] = useState(null);
|
|
1721
|
+
const [picking, setPicking] = useState(false);
|
|
1722
|
+
|
|
1723
|
+
// 初始:默认克隆目录(home)+ 工作区 + 自定义目录,回填 dir。
|
|
1724
|
+
const refreshDirs = useCallback(async () => {
|
|
1725
|
+
try {
|
|
1726
|
+
const [def, ws] = await Promise.all([
|
|
1727
|
+
jget("/clone/home").catch(() => null),
|
|
1728
|
+
jget("/workspaces").catch(() => null),
|
|
1729
|
+
]);
|
|
1730
|
+
const list = [];
|
|
1731
|
+
if (def && def.dir) { list.push(def.dir); if (!dir) setDir(def.dir); }
|
|
1732
|
+
if (ws) {
|
|
1733
|
+
if (Array.isArray(ws.workspaces)) for (const w of ws.workspaces) if (!list.includes(w)) list.push(w);
|
|
1734
|
+
if (Array.isArray(ws.customDirs)) for (const c of ws.customDirs) if (!list.includes(c)) list.push(c);
|
|
1735
|
+
}
|
|
1736
|
+
setDirs(list);
|
|
1737
|
+
} catch {}
|
|
1738
|
+
}, [dir]);
|
|
1739
|
+
useEffect(() => { void refreshDirs(); }, [refreshDirs]);
|
|
1740
|
+
|
|
1741
|
+
// 拉取远程仓库列表(含本地存在标记)。
|
|
1742
|
+
const load = useCallback(async () => {
|
|
1743
|
+
setLoading(true); setErr(null);
|
|
1744
|
+
try {
|
|
1745
|
+
const r = await jget("/clone/repos?provider=" + encodeURIComponent(prov) + "&dir=" + encodeURIComponent(dir || ""));
|
|
1746
|
+
if (r && r.ok) { setRepos(r.repos || []); if (r.error) setErr(r.error); }
|
|
1747
|
+
else setErr((r && r.error) || t("加载失败"));
|
|
1748
|
+
} catch (e) { setErr(t("加载失败:") + e); }
|
|
1749
|
+
finally { setLoading(false); }
|
|
1750
|
+
}, [prov, dir]);
|
|
1751
|
+
useEffect(() => { void load(); }, [load]);
|
|
1752
|
+
|
|
1753
|
+
// 克隆单个仓库。
|
|
1754
|
+
const doClone = useCallback(async (repo) => {
|
|
1755
|
+
setBusyName(repo.name); setMsg(null); setErr(null);
|
|
1756
|
+
try {
|
|
1757
|
+
const r = await jpost("/clone/run", { url: repo.sshUrl || repo.htmlUrl || "", dest: dir, name: repo.name });
|
|
1758
|
+
if (r && r.ok) { setMsg(t("已克隆到 ") + r.dir); void load(); }
|
|
1759
|
+
else setErr((r && r.error) || t("克隆失败"));
|
|
1760
|
+
} catch (e) { setErr(t("克隆失败:") + e); }
|
|
1761
|
+
finally { setBusyName(null); }
|
|
1762
|
+
}, [dir, load]);
|
|
1763
|
+
|
|
1764
|
+
// 按仓库地址克隆:从 URL 末段推导仓库名(去 .git 后缀),POST /clone/run。
|
|
1765
|
+
// 复用了顶部「克隆到目录」的 dir(含 选择目录… 记忆到自定义目录)。
|
|
1766
|
+
// cloneFlow 成功后会把目标目录记录到本地自定义目录,因此「目标位置已有」
|
|
1767
|
+
// 的知识会自动持久化,下次列表会标记本地已有。
|
|
1768
|
+
const doCloneUrl = useCallback(async () => {
|
|
1769
|
+
const u = String(cloneUrl || "").trim();
|
|
1770
|
+
if (!u) { setErr(t("请输入仓库地址")); return; }
|
|
1771
|
+
const dest = String(dir || "").trim();
|
|
1772
|
+
if (!dest) { setErr(t("请选择克隆到目录")); return; }
|
|
1773
|
+
// 提取仓库名:取 URL 末段,去掉尾部 .git
|
|
1774
|
+
const seg = u.replace(/\/+$/, "").split("/").pop() || "";
|
|
1775
|
+
const name = seg.replace(/\.git$/i, "").trim();
|
|
1776
|
+
if (!name) { setErr(t("无法从地址识别仓库名称")); return; }
|
|
1777
|
+
setCloneBusy(true); setMsg(null); setErr(null);
|
|
1778
|
+
try {
|
|
1779
|
+
const r = await jpost("/clone/run", { url: u, dest, name });
|
|
1780
|
+
if (r && r.ok) { setMsg(t("已克隆到 ") + r.dir); setCloneUrl(""); }
|
|
1781
|
+
else setErr((r && r.error) || t("克隆失败"));
|
|
1782
|
+
} catch (e) { setErr(t("克隆失败:") + e); }
|
|
1783
|
+
finally { setCloneBusy(false); }
|
|
1784
|
+
}, [cloneUrl, dir]);
|
|
1785
|
+
|
|
1786
|
+
// 目录选择器交互(同 ③):
|
|
1787
|
+
const openPick = useCallback(() => { setPickPath(dir || ""); setPickErr(null); setPickOpen(true); }, [dir]);
|
|
1788
|
+
const browseDir = useCallback(async () => {
|
|
1789
|
+
setPicking(true); setPickErr(null);
|
|
1790
|
+
try {
|
|
1791
|
+
const r = await jpost("/pick-dir", { initial: pickPath || dir || undefined });
|
|
1792
|
+
if (r.ok && r.dir) setPickPath(r.dir);
|
|
1793
|
+
else setPickErr(r.error || t("未选择目录"));
|
|
1794
|
+
} catch (e) { setPickErr(t("选择失败:") + e); }
|
|
1795
|
+
finally { setPicking(false); }
|
|
1796
|
+
}, [pickPath, dir]);
|
|
1797
|
+
const confirmPick = useCallback(async () => {
|
|
1798
|
+
const path = String(pickPath || "").trim();
|
|
1799
|
+
if (!path) { setPickErr(t("请输入或选择目录路径")); return; }
|
|
1800
|
+
setPicking(true); setPickErr(null);
|
|
1801
|
+
try {
|
|
1802
|
+
const r = await jpost("/add-workspace", { dir: path });
|
|
1803
|
+
if (r.ok) {
|
|
1804
|
+
setDir(path);
|
|
1805
|
+
if (Array.isArray(r.workspaces) || Array.isArray(r.customDirs)) {
|
|
1806
|
+
const list = [path];
|
|
1807
|
+
if (Array.isArray(r.workspaces)) for (const w of r.workspaces) if (!list.includes(w)) list.push(w);
|
|
1808
|
+
if (Array.isArray(r.customDirs)) for (const c of r.customDirs) if (!list.includes(c)) list.push(c);
|
|
1809
|
+
setDirs(list);
|
|
1810
|
+
}
|
|
1811
|
+
setPickOpen(false); setPickErr(null);
|
|
1812
|
+
setMsg(t("已记住目录") + ":" + path);
|
|
1813
|
+
} else {
|
|
1814
|
+
setPickErr(r.error || t("添加目录失败"));
|
|
1815
|
+
}
|
|
1816
|
+
} catch (e) { setPickErr(t("添加目录失败:") + e); }
|
|
1817
|
+
finally { setPicking(false); }
|
|
1818
|
+
}, [pickPath]);
|
|
1819
|
+
const cancelPick = useCallback(() => { setPickOpen(false); setPickErr(null); }, []);
|
|
1820
|
+
|
|
1821
|
+
const titleExtra = h("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, flexShrink: 0 } },
|
|
1822
|
+
h("span", { style: { fontSize: 12, color: T.secondary } }, t("平台")),
|
|
1823
|
+
h("select", {
|
|
1824
|
+
value: prov,
|
|
1825
|
+
onChange: (e) => { setProv(e.target.value); },
|
|
1826
|
+
title: t("选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换"),
|
|
1827
|
+
style: { font: "inherit", fontSize: 12, padding: "3px 6px", border: "1px solid " + T.border, borderRadius: 6, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
1828
|
+
},
|
|
1829
|
+
h("option", { value: "github" }, t("GitHub(默认)")),
|
|
1830
|
+
h("option", { value: "gitee" }, "Gitee"),
|
|
1831
|
+
)
|
|
1832
|
+
);
|
|
1833
|
+
|
|
1834
|
+
return h(Box, { title: t("④ 克隆仓库"), defaultCollapsed: true, titleExtra },
|
|
1835
|
+
h("div", { style: { display: "flex", gap: 8, marginBottom: 8, alignItems: "center" } },
|
|
1836
|
+
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, t("克隆到目录")),
|
|
1837
|
+
h("select", {
|
|
1838
|
+
value: dir,
|
|
1839
|
+
onChange: (e) => setDir(e.target.value),
|
|
1840
|
+
style: { flex: 1, font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
1841
|
+
},
|
|
1842
|
+
dirs.length === 0 ? h("option", { value: "" }, "…")
|
|
1843
|
+
: dirs.map((d) => h("option", { key: d, value: d }, d))
|
|
1844
|
+
),
|
|
1845
|
+
h(Btn, { label: t("选择目录…"), onClick: openPick, disabled: picking, noBg: true }),
|
|
1846
|
+
h(Btn, { label: loading ? t("加载中…") : t("刷新仓库列表"), onClick: load, disabled: loading, noBg: true, tone: "primary" }),
|
|
1847
|
+
),
|
|
1848
|
+
h("div", { style: { fontSize: 11, color: T.secondary, marginBottom: 8 } }, t("远程仓库列表") + "(" + (prov === "gitee" ? "Gitee" : "GitHub") + "):"),
|
|
1849
|
+
repos.length === 0
|
|
1850
|
+
? h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 8 } }, loading ? t("加载中…") : t("(暂无仓库)"))
|
|
1851
|
+
: h("div", { style: { maxHeight: 240, overflow: "auto", border: "1px solid " + T.border, borderRadius: 8, padding: 6, marginBottom: 8 } },
|
|
1852
|
+
repos.map((repo) =>
|
|
1853
|
+
h("div", { key: repo.fullName || repo.name, style: { display: "flex", alignItems: "center", gap: 8, padding: "5px 4px", borderBottom: "1px solid " + T.border } },
|
|
1854
|
+
h("div", { style: { flex: 1, minWidth: 0 } },
|
|
1855
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
|
|
1856
|
+
h("span", { style: { color: T.label, fontSize: 13, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, repo.name),
|
|
1857
|
+
repo.visibility === "public"
|
|
1858
|
+
? h("span", { style: { fontSize: 11, color: T.success, flexShrink: 0 } }, t("公开"))
|
|
1859
|
+
: h("span", { style: { fontSize: 11, color: T.warn, flexShrink: 0 } }, t("私有")),
|
|
1860
|
+
),
|
|
1861
|
+
repo.description ? h("div", { style: { color: T.secondary, fontSize: 11, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, repo.description) : null,
|
|
1862
|
+
),
|
|
1863
|
+
repo.local
|
|
1864
|
+
? h("span", { style: { fontSize: 12, color: T.success, flexShrink: 0 } }, t("本地已有"))
|
|
1865
|
+
: h(Btn, { label: busyName === repo.name ? t("克隆中…") : t("克隆"), onClick: () => doClone(repo), disabled: !!busyName || !dir, tone: "primary", noBg: true }),
|
|
1866
|
+
)
|
|
1867
|
+
)
|
|
1868
|
+
),
|
|
1869
|
+
// 按仓库地址克隆(支持任意 git 仓库地址,克隆到选择的位置)
|
|
1870
|
+
h("div", { style: { border: "1px solid " + T.border, borderRadius: 8, padding: 8, marginBottom: 8 } },
|
|
1871
|
+
h("div", { style: { fontSize: 11, color: T.secondary, marginBottom: 6 } }, t("也可以按仓库地址克隆到任意位置")),
|
|
1872
|
+
h("input", {
|
|
1873
|
+
type: "text", value: cloneUrl, disabled: cloneBusy,
|
|
1874
|
+
placeholder: t("仓库地址") + "(https://github.com/owner/repo 或 git@gitee.com:owner/repo.git)",
|
|
1875
|
+
onChange: (e) => setCloneUrl(e.target.value),
|
|
1876
|
+
onKeyDown: (e) => { if (e.key === "Enter") void doCloneUrl(); },
|
|
1877
|
+
style: { width: "100%", boxSizing: "border-box", font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", marginBottom: 6 },
|
|
1878
|
+
}),
|
|
1879
|
+
h("div", { style: { display: "flex", gap: 8, alignItems: "center" } },
|
|
1880
|
+
h("div", { style: { fontSize: 11, color: T.secondary, whiteSpace: "nowrap" } }, t("克隆到")),
|
|
1881
|
+
h("input", { type: "text", value: dir, disabled: cloneBusy, placeholder: t("选择目录…"), onChange: (e) => setDir(e.target.value), style: { flex: 1, font: "inherit", fontSize: 12, padding: "5px 8px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none" } }),
|
|
1882
|
+
h(Btn, { label: t("选择目录…"), onClick: openPick, disabled: picking, noBg: true }),
|
|
1883
|
+
h(Btn, { label: cloneBusy ? t("克隆中…") : t("克隆"), onClick: () => void doCloneUrl(), disabled: cloneBusy || !String(dir || "").trim(), tone: "primary", noBg: true }),
|
|
1884
|
+
),
|
|
1885
|
+
),
|
|
1886
|
+
msg ? h("div", { style: { color: T.success, fontSize: 12 } }, "✅ " + msg) : null,
|
|
1887
|
+
err ? h("div", { style: { color: T.warn, fontSize: 12 } }, err) : null,
|
|
1888
|
+
pickOpen ? ReactDOM.createPortal(
|
|
1889
|
+
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
1890
|
+
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: cancelPick }),
|
|
1891
|
+
h("div", { style: { position: "relative", zIndex: 1, width: 480, maxWidth: "calc(100vw - 48px)", background: "var(--dsw-alias-bg-layer-3, #fff)", border: "1px solid " + T.border, borderRadius: 14, padding: 18, color: T.label, boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))" }, role: "dialog", "aria-modal": "true", "aria-label": t("选择目录") },
|
|
1892
|
+
h("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, t("选择代码目录")),
|
|
1893
|
+
h("div", { style: { fontSize: 12, color: T.secondary, marginBottom: 8, lineHeight: 1.6 } },
|
|
1894
|
+
t("可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到常用目录并记住,下次打开无需重新选择。")),
|
|
1895
|
+
h("input", {
|
|
1896
|
+
type: "text", value: pickPath, disabled: picking,
|
|
1897
|
+
placeholder: t("C:\\Users\\你的用户名\\项目目录"),
|
|
1898
|
+
onChange: (e) => setPickPath(e.target.value),
|
|
1899
|
+
onKeyDown: (e) => { if (e.key === "Enter") void confirmPick(); },
|
|
1900
|
+
style: { width: "100%", boxSizing: "border-box", font: "inherit", fontSize: 13, padding: "8px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none" },
|
|
1901
|
+
}),
|
|
1902
|
+
pickErr ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + pickErr) : null,
|
|
1903
|
+
h("div", { style: { display: "flex", gap: 8, marginTop: 14, justifyContent: "flex-end" } },
|
|
1904
|
+
h(Btn, { label: t("浏览…"), onClick: browseDir, disabled: picking, noBg: true }),
|
|
1905
|
+
h(Btn, { label: t("取消"), onClick: cancelPick, disabled: picking }),
|
|
1906
|
+
h(Btn, { label: t("确定"), onClick: confirmPick, tone: "primary", noBg: true, disabled: picking || !String(pickPath || "").trim() })
|
|
1907
|
+
)
|
|
1908
|
+
)
|
|
1909
|
+
),
|
|
1910
|
+
document.body
|
|
1911
|
+
) : null,
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
// ---------- section 5: 发布 npm 包 ----------
|
|
1918
|
+
function NpmSection() {
|
|
1919
|
+
const [dir, setDir] = useState("");
|
|
1920
|
+
const [dirs, setDirs] = useState([]);
|
|
1921
|
+
const [status, setStatus] = useState(null);
|
|
1922
|
+
const [busyStep, setBusyStep] = useState(null);
|
|
1923
|
+
const [out, setOut] = useState({});
|
|
1924
|
+
const [confirmPublish, setConfirmPublish] = useState(false);
|
|
1925
|
+
const [loginBusy, setLoginBusy] = useState(false);
|
|
1926
|
+
const [msg, setMsg] = useState(null);
|
|
1927
|
+
const [err, setErr] = useState(null);
|
|
1928
|
+
// 目录选择器(同 ③/④:输入 / 浏览… / 记住到自定义目录)
|
|
1929
|
+
const [pickOpen, setPickOpen] = useState(false);
|
|
1930
|
+
const [pickPath, setPickPath] = useState("");
|
|
1931
|
+
const [pickErr, setPickErr] = useState(null);
|
|
1932
|
+
const [picking, setPicking] = useState(false);
|
|
1933
|
+
|
|
1934
|
+
const refreshDirs = useCallback(async () => {
|
|
1935
|
+
try {
|
|
1936
|
+
const [def, ws] = await Promise.all([
|
|
1937
|
+
jget("/clone/default-dir").catch(() => null),
|
|
1938
|
+
jget("/workspaces").catch(() => null),
|
|
1939
|
+
]);
|
|
1940
|
+
const list = [];
|
|
1941
|
+
// ⑤ 目标目录默认留空,由用户手动选择(不复选默认目录)。
|
|
1942
|
+
if (def && def.dir) { if (!list.includes(def.dir)) list.push(def.dir); }
|
|
1943
|
+
if (ws) {
|
|
1944
|
+
if (Array.isArray(ws.workspaces)) for (const w of ws.workspaces) if (!list.includes(w)) list.push(w);
|
|
1945
|
+
if (Array.isArray(ws.customDirs)) for (const c of ws.customDirs) if (!list.includes(c)) list.push(c);
|
|
1946
|
+
}
|
|
1947
|
+
setDirs(list);
|
|
1948
|
+
} catch {}
|
|
1949
|
+
}, [dir]);
|
|
1950
|
+
useEffect(() => { void refreshDirs(); }, [refreshDirs]);
|
|
1951
|
+
|
|
1952
|
+
const loadStatus = useCallback(async () => {
|
|
1953
|
+
try {
|
|
1954
|
+
if (!String(dir || "").trim()) { setStatus(null); return; }
|
|
1955
|
+
const r = await jget("/npm/status?dir=" + encodeURIComponent(dir || "")); if (r && r.ok) setStatus(r);
|
|
1956
|
+
}
|
|
1957
|
+
catch (e) { setErr(t("读取 npm 状态失败:") + e); }
|
|
1958
|
+
}, [dir]);
|
|
1959
|
+
useEffect(() => { void loadStatus(); }, [loadStatus]);
|
|
1960
|
+
|
|
1961
|
+
const openLogin = useCallback(async () => {
|
|
1962
|
+
if (!String(dir || "").trim()) { setErr(t("请先选择目标目录")); return; }
|
|
1963
|
+
setLoginBusy(true); setErr(null); setMsg(null);
|
|
1964
|
+
try {
|
|
1965
|
+
const r = await jpost("/npm/login", { dir });
|
|
1966
|
+
if (r && r.ok) setMsg(r.detail || t("登录窗口已打开,请在弹出的终端完成登录后点「重新检查」"));
|
|
1967
|
+
else { setMsg(r && r.command ? t("请手动在终端运行:") + r.command : (r && r.detail) || t("打开终端失败")); }
|
|
1968
|
+
} catch (e) { setErr(t("发布失败:") + e); }
|
|
1969
|
+
finally { setLoginBusy(false); }
|
|
1970
|
+
}, [dir]);
|
|
1971
|
+
|
|
1972
|
+
const runStep = useCallback(async (step) => {
|
|
1973
|
+
if (!String(dir || "").trim()) { setErr(t("请先选择目标目录")); return; }
|
|
1974
|
+
setBusyStep(step); setErr(null); setMsg(null);
|
|
1975
|
+
try {
|
|
1976
|
+
const r = await jpost("/npm/step", { dir, step });
|
|
1977
|
+
setOut((p) => ({ ...p, [step]: (r && r.output) || "" }));
|
|
1978
|
+
if (step === "publish") {
|
|
1979
|
+
if (r && r.ok) setMsg(t("发布完成"));
|
|
1980
|
+
else setErr((r && r.error) || t("发布失败"));
|
|
1981
|
+
}
|
|
1982
|
+
} catch (e) { setErr(t("发布失败:") + e); }
|
|
1983
|
+
finally { setBusyStep(null); }
|
|
1984
|
+
}, [dir]);
|
|
1985
|
+
|
|
1986
|
+
// 目录选择器交互(同 ④):
|
|
1987
|
+
const openPick = useCallback(() => { setPickPath(dir || ""); setPickErr(null); setPickOpen(true); }, [dir]);
|
|
1988
|
+
const browseDir = useCallback(async () => {
|
|
1989
|
+
setPicking(true); setPickErr(null);
|
|
1990
|
+
try {
|
|
1991
|
+
const r = await jpost("/pick-dir", { initial: pickPath || dir || undefined });
|
|
1992
|
+
if (r.ok && r.dir) setPickPath(r.dir);
|
|
1993
|
+
else setPickErr(r.error || t("未选择目录"));
|
|
1994
|
+
} catch (e) { setPickErr(t("选择失败:") + e); }
|
|
1995
|
+
finally { setPicking(false); }
|
|
1996
|
+
}, [pickPath, dir]);
|
|
1997
|
+
const confirmPick = useCallback(async () => {
|
|
1998
|
+
const path = String(pickPath || "").trim();
|
|
1999
|
+
if (!path) { setPickErr(t("请输入或选择目录路径")); return; }
|
|
2000
|
+
setPicking(true); setPickErr(null);
|
|
2001
|
+
try {
|
|
2002
|
+
const r = await jpost("/add-workspace", { dir: path });
|
|
2003
|
+
if (r.ok) {
|
|
2004
|
+
setDir(path);
|
|
2005
|
+
if (Array.isArray(r.workspaces) || Array.isArray(r.customDirs)) {
|
|
2006
|
+
const list = [path];
|
|
2007
|
+
if (Array.isArray(r.workspaces)) for (const w of r.workspaces) if (!list.includes(w)) list.push(w);
|
|
2008
|
+
if (Array.isArray(r.customDirs)) for (const c of r.customDirs) if (!list.includes(c)) list.push(c);
|
|
2009
|
+
setDirs(list);
|
|
2010
|
+
}
|
|
2011
|
+
setPickOpen(false); setPickErr(null);
|
|
2012
|
+
setMsg(t("已记住目录") + ":" + path);
|
|
2013
|
+
} else {
|
|
2014
|
+
setPickErr(r.error || t("添加目录失败"));
|
|
2015
|
+
}
|
|
2016
|
+
} catch (e) { setPickErr(t("添加目录失败:") + e); }
|
|
2017
|
+
finally { setPicking(false); }
|
|
2018
|
+
}, [pickPath]);
|
|
2019
|
+
const cancelPick = useCallback(() => { setPickOpen(false); setPickErr(null); }, []);
|
|
2020
|
+
|
|
2021
|
+
const steps = [
|
|
2022
|
+
{ key: "registry", label: t("① 确认源(registry)") },
|
|
2023
|
+
{ key: "list", label: t("② 查看认证配置(config list)") },
|
|
2024
|
+
{ key: "whoami", label: t("③ 验证身份(whoami)") },
|
|
2025
|
+
{ key: "pack", label: t("④ 预览打包文件(pack --dry-run)") },
|
|
2026
|
+
{ key: "publish", label: t("⑤ 发布(publish)") },
|
|
2027
|
+
];
|
|
2028
|
+
|
|
2029
|
+
return h(Box, { title: t("⑤ 发布 npm 包"), defaultCollapsed: true },
|
|
2030
|
+
h("div", { style: { fontSize: 11, color: T.secondary, marginBottom: 8 } }, t("发布 npm 包五步向导:确认源 → 认证配置 → 身份 → 打包预览 → 发布")),
|
|
2031
|
+
h("div", { style: { display: "flex", gap: 8, marginBottom: 8, alignItems: "center" } },
|
|
2032
|
+
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, t("目标目录(含 package.json)")),
|
|
2033
|
+
h("select", {
|
|
2034
|
+
value: dir,
|
|
2035
|
+
onChange: (e) => setDir(e.target.value),
|
|
2036
|
+
style: { flex: 1, font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
2037
|
+
},
|
|
2038
|
+
dirs.length === 0 ? h("option", { value: "" }, "…")
|
|
2039
|
+
: dirs.map((d) => h("option", { key: d, value: d }, d))
|
|
2040
|
+
),
|
|
2041
|
+
h(Btn, { label: t("选择目录…"), onClick: openPick, disabled: picking, noBg: true }),
|
|
2042
|
+
h(Btn, { label: t("重新检查"), onClick: loadStatus, noBg: true, disabled: !String(dir || "").trim() }),
|
|
2043
|
+
),
|
|
2044
|
+
!String(dir || "").trim()
|
|
2045
|
+
? h("div", { style: { fontSize: 12, color: T.warn, marginBottom: 8 } }, "⚠️ " + t("请先选择目标目录(当前留空,不会自动使用默认目录)"))
|
|
2046
|
+
: null,
|
|
2047
|
+
status ? h("div", { style: { fontSize: 12, color: T.secondary, marginBottom: 8, lineHeight: 1.6 } },
|
|
2048
|
+
status.dirExists === false
|
|
2049
|
+
? h("div", { style: { color: T.danger } }, "❌ " + t("目录不存在:") + (status.dir || ""))
|
|
2050
|
+
: null,
|
|
2051
|
+
status.hasPackageJson === false
|
|
2052
|
+
? h("div", { style: { color: T.warn } }, "⚠️ " + t("该目录没有 package.json(请选择含 package.json 的包目录)"))
|
|
2053
|
+
: null,
|
|
2054
|
+
h("div", null, (status.installed ? "" : t("npm 未安装(请先在 ① 环境检查安装)") + " ") + (status.npmPath || "npm")),
|
|
2055
|
+
status.registry ? h("div", null, t("registry:") + status.registry) : null,
|
|
2056
|
+
status.whoami
|
|
2057
|
+
? h("div", { style: { color: T.success } }, t("已登录:") + status.whoami)
|
|
2058
|
+
: h("div", { style: { color: T.warn } }, "⚠️ " + (status.whoamiError || t("未登录 npm"))),
|
|
2059
|
+
) : null,
|
|
2060
|
+
status && status.installed && !status.whoami
|
|
2061
|
+
? h("div", { style: { border: "1px dashed " + T.border, borderRadius: 8, padding: 8, marginBottom: 8, background: T.layer1 } },
|
|
2062
|
+
h("div", { style: { fontSize: 12, color: T.warn, marginBottom: 6 } }, t("未登录 npm,发布前需先登录")),
|
|
2063
|
+
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
|
|
2064
|
+
h(Btn, { label: loginBusy ? t("执行中…") : t("打开终端执行 npm login"), onClick: openLogin, disabled: loginBusy || !String(dir || "").trim(), tone: "primary", noBg: true }),
|
|
2065
|
+
h(Btn, { label: t("复制登录命令"), onClick: () => { void copyText("npm login"); setMsg(t("已复制:") + "npm login"); }, noBg: true }),
|
|
2066
|
+
),
|
|
2067
|
+
) : null,
|
|
2068
|
+
steps.map((s) =>
|
|
2069
|
+
h("div", { key: s.key, style: { marginBottom: 6 } },
|
|
2070
|
+
h("div", { style: { display: "flex", gap: 8, alignItems: "center" } },
|
|
2071
|
+
h(Btn, {
|
|
2072
|
+
label: busyStep === s.key ? t("执行中…") : t("执行"),
|
|
2073
|
+
onClick: () => runStep(s.key),
|
|
2074
|
+
disabled: !!busyStep || !dir || (s.key === "publish" && !confirmPublish),
|
|
2075
|
+
noBg: true, tone: s.key === "publish" ? "danger" : undefined,
|
|
2076
|
+
}),
|
|
2077
|
+
h("span", { style: { flex: 1, color: T.label, fontSize: 12 } }, s.label),
|
|
2078
|
+
),
|
|
2079
|
+
out[s.key] ? h("pre", { style: { marginTop: 4, whiteSpace: "pre-wrap", wordBreak: "break-all", fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 11, lineHeight: 1.5, color: T.secondary, background: T.layer1, borderRadius: 8, padding: 8, maxHeight: 160, overflow: "auto" } }, out[s.key]) : null,
|
|
2080
|
+
)
|
|
2081
|
+
),
|
|
2082
|
+
h("label", { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 6, fontSize: 12, color: T.label, cursor: "pointer" } },
|
|
2083
|
+
h("input", { type: "checkbox", checked: confirmPublish, onChange: (e) => setConfirmPublish(e.target.checked) }),
|
|
2084
|
+
t("我已核对以上内容,确认发布到 npm registry")
|
|
2085
|
+
),
|
|
2086
|
+
msg ? h("div", { style: { color: T.success, fontSize: 12, marginTop: 6 } }, "✅ " + msg) : null,
|
|
2087
|
+
err ? h("div", { style: { color: T.danger, fontSize: 12, marginTop: 6 } }, "❌ " + err) : null,
|
|
2088
|
+
pickOpen ? ReactDOM.createPortal(
|
|
2089
|
+
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
2090
|
+
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: cancelPick }),
|
|
2091
|
+
h("div", { style: { position: "relative", zIndex: 1, width: 480, maxWidth: "calc(100vw - 48px)", background: "var(--dsw-alias-bg-layer-3, #fff)", border: "1px solid " + T.border, borderRadius: 14, padding: 18, color: T.label, boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))" }, role: "dialog", "aria-modal": "true", "aria-label": t("选择目录") },
|
|
2092
|
+
h("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, t("选择代码目录")),
|
|
2093
|
+
h("div", { style: { fontSize: 12, color: T.secondary, marginBottom: 8, lineHeight: 1.6 } },
|
|
2094
|
+
t("可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到常用目录并记住,下次打开无需重新选择。")),
|
|
2095
|
+
h("input", {
|
|
2096
|
+
type: "text", value: pickPath, disabled: picking,
|
|
2097
|
+
placeholder: t("C:\\Users\\你的用户名\\项目目录"),
|
|
2098
|
+
onChange: (e) => setPickPath(e.target.value),
|
|
2099
|
+
onKeyDown: (e) => { if (e.key === "Enter") void confirmPick(); },
|
|
2100
|
+
style: { width: "100%", boxSizing: "border-box", font: "inherit", fontSize: 13, padding: "8px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none" },
|
|
2101
|
+
}),
|
|
2102
|
+
pickErr ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + pickErr) : null,
|
|
2103
|
+
h("div", { style: { display: "flex", gap: 8, marginTop: 14, justifyContent: "flex-end" } },
|
|
2104
|
+
h(Btn, { label: t("浏览…"), onClick: browseDir, disabled: picking, noBg: true }),
|
|
2105
|
+
h(Btn, { label: t("取消"), onClick: cancelPick, disabled: picking }),
|
|
2106
|
+
h(Btn, { label: t("确定"), onClick: confirmPick, tone: "primary", noBg: true, disabled: picking || !String(pickPath || "").trim() })
|
|
2107
|
+
)
|
|
2108
|
+
)
|
|
2109
|
+
),
|
|
2110
|
+
document.body
|
|
2111
|
+
) : null,
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1608
2115
|
// ---------- the main panel ----------
|
|
1609
2116
|
// `variant` 决定布局:'drawer'(右侧栏集成面板,显示关闭按钮、填充容器)与 'tab'
|
|
1610
2117
|
// (作为 dsh-better-sidebar 侧边栏 Tab 内容,填充容器、隐藏关闭按钮)。
|
|
@@ -1620,12 +2127,16 @@ function useSession() {
|
|
|
1620
2127
|
variant === "tab" ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": t("关闭"), onClick: onClose }, "✕")
|
|
1621
2128
|
),
|
|
1622
2129
|
h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1623
|
-
t("按顺序完成:①环境检查 → ②SSH 密钥与连接 →
|
|
2130
|
+
t("按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理,以及 ④克隆仓库 / ⑤发布 npm 包。推送会自动忽略 >100MB 的文件(") + (provider === "gitee" ? "Gitee" : "GitHub") + t(" 限制)并说明原因。")),
|
|
1624
2131
|
h(EnvSection, null),
|
|
1625
2132
|
h("div", { style: { height: 12 } }),
|
|
1626
2133
|
h(SshSection, { provider, setProvider }),
|
|
1627
2134
|
h("div", { style: { height: 12 } }),
|
|
1628
|
-
h(RepoSection, { provider })
|
|
2135
|
+
h(RepoSection, { provider }),
|
|
2136
|
+
h("div", { style: { height: 12 } }),
|
|
2137
|
+
h(CloneSection, { provider, setProvider }),
|
|
2138
|
+
h("div", { style: { height: 12 } }),
|
|
2139
|
+
h(NpmSection, null)
|
|
1629
2140
|
);
|
|
1630
2141
|
}
|
|
1631
2142
|
|