安装 DSH 扩展(源码 fork 流水线)
装扩展 = 拿到源码的归属权,再让 harness 引用它。本机总纲(ADR-0007):生产 profile 里不出现 npm 交付的依赖,一切走 link: 源码安装;所以"装扩展"永远是"fork → 克隆 → link:"三步,不是 pnpm add <包名>。
与 dsh-extension-dev 的分工:那个技能管「怎么写/改扩展」,本技能管「怎么把现成扩展装进来」。写完一个新扩展要装进生产 → 用本技能。
先分形态,再选链路
问一句「这个扩展是插件还是技能?」,后续每一步都不同:
| | 插件形态 | 技能分组仓形态 | MCP 形态 |
|---|---|---|---|
| 是什么 | 独立 npm 包(有 package.json、dsh.bundle) | 一组 SKILL.md 目录 | 外部 MCP server 的接入配置 |
| 落点 | dsh-extensions/vendor/<仓库名>/ | dsh-extensions/skills/<上游仓库名>/ | ~/.dsh/profiles/web/cordis.patch.yml |
| 接入 | profiles/web/package.json 的 link: 依赖 | install-skill.sh 递归扫 SKILL.md 软链 | insert 一行 @deepseek-ai/dsh-mcp-client |
| 生效 | 必须重启 | 不必重启(技能按需扫描) | 不必重启(patch 层热加载) |
| 父仓指针 | submodule | submodule |
两者都是 git submodule,装完必须回 dsh-extensions 更新指针(ADR-0005:这是固定成本,不是收尾可选项)。
不属于本技能:动态 Cordis 插件(
cordis_define/cordis_run,进程内、重启即失,见cordis-plugin-development);MCP 服务接入(改cordis.patch.yml挂 mcp 行,不是源码扩展)。
通用侦察(插件与技能形态相同;MCP 只需第 2 步)
MCP 形态跳过第 1、3 步——它不 fork、不上游,只需第 2 步确认「是不是已经接过了」,然后直接进链路 C。
1. 定位上游仓库
要 fork 就得先有确切的上游 URL。来源通常是用户给的链接或包名。若只给了包名:
# 已发布到 npm 的扩展:从 registry 反查仓库地址
npm view <包名> repository.url
拿到上游后确认它真的是 DSH 扩展(package.json 里有 dsh.bundle 或 dsh.client;技能则是仓库里有 SKILL.md)。
2. 本地侦察——可能早就装过了
# 已装的 profile 依赖(link: 的都是源码插件)
cat ~/.dsh/profiles/web/package.json | python3 -m json.tool | grep -A20 dependencies
# 已装的技能
ls ~/.dsh/skills/ | grep -i <关键词>
# 已接入的 MCP(profile 与 home 两处 user patch 层)
grep -n 'serverName' ~/.dsh/profiles/web/cordis.patch.yml ~/.dsh/cordis.patch.yml 2>/dev/null
# 已在 vendor/ 或 skills/ 里的
ls ~/projects/MyAI/dsh-extensions/vendor/ ~/projects/MyAI/dsh-extensions/skills/
已存在就不要重装:直接改现成的那份(回到 dsh-extension-dev 规矩一的复用分支)。
3. 验证可达性(决定能否 fork)
git ls-remote <上游URL> HEAD # exit 0 = 可 clone
gh repo view <owner>/<repo> --json name,visibility,isFork
git ls-remote 失败通常是网络/代理问题,见「常见坑」。
场景:它已经装好了怎么办
第 2 步侦察要是发现目标已装,不要重装——重跑 pnpm install 会重算 lockfile 引入漂移风险,而且不会带来任何新能力。改成巡检,逐条只读核实,哪条不过才补哪条:
# ① 依赖与层栈
grep -E '<包名>|link:' ~/.dsh/profiles/web/package.json
# ② 软链真的落在源码上
ls -la ~/.dsh/profiles/web/node_modules/<包名>
# ③ 产物新鲜(见 A5)
find <源码>/src -newer <源码>/lib/index.js -type f | head
# ④ submodule 指针已提交(见「验收清单」第 1 项)
cd ~/projects/MyAI/dsh-extensions && git ls-tree HEAD vendor/<repo>
# ⑤ 无悬空软链(改名/移动过检出时的典型故障)
find ~/projects/MyAI/dsh-extensions -xtype l | wc -l # 期望 0
# ⑥ 服务健康
systemctl --user show dsh-web -p NRestarts # 期望不是很大的数
最强的证据是运行时:插件的 Tool 出没出现在工具表、它带来的技能出没出现在会话技能目录。文件摆对只说明「摆对了」,功能真的可见才证明「加载起来了」。
巡检结论是四层都不用提交时,这本身就是正确交付——不要为了"做了点什么"而制造改动。
链路 A:插件形态
A1. fork 上游到自己的账号
# 账号 xgx1(gh 已登录)。不指定 --org 时 fork 到当前登录账号
gh repo fork <owner>/<repo> --clone=false
# → 得到 https://github.com/xgx1/<repo>
gh repo view xgx1/<repo> --json name,isFork,parent # 确认 fork 真的建好了
默认走 fork:fork 之后 origin 是自己的仓,本机改动才能推送回去、才能跨设备重建;上游降级为 upstream 远端只作对照。
但要如实区分现状,别把两个分组的设计搞混(2026-09-13 实测):
| 分组 | 现状 | 设计 |
|---|---|---|
| skills/ | 14/14 都是 xgx1/* fork(或自建仓) | fork——本机改编版是主内容,要能推(ADR-0006) |
| vendor/ | 3/3 都直连上游(omdsh-dev/*、loudMore/*),没有 upstream 远端 | 上游克隆,只跟踪(ADR-0005) |
所以「必须 fork」是 skills/ 的纪律,不是 vendor/ 的现状。在 vendor/ 里装新插件时:
- 你打算改这个插件的源码 → 先 fork,再
git submodule addfork 的 URL(origin=fork → 能推) - 只跟着上游走、不改 → 可以直连上游,但那样你推不了任何修复;要推送就必须先 fork
存量迁移(把已直连上游的 vendor 子模块改成 fork,非必须):
gh repo fork <owner>/<repo> --clone=false
cd ~/projects/MyAI/dsh-extensions
git submodule set-url vendor/<repo> https://github.com/xgx1/<repo>.git
git submodule sync vendor/<repo> && git -C vendor/<repo> remote set-url origin https://github.com/xgx1/<repo>.git
git -C vendor/<repo> remote add upstream <上游URL>
git add .gitmodules vendor/<repo> && git commit -m "chore: vendor/<repo> 改指 fork"
重建 submodule 期间 link: 会短暂指向不可用路径,不要在生产运行时段做。
A2. 克隆 fork 并登记为 submodule
关键:用 git submodule add,不要先 git clone 再补。前者一次性写进 .gitmodules,后者只会留下一个未登记的 gitlink,父仓记不住它是什么。
cd ~/projects/MyAI/dsh-extensions
git submodule add https://github.com/xgx1/<repo>.git vendor/<repo> # A1 走了 fork 时
cd vendor/<repo>
git remote add upstream <上游URL> # 上游只作对照,不覆盖本地
git fetch upstream
git log --oneline -1 upstream/<分支> # 确认对照基线拿得到
A3. 确认包名——不等于仓库名
python3 -c "import json;d=json.load(open('package.json'));print(d['name']);print('bundle:',d.get('dsh',{}).get('bundle',{}).get('patch'))"
name 和仓库名经常不同(例:仓库 dsh-genui → 包名 @changfenhuang/dsh-genui)。后续 link: 的键必须用这个 name。
A4. 接入 profile(dsh plugin add 一次搞定两件事)
dsh plugin --profile web add link:/home/sx/projects/MyAI/dsh-extensions/vendor/<repo>
它是一条 pnpm 转发器,会依次做三件事:
- 在
~/.dsh/profiles/web/里执行pnpm add link:<绝对路径>→ 写入 dependencies - 因 cwd 是 profile 目录,相对路径会被锚到调用处;生产配置里一律写绝对路径
- 自动 reconcile
dsh.profile.bundles:把声明了dsh.bundle的依赖追加进 bundles 层栈(源码见deepseek-harness/apps/cli/src/plugin.ts的reconcilePlugins)
所以 bundles 不需要手改。仅在 dsh plugin add 不可用时(比如 pnpm 报错后手工修的)才手工补:编辑 ~/.dsh/profiles/web/package.json,依赖键用 A3 查到的 name,并把它加进 dsh.profile.bundles 数组。
装完复查一次:
grep -E '<包名>|link:' ~/.dsh/profiles/web/package.json
A5. 构建产物必须新鲜
link: 引用的是源码,但 profile 真正加载的是构建产物。只检查目录存在是不够的——源码改过、产物没重建,是最隐蔽的失效方式(插件照常加载,跑的是旧代码)。
# 1. 产物在不在(按 package.json 的 main/exports 找,通常是 lib/ 或 dist/)
ls lib/ dist/ 2>/dev/null
# 2. 产物新不新:有没有 src 文件比入口产物更晚
find src -newer lib/index.js -type f 2>/dev/null | head # 有输出 = 陈旧,需要重建
需要重建时:
pnpm install # vendor/ 下的克隆常没有 node_modules(实测 dsh-genui 就没有)
pnpm build # 按上游 README;产物目录通常被上游 .gitignore 忽略,不进任何仓库
pnpm install 在这类仓库里有两个已知拦路虎:ERR_PNPM_IGNORED_BUILDS(按报错点名的键加进该目录 pnpm-workspace.yaml 的 allowBuilds)与网络代理(见「常见坑」)。
运行时依赖必须能解析(2026-09-13 实测):link: 插件的 import 由 Node 从 vendor 真实路径解析,不走 profile 的 node_modules;插件的 lib/*.js 里 externalize 的 @deepseek-ai/* 不会自动看到安装闭包,缺一个就整棵插件树加载失败(dsh-web 进入崩溃重启循环,且报错先撞上哪个包就报哪个)。先看产物到底 import 了什么:
cd ~/projects/MyAI/dsh-extensions/vendor/<repo>
grep -rhoE 'from "[^.][^"]*"' lib/*.js | sort -u
修法(按优先级):
- 把
lib/里出现的@deepseek-ai/*声明进dependencies(推荐,pnpm install --prod也能重建)。版本对齐生产 harness,例如"@deepseek-ai/dsh-llm": "^0.1.5-rc.2"。 - 同时把
devDependencies里对应包对齐到同一生产版本:pnpm install(完整安装)必须能产出可解析的运行时,否则重建时又回到坑里。注意 npm 上部分dsh-client-*包停在旧版本(如dsh-client-runtime只有 0.1.1-rc.2),client 包以实际可得的最近版本为准,只用于类型/构建,不进运行时。 - 不要手工把 vendor
node_modules/@deepseek-ai/*软链到~/.dsh/profiles/node_modules:pnpm install会覆盖,实例/版本又随 profile heal 漂移,无法从 git 重建(已弃用)。
判断插件的 import 是否全部可解析,不必起服务:
cd ~/.dsh/profiles/web && node --input-type=module -e "await import('<包名>'); console.log('import OK')"
版本上跳(如 0.1.1 → 0.1.5)时,client 插件要额外核对:服务改名(例:conversationEvents → uiConversation.events)、slot 名与 owner props、dsh.client.inject 的模块行是否存在、以及服务端事件形状(例:系统提示词从 request/header 移到 system/message)。推荐流程:pnpm typecheck 定位类型漂移 → pnpm test → pnpm build → 无头浏览器开一次页面看 Failed to load plugins 横幅与插件控件。
A6. 重启(见下方「重启规则」)
为什么 bundle 装完必须重启(不是凭经验,是机制):profile-boot.ts 的 composeLive() 只重读两个 user patch 文件——profiles/web/cordis.patch.yml 与 ~/.dsh/cordis.patch.yml;bundle 层的 patch 在 boot 时就被快照进 composed.bundlePatches,不参与热重载。所以:
| 改什么 | 生效方式 |
|---|---|
| profiles/web/cordis.patch.yml、~/.dsh/cordis.patch.yml | 热重载,立即生效 |
| 新增 bundle / 改 bundle 自己的 cordis.patch.yml | 必须重启 |
| bundle 的 lib 产物 | 必须重启 |
⚠️ profile patch 里那句「DSH 会热加载本 patch,改完立刻生效」只对它自己这个文件成立,不能外推到 bundle 层。
链路 B:技能分组仓形态
B1. 确定分组
ls ~/projects/MyAI/dsh-extensions/skills/ # 已有分组
- 有对应上游 → fork 后放
skills/<上游仓库名>/,组内目录与上游一一对应 - 本机自写、无上游 → 放进
self-*自建组(self-dsh自研 DSH 运维 /self-ops系统运维 /self-ueUE 实践)
B2. fork + 克隆(有上游时)
gh repo fork <owner>/<repo> --clone=false
cd ~/projects/MyAI/dsh-extensions
git submodule add https://github.com/xgx1/<repo>.git skills/<上游仓库名>
cd skills/<上游仓库名> && git remote add upstream <上游URL> && git fetch upstream
有上游的组必须带 upstream 远端(ADR-0006)——git diff upstream/<分支> 就是「本机改了什么」的权威答案。self-* 自建组没有 upstream。
B3. 放置技能目录
技能目录 = 含 SKILL.md 的目录,放哪个层级都行(安装器按 SKILL.md 递归发现,不限深度)。上游的层级原样保留(skills/<名>/、.agents/skills/<名>/、plugins/<插件>/skills/<名>/);本机独有技能放到该组的主技能根下。
一个技能 = 一个目录,最少一个 SKILL.md。frontmatter 至少 name 与 description;name 用字母数字连字符,安装器优先取 frontmatter 的 name:,没有才用目录名。
B4. 软链 + 验证
cd ~/projects/MyAI/dsh-extensions
./install-skill.sh --dry-run # 预演:确认「新建 N」与实际新增数一致
./install-skill.sh # 真正软链进 ~/.dsh/skills/
./install-skill.sh --list | grep <技能名>
ls -la ~/.dsh/skills/<技能名> # 应是指向分组仓的符号链接
技能够了,不必重启:DSH 按需扫描技能目录,软链建好即可用。
链路 C:MCP 形态
MCP 是配置层,不是依赖层——不要 fork、不要 git submodule add、不要改 package.json。
@deepseek-ai/dsh-mcp-client 是 DSH 自带包,每个 MCP server 只是 /profiles/web/cordis.patch.yml 里的一个 insert 块。这也和"一切走源码安装"的总纲(ADR-0007)不冲突:那条总纲针对的是要装进 profile 的插件依赖,MCP 没有依赖可装。
C1. 先确认 server 本体可用
MCP 只负责"接进来",不负责"装上"。接入前先确保那台 server 真的能跑:
# stdio 型:命令必须在 PATH 里(DSH 在它自己的进程环境里解析命令)
command -v <command> && <command> --help 2>&1 | head -3
# http 型:服务得先起着
curl -sS -o /dev/null -w '%{http_code}\n' <url>
stdio 型 server 的安装属于普通软件安装(npm i -g / uv tool install / pacman…),做完要确认命令落在 PATH 上——MCP 子进程继承的是 dsh-web 的环境。
C2. 写 insert 块
在 ~/.dsh/profiles/web/cordis.patch.yml 末尾追加(一个 server 一个块,不要塞进已有的块里):
# stdio 型:DSH 自己拉起子进程
- insert:
- id: mcp-<名字>
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: <名字> # 工具前缀就是 mcp__<名字>__
transport: stdio
command: <命令>
args:
- '<参数>'
# http 型:连已存在的服务
- insert:
- id: mcp-<名字>
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: <名字>
transport: streamable-http
url: http://127.0.0.1:<端口>/mcp
字段说明:id 与 serverName 都要唯一(同一个 serverName 注册两次会冲突);transport 只有 stdio 与 streamable-http 两种。
C3. 首次务必让失败可见(failOnStartupError)
默认(不写这个字段)是 true:MCP 起不来就报错。第一次接一个 server 时保持默认——错了立刻知道。
反过来,这种配置会静默消失:failOnStartupError: false + 命令不存在 / 端口没服务。本机就踩过——headroom 的旧配置写着一个从不存在的 127.0.0.1:8791,又带着 false,于是 DSH 默默跳过,会话里一直没有 mcp__headroom__* 工具,没人发现。所以:
- 调试期用默认(true),确认稳定后再决定是否放宽
- 写
failOnStartupError: false时必须配一条注释说明为什么可以容忍它不在(例:mcp-ue依赖编辑器开着)
C4. 生效:热加载,不用重启
profiles/web/cordis.patch.yml 属于 user patch 层,DSH 会热加载它——改完立即重拉 MCP 子进程。这条只对 user patch 层成立(profiles/web/cordis.patch.yml 与 ~/.dsh/cordis.patch.yml);bundle 层 patch 不参与热加载,那是插件形态才要重启的原因(见 A6)。
验证:
# ① 子进程真的被拉起来了(父进程应为 dsh-web 的 MainPID)
pgrep -af '<命令>' | head
systemctl --user show dsh-web.service -p MainPID
# ② 起不来时看日志
journalctl --user -u dsh-web -n 60 | grep -i mcp
# ③ 最终判据:新会话的工具表里出现 mcp__<serverName>__*
若配置写坏导致 dsh-web 崩溃循环:直接在那个块里加 disabled: true 整个禁用它,或删掉该块;热加载会让它立刻退场。别让服务留在重启循环里(每 3s 一次,NRestarts 飙升)。
C5. 提交(这一步才是"换台设备也能用")
好消息:profiles/web/cordis.patch.yml 本来就在 ~/.dsh 仓库的跟踪范围内,所以改完它天然会随仓库走:
cd ~/.dsh
git add profiles/web/cordis.patch.yml
git commit -m "feat: 接入 <名字> MCP"
git push
但配置能同步 ≠ 那台设备就能用:stdio 型依赖那台设备的 PATH 里有那个命令,http 型依赖那台设备的服务在跑。所以注释里要写清前置条件,否则新设备上就是 C3 那种静默消失。写注释时至少交代:server 本体怎么装、依赖什么外部服务、代理要不要清(本机 headroom 那条就写明了必须清 all_proxy,否则 MCP 子进程连本机 8787 会报 socksio 缺失)。
重启规则
插件形态装完必须重启 dsh-web.service 才会加载新代码。但重启会杀掉当前会话——包括正在执行本流程的 agent——所以顺序和时机是硬要求:
- 先做完一切:报告、验收证据、后续指令全部在重启命令之前完成。重启后再补一句是不可能的。
- 再延时重启,用 systemd 的定时器把重启与本进程解耦,让当前会话有机会先把话说完:
# 10 秒后重启:定时单元独立于本进程,报告来得及发出去
systemd-run --user --collect --on-active=10 --unit=dsh-restart-once \
systemctl --user restart dsh-web.service
--collect让一次性单元执行后自动清理,单元名可以重复使用(不加它,第二次装扩展会因单元名已存在而失败)。它不影响取消:延迟窗口内systemctl --user stop dsh-restart-once.timer仍可撤销(实测)。- 命令返回即表示重启已排程,此刻还没断线——本轮最终回复必须在 10 秒内说完;排程后不要再发新一轮工具调用(每个调用都在消耗延迟窗口)。
- 延迟取 10 秒:给「排程返回 → 报告说完」留余量。3 秒会让长报告说到一半就被切断。
不要用 nohup ... sleep 3 ... & 兜底:dsh-web.service 是 KillMode=control-group(已实测),重启会杀掉整个 cgroup,连你的后台延时进程一起带走,重启永远不会发生。systemd-run 不在同组,所以只有它能做这件事。
若环境无 systemd-run:放弃延时,把报告发完之后直接执行下面这条,并明确告知用户页面会短暂中断:
systemctl --user restart dsh-web.service
- 报告里必须写明:重启已排程、页面会自动恢复、刷新即可;并给出恢复后的验证命令:
systemctl --user is-active dsh-web.service # 期望 active
systemctl --user show dsh-web -p NRestarts # 期望不是很大的数
journalctl --user -u dsh-web -n 60 # 起不来时看这里
重启失败会导致 dsh-web 进入崩溃重启循环(每 3s 一次,NRestarts 飙升),排查见「常见坑」。
Windows(PowerShell)
本机无 Windows 实例,以下为等价形态、未在本机验证:
# 延时重启:无 systemd,用计划任务或分离进程
Start-Process powershell -ArgumentList '-Command','Start-Sleep 3; Restart-Service dsh-web'
Get-Service dsh-web
验收清单
装完逐项确认,缺一项就不算装好。前 4 项两条链路都要;后 2 项按形态二选一——插件形态不看技能项,技能形态不看插件项:
通用(插件、技能都要)
- [ ] submodule 已提交:
git ls-tree HEAD <路径>返回160000 commit …。别用git status判断——指针提交后 status 本来就是干净的,它无法区分「已提交」与「根本没登记」 - [ ] 上游可对照:有上游的仓库
git remote -v里能看到upstream - [ ]
dsh-extensions的git status不再显示该 submodule 为 modified(这才是git status的正确用法:看漏更新指针) - [ ] 功能实际可见(新 Tool / 新界面 / 设置项 / 新技能)
插件形态专属
- [ ]
~/.dsh/profiles/web/package.json的 dependencies 里有指向 fork 的link:,路径是绝对路径 - [ ]
dsh.profile.bundles里出现了该包名(dsh plugin add自动加;手工装的要自己加) - [ ]
ls -la ~/.dsh/profiles/web/node_modules/<包名>指向 vendor 源码 - [ ] 重启后
systemctl --user is-active dsh-web.service=active,页面能打开
技能形态专属
- [ ]
./install-skill.sh --list能列出该技能,~/.dsh/skills/<名>是指向分组仓的软链
插件自带技能时不看上面的技能项:有些插件通过
ctx.skills.registerProvider()在运行时提供技能(例:dsh-genui的~/.dsh/skills/genui不存在是正常的)。install-skill.sh只扫skills/、update-app/skills/、各项目.dsh/skills/三个源,不扫vendor/——所以在~/.dsh/skills/里找不到它不代表装漏了,看会话技能目录里有没有出现即可。
回滚
dsh plugin --profile web remove <包名> # 依赖与 bundles 一并退场
cd ~/projects/MyAI/dsh-extensions && git rm vendor/<repo> && git commit
systemd-run --user --collect --on-active=10 --unit=dsh-restart-once systemctl --user restart dsh-web.service
常见坑
| 症状 | 原因与修法 |
|---|---|
| pnpm 超时 / fetch failed | 代理没设。Linux:export https_proxy=http://127.0.0.1:7897 http_proxy=http://127.0.0.1:7897 all_proxy=socks5://127.0.0.1:7897;Windows:$env:HTTPS_PROXY="http://127.0.0.1:7897" |
| ERR_PNPM_IGNORED_BUILDS | 依赖带 postinstall 被 pnpm 拦下。按报错点名的确切键加进 ~/.dsh/profiles/web/pnpm-workspace.yaml 的 allowBuilds,再重跑 |
| lockfile 漂移报错 | 加 --no-frozen-lockfile |
| 重启后 plugin tree failed to load、页面打不开 | 新插件没激活(多半 bundles 缺项,或它自己的 cordis.patch.yml 有语法错)。别让它留在崩溃循环里:先 journalctl --user -u dsh-web -n 60 看报错,改完再重启 |
| Cannot find package '@deepseek-ai/…' imported from …/vendor/… | 两种原因:① 检出被改名/移动过,vendor 里的绝对符号链接悬空——find ~/projects/MyAI/dsh-extensions -xtype l 排查后重指;② 插件 externalize 了 @deepseek-ai/*,但 vendor 的 node_modules 里没有可解析副本(没装依赖,或 devDeps 还是旧版本)。修法见 A5「运行时依赖必须能解析」,装好再重启 |
| 重启后当前对话断了、agent 没来得及报告 | 重启前没把话说完。回到「重启规则」第 1 步 |
| 装完 dsh-extensions 一直显示 dirty | 没更新父仓指针。git add <submodule 路径> && git commit |
| git submodule add 报路径已存在 | 目录已被 git clone 占位。删掉重新用 submodule add,或 git submodule add --force |
提交纪律
装完要让改动可重建,三个仓库按层级各提交一次(ADR-0005 的两层拓扑):
# 1. 上游 fork 仓(只在改了它的源码时)
cd dsh-extensions/vendor/<repo> && git add -A && git commit -m "..." && git push origin main
# 2. dsh-extensions:更新 submodule 指针
cd ~/projects/MyAI/dsh-extensions && git add .gitmodules vendor/<repo> && git commit -m "..." && git push
# 3. MyAI:更新 dsh-extensions 指针
cd ~/projects/MyAI && git add dsh-extensions && git commit -m "..." && git push
~/.dsh 是独立仓库(xgx1/dsh-home),profile 的 link: 改动记在它的仓库里:
cd ~/.dsh && git add profiles/web/package.json profiles/web/pnpm-lock.yaml && git commit -m "..." && git push
这是四个仓库——漏掉任何一个,换台设备就重建不出来。
Scan to join WeChat group