dsh-mobile-plugin 0.1.95 → 0.1.97
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.md +73 -50
- package/config/agent-presets/standard/agent.cordis.yml +13 -0
- package/dsh-mobile-credentials/lib/credentials.js +24 -1
- package/lib/git/exec.js +129 -0
- package/lib/git/package.json +3 -0
- package/lib/git/parse.js +247 -0
- package/lib/git/read.js +617 -0
- package/lib/impl.js +2 -2
- package/lib/mirror/pusher.cjs +25 -2
- package/lib/mobile/archive-clean.js +31 -1
- package/lib/mobile/channels.js +583 -0
- package/lib/mobile/events.js +10 -8
- package/lib/mobile/git-repos.js +210 -0
- package/lib/mobile/git.js +603 -0
- package/lib/mobile/history-derive.js +79 -1
- package/lib/mobile/host-bridge/endpoints.js +3 -0
- package/lib/mobile/host-bridge/event-synthesis.js +109 -1
- package/lib/mobile/host-bridge/shims.js +9 -2
- package/lib/mobile/host-bridge/transport.js +30 -1
- package/lib/mobile/host-settings.js +41 -0
- package/lib/mobile/inbox-clean.js +337 -0
- package/lib/mobile/pairing.js +18 -1
- package/lib/mobile/questions.js +31 -9
- package/lib/mobile/schedule.js +440 -0
- package/lib/mobile/session-cache.js +18 -0
- package/lib/mobile/session-file.js +192 -0
- package/lib/mobile/session-model-options.js +184 -0
- package/lib/mobile/session-models.js +24 -2
- package/lib/mobile/session-schedule-mark.js +70 -0
- package/lib/mobile/session-snapshot.js +12 -1
- package/lib/mobile/sessions.js +8 -0
- package/lib/mobile/stats-collect.cjs +257 -0
- package/lib/mobile/stats-ledger.cjs +407 -0
- package/lib/mobile/stats.js +38 -0
- package/lib/mobile/stream.js +18 -10
- package/lib/mobile/uploads.js +24 -6
- package/lib/mobile/workspaces.js +30 -6
- package/lib/panel/index.html +4 -4
- package/lib/relay/lan-proxy.js +10 -1
- package/lib/routes/control-plane.js +38 -0
- package/lib/routes/mobile-api.js +70 -1
- package/lib/session-cleanup.cjs +8 -0
- package/lib/shared/sse-write.js +17 -6
- package/lib/shell/updater.js +3 -3
- package/package.json +3 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 项目目录的 Git 仓库识别:回传「这里有没有仓库、仓库根在哪、当前在哪个分支」。
|
|
5
|
+
*
|
|
6
|
+
* 为什么由插件做:手机看不到电脑上的目录,侧栏那个 Git 入口的可见性只能靠宿主回传的
|
|
7
|
+
* 标记决定(docs/git-manager.md 第 2 节)。判据不许 App 猜,也不许等用户点进去才发现
|
|
8
|
+
* 是空页 —— 字段缺失一律按「不是仓库」处理。
|
|
9
|
+
*
|
|
10
|
+
* 为什么只读文件、不起 git 进程:这段代码跟着工作区清单一轮一轮地跑(镜像推送每
|
|
11
|
+
* 500ms 构建一次清单),起进程太贵;而且宿主上未必装了 git,装没装不该影响「这个项目
|
|
12
|
+
* 是不是仓库」这个事实。仓库的形状就是 `.git` 这个入口加上里面的 HEAD,读到就够答了。
|
|
13
|
+
*
|
|
14
|
+
* 认哪些(三种都算「有仓库」):
|
|
15
|
+
* 1. 项目根自己就是仓库根;
|
|
16
|
+
* 2. 项目目录落在某个仓库的子目录里 —— 往上找最近的那个根,操作按它执行;
|
|
17
|
+
* 3. 项目下面并列着若干个仓库(第 1、2 层,含 `.git` 是文件的子模块与 worktree)。
|
|
18
|
+
* 往上走到主目录为止不再往上:主目录本身是个 dotfiles 仓库时,再往上是把每个项目都
|
|
19
|
+
* 判成仓库,那不是这些项目的意思。往下只扫两层、条数封顶,扫满置 `truncated`。
|
|
20
|
+
*
|
|
21
|
+
* 结果按目录缓存一会儿(GIT_CACHE_MS):下拉刷新带 `fresh` 跳过缓存,见
|
|
22
|
+
* lib/mobile/workspaces.js 的 handleWorkspaces。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { readdir, readFile, stat } = require('node:fs/promises');
|
|
26
|
+
const { homedir } = require('node:os');
|
|
27
|
+
const { basename, dirname, isAbsolute, join, relative, resolve } = require('node:path');
|
|
28
|
+
|
|
29
|
+
/** 识别结果的缓存时长:与侧栏 15 秒轮询同频,镜像推送每 500ms 构建清单时靠它挡住重复扫盘。 */
|
|
30
|
+
const GIT_CACHE_MS = 15 * 1000;
|
|
31
|
+
/** 往下只扫这么多层(项目目录的直接子目录算第 1 层)。 */
|
|
32
|
+
const MAX_DEPTH = 2;
|
|
33
|
+
/** 一次最多回这么多仓库(含主仓库),超出的截断并置 `truncated`。 */
|
|
34
|
+
const MAX_REPOS = 20;
|
|
35
|
+
/** 明显不该往里找仓库的目录:依赖树。点开头的目录由命名规则挡掉。 */
|
|
36
|
+
const SKIP_DIRS = new Set(['node_modules']);
|
|
37
|
+
|
|
38
|
+
/** projectDir → { at, value }。键是解析过的绝对路径,条目数就是工作区条数。 */
|
|
39
|
+
const cache = new Map();
|
|
40
|
+
|
|
41
|
+
function emptyMarker() {
|
|
42
|
+
return { isRepo: false, root: null, branch: null, detached: false, repos: [], truncated: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 读 HEAD:`ref: refs/heads/<名字>` 是正常分支,裸哈希是 detached(此时没有分支名)。
|
|
47
|
+
* 读不到(权限、仓库损坏、子模块还没 init)都当「不知道分支」—— 仓库本身仍然算存在。
|
|
48
|
+
*/
|
|
49
|
+
async function readHead(gitDir) {
|
|
50
|
+
if (!gitDir) return null;
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = await readFile(join(gitDir, 'HEAD'), 'utf8');
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const line = text.trim();
|
|
58
|
+
const ref = /^ref:\s*(\S+)$/.exec(line);
|
|
59
|
+
if (ref) {
|
|
60
|
+
const name = ref[1];
|
|
61
|
+
return { branch: name.startsWith('refs/heads/') ? name.slice('refs/heads/'.length) : name, detached: false };
|
|
62
|
+
}
|
|
63
|
+
if (/^[0-9a-f]{40,64}$/i.test(line)) return { branch: null, detached: true };
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 看一个目录自己是不是仓库根。
|
|
69
|
+
*
|
|
70
|
+
* `.git` 是目录 = 普通仓库;是文件 = 子模块 / worktree / 分离 git 目录,文件里那行
|
|
71
|
+
* `gitdir:` 写着真正的 git 目录在哪,HEAD 去那边读。所以这三种形态不需要各写一套判据,
|
|
72
|
+
* 差别只在 kind 上,给界面决定叫「子模块」还是「仓库」。
|
|
73
|
+
*/
|
|
74
|
+
async function probeRepo(dir) {
|
|
75
|
+
const root = resolve(dir);
|
|
76
|
+
const dotGit = join(root, '.git');
|
|
77
|
+
let entry;
|
|
78
|
+
try {
|
|
79
|
+
entry = await stat(dotGit);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
let gitDir = null;
|
|
84
|
+
let kind = 'repo';
|
|
85
|
+
if (entry.isFile()) {
|
|
86
|
+
let text = '';
|
|
87
|
+
try {
|
|
88
|
+
text = await readFile(dotGit, 'utf8');
|
|
89
|
+
} catch {
|
|
90
|
+
text = '';
|
|
91
|
+
}
|
|
92
|
+
const pointer = /^\s*gitdir:\s*(.+?)\s*$/m.exec(text);
|
|
93
|
+
if (pointer) {
|
|
94
|
+
const target = pointer[1];
|
|
95
|
+
gitDir = isAbsolute(target) ? target : resolve(root, target);
|
|
96
|
+
kind = /(^|[\\/])modules[\\/]/.test(gitDir)
|
|
97
|
+
? 'submodule'
|
|
98
|
+
: /(^|[\\/])worktrees[\\/]/.test(gitDir)
|
|
99
|
+
? 'worktree'
|
|
100
|
+
: 'linked';
|
|
101
|
+
} else {
|
|
102
|
+
kind = 'linked';
|
|
103
|
+
}
|
|
104
|
+
} else if (entry.isDirectory()) {
|
|
105
|
+
gitDir = dotGit;
|
|
106
|
+
} else {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const head = await readHead(gitDir);
|
|
110
|
+
return {
|
|
111
|
+
root,
|
|
112
|
+
name: basename(root),
|
|
113
|
+
kind,
|
|
114
|
+
branch: head ? head.branch : null,
|
|
115
|
+
detached: head ? head.detached : false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 从项目目录往上找最近的仓库根(含项目目录自己),到主目录为止。 */
|
|
120
|
+
async function findAncestorRepo(projectDir) {
|
|
121
|
+
const stopAt = resolve(homedir());
|
|
122
|
+
let current = projectDir;
|
|
123
|
+
for (;;) {
|
|
124
|
+
const repo = await probeRepo(current);
|
|
125
|
+
if (repo) return repo;
|
|
126
|
+
if (current === stopAt) return null;
|
|
127
|
+
const parent = dirname(current);
|
|
128
|
+
if (parent === current) return null;
|
|
129
|
+
current = parent;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 从项目目录往下扫并列的仓库,最多 MAX_DEPTH 层。
|
|
135
|
+
*
|
|
136
|
+
* 不进符号链接目录(绕开环,仓库根几乎都是真目录),点开头的目录与依赖树一律跳过。
|
|
137
|
+
* 找到仓库之后仍然往下走一层:子模块就住在主仓库下面,不进去就看不见它。
|
|
138
|
+
*/
|
|
139
|
+
async function scanNested(projectDir) {
|
|
140
|
+
const found = [];
|
|
141
|
+
let frontier = [projectDir];
|
|
142
|
+
for (let depth = 1; depth <= MAX_DEPTH && frontier.length > 0; depth += 1) {
|
|
143
|
+
const next = [];
|
|
144
|
+
for (const parent of frontier) {
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = await readdir(parent, { withFileTypes: true });
|
|
148
|
+
} catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (found.length > MAX_REPOS) return found;
|
|
154
|
+
if (!entry.isDirectory()) continue;
|
|
155
|
+
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
|
|
156
|
+
const child = join(parent, entry.name);
|
|
157
|
+
const repo = await probeRepo(child);
|
|
158
|
+
if (repo) found.push(repo);
|
|
159
|
+
next.push(child);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
frontier = next;
|
|
163
|
+
}
|
|
164
|
+
return found;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 主仓库排第一(`main: true`),其余按扫描顺序;`relative` 是相对项目目录的写法。 */
|
|
168
|
+
async function recognize(projectDir) {
|
|
169
|
+
const primary = await findAncestorRepo(projectDir);
|
|
170
|
+
const nested = await scanNested(projectDir);
|
|
171
|
+
const raw = primary ? [primary, ...nested] : nested;
|
|
172
|
+
const truncated = raw.length > MAX_REPOS;
|
|
173
|
+
const repos = raw.slice(0, MAX_REPOS).map((repo, index) => ({
|
|
174
|
+
root: repo.root,
|
|
175
|
+
name: repo.name,
|
|
176
|
+
relative: relative(projectDir, repo.root) || '.',
|
|
177
|
+
kind: repo.kind,
|
|
178
|
+
branch: repo.branch,
|
|
179
|
+
detached: repo.detached,
|
|
180
|
+
main: index === 0,
|
|
181
|
+
}));
|
|
182
|
+
const head = repos[0];
|
|
183
|
+
return {
|
|
184
|
+
isRepo: repos.length > 0,
|
|
185
|
+
root: head ? head.root : null,
|
|
186
|
+
branch: head ? head.branch : null,
|
|
187
|
+
detached: head ? head.detached : false,
|
|
188
|
+
repos,
|
|
189
|
+
truncated,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 一个项目目录的仓库标记。永远不抛:目录不存在、没有权限、路径根本不是目录,都回
|
|
195
|
+
* 「不是仓库」,让工作区清单照常下发。
|
|
196
|
+
*
|
|
197
|
+
* @param {unknown} dir 项目目录(宿主 workspace.list 里的 `path`)
|
|
198
|
+
* @param {{ fresh?: boolean }} [options] `fresh` 跳过缓存(下拉刷新 / 写后刷新)
|
|
199
|
+
*/
|
|
200
|
+
async function describeProjectGit(dir, options = {}) {
|
|
201
|
+
if (typeof dir !== 'string' || dir.trim() === '') return emptyMarker();
|
|
202
|
+
const projectDir = resolve(dir.trim());
|
|
203
|
+
const cached = cache.get(projectDir);
|
|
204
|
+
if (cached && !options.fresh && Date.now() - cached.at < GIT_CACHE_MS) return cached.value;
|
|
205
|
+
const value = await recognize(projectDir);
|
|
206
|
+
cache.set(projectDir, { at: Date.now(), value });
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { describeProjectGit, GIT_CACHE_MS, MAX_DEPTH, MAX_REPOS };
|