@dshfly/remote-connector 0.2.1 → 0.3.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/core/connector-core.js +70 -2
- package/core/deps-check-cli.js +33 -0
- package/core/deps-check.js +103 -0
- package/core/dsh-web.js +15 -1
- package/core/history-slim.js +59 -0
- package/core/tunnel.js +42 -13
- package/core/version.js +63 -0
- package/dist/client.js +31 -2
- package/dist/client.js.map +2 -2
- package/http-api.js +75 -3
- package/index.js +5 -1
- package/mobile-bridge/core/bridge-core.js +7 -5
- package/mobile-bridge/core/files.js +22 -1
- package/mobile-bridge/core/git-status.js +121 -0
- package/mobile-bridge/index.js +1 -0
- package/package.json +5 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// core/git-status.js —— Git 工作区状态探测(mobile.files.list 的 gitStatus 来源)。
|
|
2
|
+
//
|
|
3
|
+
// 设计(docs/file-tree-plan.md 的边界不后退):
|
|
4
|
+
// - **PC 侧计算**:手机是薄终端,git 在 PC 上跑;本模块零依赖、纯 Node、可单测。
|
|
5
|
+
// - **只读**:只用 `git status --porcelain`(绝不 add/commit/checkout 等会 mutate 的命令)。
|
|
6
|
+
// - **只算当前浏览目录**:`git -C repo status --porcelain=v1 -z -uall -- <relDir>` 路径限定,
|
|
7
|
+
// 只拿"当前目录子树"的变更,避免扫整个大仓库;映射回该层条目的绝对路径。
|
|
8
|
+
// - **优雅降级**:非 git 仓库 / git 未安装 / git 失败 → 返回空表(手机端不显示任何点)——
|
|
9
|
+
// 绝不让 git 探测失败影响 files.list 主链路。
|
|
10
|
+
// - **安全**:仅给"当前层已出现在结果里的条目"注记,不额外暴露路径;错误不携带绝对路径。
|
|
11
|
+
//
|
|
12
|
+
// 分类(供 UI 渲染色点):modified / added / renamed / copied / untracked / deleted / conflict。
|
|
13
|
+
|
|
14
|
+
import { execFile } from 'node:child_process';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
const sepRe = new RegExp('\\' + path.sep, 'g'); // OS 分隔符 → '/' 的替换正则
|
|
18
|
+
const pathRelative = (from, to) => path.relative(from, to);
|
|
19
|
+
const joinPaths = (...ps) => path.join(...ps);
|
|
20
|
+
|
|
21
|
+
/** 默认 git 执行器:execFile 超时保护 + 绝不经 shell。 */
|
|
22
|
+
function defaultRunGit(args, { cwd }) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
execFile('git', args, { cwd, timeout: 8000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
25
|
+
if (err) reject(err);
|
|
26
|
+
else resolve({ stdout: String(stdout || '') });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** porcelain v1 双状态字符 → 分类(null=无需显示)。顺序即优先级。 */
|
|
32
|
+
export function categorizeStatus(x, y) {
|
|
33
|
+
if (x === '?' && y === '?') return 'untracked';
|
|
34
|
+
if (x === 'U' || y === 'U') return 'conflict';
|
|
35
|
+
if (x === 'M' || y === 'M') return 'modified';
|
|
36
|
+
if (x === 'D' || y === 'D') return 'deleted';
|
|
37
|
+
if (x === 'A' || x === 'R' || x === 'C') return 'added';
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 解析 `git status --porcelain=v1 -z` 输出(NUL 分隔)→ Map<repo相对path, 分类>。
|
|
43
|
+
* 路径用正斜杠(git 输出即如此);重命名/复制有额外 old-path 字段需跳过。
|
|
44
|
+
*/
|
|
45
|
+
export function parsePorcelainZ(stdout) {
|
|
46
|
+
const map = new Map();
|
|
47
|
+
const parts = String(stdout || '').split('\0');
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < parts.length) {
|
|
50
|
+
const line = parts[i];
|
|
51
|
+
i += 1;
|
|
52
|
+
if (!line) continue;
|
|
53
|
+
const x = line[0] || '';
|
|
54
|
+
const y = line[1] || '';
|
|
55
|
+
// "XY path":XY 后必有空格;path 为 repo 相对路径(/ 分隔)
|
|
56
|
+
const rel = line.length > 3 ? line.slice(3) : '';
|
|
57
|
+
const cat = categorizeStatus(x, y);
|
|
58
|
+
if (cat && rel) map.set(rel, cat);
|
|
59
|
+
// 重命名/复制的第二段是 old-path,消费掉(不入表)
|
|
60
|
+
if (x === 'R' || x === 'C') i += 1;
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* GitStatusService:给出某目录的"变更文件→分类"映射(绝对路径键)。
|
|
67
|
+
* 缓存 repoRoot+relDir 的判定结果,TTL 内免重复 spawn git。
|
|
68
|
+
*/
|
|
69
|
+
export class GitStatusService {
|
|
70
|
+
/**
|
|
71
|
+
* @param {{runGit?: (args: string[], opts: {cwd: string}) => Promise<{stdout: string}>, ttlMs?: number}} opts
|
|
72
|
+
* runGit:可注入(单测/CI 用假 git);缺省 execFile 真跑。ttlMs:缓存时长(默认 4000)。
|
|
73
|
+
*/
|
|
74
|
+
constructor({ runGit = defaultRunGit, ttlMs = 4000 } = {}) {
|
|
75
|
+
this._runGit = runGit;
|
|
76
|
+
this._ttlMs = ttlMs;
|
|
77
|
+
this._cache = new Map(); // key: absDir -> {at, map}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 清缓存(下拉刷新/外部变更时调用)。 */
|
|
81
|
+
invalidate() {
|
|
82
|
+
this._cache.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 目录相对其 repo 根的路径(正斜杠;repo 根自身 → '.')。 */
|
|
86
|
+
_relDir(repoRoot, absDir) {
|
|
87
|
+
const rel = pathRelative(repoRoot, absDir);
|
|
88
|
+
return rel === '' || rel === '.' ? '.' : rel.split(sepRe).join('/');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 返回 absDir 下"文件级"变更映射(绝对路径 → 分类)。非 git 仓库 / git 失败 → 空表。
|
|
93
|
+
* @param {string} absDir 已被 files 服务校验过、位于授权根内的绝对目录。
|
|
94
|
+
*/
|
|
95
|
+
async statusForDir(absDir) {
|
|
96
|
+
const cached = this._cache.get(absDir);
|
|
97
|
+
if (cached && Date.now() - cached.at < this._ttlMs) return cached.map;
|
|
98
|
+
let map = new Map();
|
|
99
|
+
try {
|
|
100
|
+
const repo = await this._runGit(['-C', absDir, 'rev-parse', '--show-toplevel'], { cwd: absDir });
|
|
101
|
+
const repoRoot = String(repo.stdout || '').trim();
|
|
102
|
+
if (!repoRoot) return map;
|
|
103
|
+
const rel = this._relDir(repoRoot, absDir);
|
|
104
|
+
const out = await this._runGit(
|
|
105
|
+
['-C', repoRoot, '-c', 'core.quotepath=false', 'status', '--porcelain=v1', '-z', '-uall', '--', rel],
|
|
106
|
+
{ cwd: repoRoot },
|
|
107
|
+
);
|
|
108
|
+
const relMap = parsePorcelainZ(out.stdout);
|
|
109
|
+
// 相对路径 → 当前目录条目的绝对路径(仅保留本目录下、文件级的变更)
|
|
110
|
+
for (const [relp, cat] of relMap) {
|
|
111
|
+
const abs = joinPaths(repoRoot, relp);
|
|
112
|
+
map.set(abs, cat);
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// 非 git 仓库 / git 缺失 / 超时:静默返回空表(手机端不显示点)
|
|
116
|
+
map = new Map();
|
|
117
|
+
}
|
|
118
|
+
this._cache.set(absDir, { at: Date.now(), map });
|
|
119
|
+
return map;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/mobile-bridge/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import os from 'node:os';
|
|
|
15
15
|
export { MobileBridgeCore, MobileBridgeError } from './core/bridge-core.js';
|
|
16
16
|
export { readProfileBundles, resolveBundleDir } from './core/enumerate.js';
|
|
17
17
|
export { FilesService, FilesError } from './core/files.js';
|
|
18
|
+
export { GitStatusService } from './core/git-status.js';
|
|
18
19
|
export { createLoopbackRootsResolver } from './core/roots.js';
|
|
19
20
|
|
|
20
21
|
/** 把 core 包装成服务对象(connector apply 里 ctx.provide('mobileBridge', ...) 用;也便于测试)。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshfly/remote-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH Fly remote connector as a DeepSeek Harness (DSH) cordis plugin — outbound relay tunnel + E2EE endpoint + loopback proxy into dsh web",
|
|
6
6
|
"keywords": [
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
34
34
|
"qrcode": "^1.5.4",
|
|
35
35
|
"ws": "^8.21.3",
|
|
36
|
+
"@dshfly/crypto": "0.1.0",
|
|
36
37
|
"@dshfly/tunnel-protocol": "0.1.0",
|
|
37
|
-
"@dshfly/mobile-plugin-schema": "0.1.0"
|
|
38
|
-
"@dshfly/crypto": "0.1.0"
|
|
38
|
+
"@dshfly/mobile-plugin-schema": "0.1.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
@@ -80,8 +80,9 @@
|
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
"scripts": {
|
|
83
|
+
"check:deps": "node core/deps-check-cli.js",
|
|
83
84
|
"build:client": "node scripts/build-client.mjs",
|
|
84
85
|
"test": "node --test test/*.test.js test/mobile-bridge/test/*.test.js",
|
|
85
|
-
"build": "node scripts/build-client.mjs"
|
|
86
|
+
"build": "node core/deps-check-cli.js && node scripts/build-client.mjs"
|
|
86
87
|
}
|
|
87
88
|
}
|