@feng0531/dsh-vcs 0.3.12
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/LICENSE +42 -0
- package/README.md +69 -0
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/cordis.patch.yml +5 -0
- package/dist/MONACO-LICENSE.txt +21 -0
- package/dist/MONACO-ThirdPartyNotices.txt +448 -0
- package/dist/client.js +73 -0
- package/dist/editor.css +1 -0
- package/dist/editor.js +698 -0
- package/dist/editor.worker.js +15 -0
- package/docs/RELEASING.md +65 -0
- package/icon.svg +1 -0
- package/index.mjs +160 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +81 -0
- package/vcs.mjs +327 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 发布清单
|
|
2
|
+
|
|
3
|
+
当前包名 @feng0531/dsh-vcs;正式发布统一走 GitHub Actions,不在本地 npm publish。公开下载不改变非商业许可证。
|
|
4
|
+
|
|
5
|
+
## GitHub + npm 联合发布
|
|
6
|
+
|
|
7
|
+
在 GitHub 仓库 Settings → Secrets and variables → Actions 添加 NPM_AUTOMATION_TOKEN。该令牌必须有 @feng0531/dsh-vcs 发布权限,并满足 npm 的 2FA 策略(需要时启用 bypass 2FA);名字叫 AUTOMATION_TOKEN 不会自动赋予权限。不要在聊天、源码或日志中公开值。
|
|
8
|
+
|
|
9
|
+
对应标签 CI 全绿及人工验收后,从 main 手动运行 Publish GitHub and npm 工作流,填写已有版本标签。构建、验证和打包步骤不注入 npm 凭据;仅 npm 发布步骤使用 NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }},且禁用生命周期脚本。同一 tgz 先发布 npm,再作为 GitHub 正式 Release 附件上传并附 SHA-256。
|
|
10
|
+
|
|
11
|
+
两平台不支持原子事务:npm 失败则不发布 GitHub;npm 成功而 GitHub 失败时,重跑仅在已发布 npm integrity 与当前包完全一致时继续,否则拒绝覆盖。GitHub 已存在 Release 时也拒绝覆盖,需先检查现场。新版本不移动旧标签。
|
|
12
|
+
|
|
13
|
+
## 1. 公开发布前的决定
|
|
14
|
+
|
|
15
|
+
- [x] 仓库地址已确认:https://github.com/f-e-n-g-0531/dsh-vcs 。
|
|
16
|
+
- [x] 正式采用 [非商业源码公开许可证 1.0](<../LICENSE>),禁止商用,允许非商业修改和分发。
|
|
17
|
+
- [ ] 每次发布检查源码权属及第三方声明;商业另行授权须覆盖全部有关权利人。
|
|
18
|
+
- [ ] 确认安全报告渠道、维护责任与支持范围。
|
|
19
|
+
- [ ] 审查待公开文件和 Git 历史,排除个人路径、令牌、用户数据、私人记录和敏感图片。
|
|
20
|
+
|
|
21
|
+
## 2. 干净环境验证
|
|
22
|
+
|
|
23
|
+
使用 Node.js 22 或更新版本,在干净检出目录中执行:
|
|
24
|
+
|
|
25
|
+
npm ci && npm run check
|
|
26
|
+
npm run verify:package
|
|
27
|
+
npm run test:install
|
|
28
|
+
npm pack --dry-run
|
|
29
|
+
npm pack
|
|
30
|
+
|
|
31
|
+
确认锁文件安装、构建和测试成功,记录失败与跳过项。完整 Git / SVN 测试需要 git、svn、svnadmin。npm pack 的 prepack 会执行 check;审查包内容是否包含客户端、编辑器、CSS、worker、宿主入口、适配器、语言文件与插件 patch。
|
|
32
|
+
|
|
33
|
+
构建会复制 Monaco 的许可证与第三方声明进入分发资源;仍应核查最终包中的声明及其他依赖义务。排除测试数据、依赖目录与私人材料。
|
|
34
|
+
|
|
35
|
+
Git 来源安装依赖 prepare 构建,需要另行验证生命周期和构建依赖。pnpm 可能需要 allowBuilds 审批;拒绝脚本的环境应优先使用已构建 tgz,不应直接关闭安全策略。
|
|
36
|
+
|
|
37
|
+
## 3. 安装与真实页面验收
|
|
38
|
+
|
|
39
|
+
dsh plugin --profile web add <absolute-tgz>
|
|
40
|
+
|
|
41
|
+
替换为包的绝对路径,必要时加引号。先确认目标 profile 和宿主兼容性;安装改变运行配置,不是无副作用检查。
|
|
42
|
+
|
|
43
|
+
**宿主重启只能由用户执行。** 用户重启后刷新现有 DSH Web 地址,在真实 Session 中验证:
|
|
44
|
+
|
|
45
|
+
- [ ] 插件入口可见;顶部单仓库下拉,无额外仓库左栏。
|
|
46
|
+
- [ ] 当前 / 上级仓库、一层子目录发现,以及明确相对子目录的定向发现。
|
|
47
|
+
- [ ] Git 三模式、SVN BASE 与属性;新增、删除、重命名、未跟踪和冲突提示。
|
|
48
|
+
- [ ] UTF-8、GBK、带 BOM 的 UTF-16;二进制与超大文件提示。
|
|
49
|
+
- [ ] 并排 / 行内、空白选项、换行、导航、主题和编辑器加载失败降级。
|
|
50
|
+
- [ ] Session / 仓库切换与重新发现不展示过期内容;预览不修改工作区或暂存区。
|
|
51
|
+
- [ ] 发现授权过期后的手动重新发现,以及取消请求的实际反馈。
|
|
52
|
+
|
|
53
|
+
记录宿主和浏览器版本、步骤及已知限制。自动化测试通过不等于这些项目已完成;未执行时明确写“未验收”,不补写成功结论。
|
|
54
|
+
|
|
55
|
+
## 4. 标签 CI 与发布工作流
|
|
56
|
+
|
|
57
|
+
普通分支 push、PR 和日常脚本修改不触发 CI;只有维护者主动推送版本标签(`vX.Y.Z`)才触发 Windows/Linux 测试矩阵。完成本地检查后,确认包版本并执行 `git tag vX.Y.Z`、`git push origin vX.Y.Z`(替换成真实版本,标签须指向包含新 CI 规则的提交)。不要移动已经发布的标签。等待该标签的 CI 全部通过并完成真实页面验收后再进行发布。在 Actions 手动运行 Release workflow 并填写已有 tag:工作流验证版本、重新构建与测试、检查包并生成 SHA256SUMS.txt,随后先发布 npm 再公开 GitHub Release。手动触发即授权两平台正式发布。未通过的真实页面验收不得勾选完成。
|
|
58
|
+
|
|
59
|
+
CI 会严格校验标签为 `vX.Y.Z` 且与包版本相同。联合发布会检查同一标签、同一提交的最新 CI 必须成功;未运行、失败或尚在运行时拒绝创建草稿。这不替代真实 DSH 页面人工验收。
|
|
60
|
+
|
|
61
|
+
## 5. 分发
|
|
62
|
+
|
|
63
|
+
确认完整附带许可证及第三方声明,不把本项目标记为标准开源软件。在已确认的仓库中按实际验证结果编写发布说明,附审核过的 tgz 和校验值,注明兼容环境与未验证事项。是否打 tag / 创建 GitHub Release 由维护者决定;不为本次整理虚构日期、历史版本或功能变更。
|
|
64
|
+
|
|
65
|
+
相关约定见 [README](<../README.md>)、[贡献指南](<../CONTRIBUTING.md>)、[变更记录](<../CHANGELOG.md>) 和 [安全说明](<../SECURITY.md>)。
|
package/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect x="7" y="8" width="50" height="48" rx="10" fill="#4176e6"/><path d="M32 14v36M15 25h10m-5-5v10m20 9h10" stroke="white" stroke-width="4" stroke-linecap="round"/></svg>
|
package/index.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import * as adapter from './vcs.mjs';
|
|
3
|
+
|
|
4
|
+
export const name = 'dsh-vcs';
|
|
5
|
+
export const inject = ['connection', 'webServer', 'sessions', 'sessionPersistence'];
|
|
6
|
+
export const RPC_CHANNEL = '/vcs-rpc';
|
|
7
|
+
const ASSETS = ['editor.js', 'editor.css', 'editor.worker.js'];
|
|
8
|
+
const failure = (code, message) => ({ ok: false, error: { code, message, details: {} } });
|
|
9
|
+
const invalid = (message) => Object.assign(new Error(message), { code: 'vcs/invalid-request' });
|
|
10
|
+
|
|
11
|
+
/** Authoritative Session lookup: never accept a browser-provided directory. */
|
|
12
|
+
export async function resolveSessionCwd(ctx, sessionId, signal) {
|
|
13
|
+
if (typeof sessionId !== 'string' || !sessionId || sessionId.length > 256 || /[\/\\\0]/.test(sessionId)) {
|
|
14
|
+
throw invalid('A valid sessionId is required.');
|
|
15
|
+
}
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
const live = ctx.sessions.get(sessionId);
|
|
18
|
+
const header = live?.header ?? (await ctx.sessionPersistence.stat(sessionId, { signal }))?.header;
|
|
19
|
+
if (!header?.cwd) throw Object.assign(new Error('The Session has no available working directory.'), { code: 'vcs/session-unavailable' });
|
|
20
|
+
return header.cwd;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rediscover = () => Object.assign(new Error('Repository authorization is missing, expired, or changed. Rediscover repositories for this Session and retry.'), { code: 'vcs/rediscover-required' });
|
|
24
|
+
|
|
25
|
+
function validatePayload(endpoint, payload) {
|
|
26
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw invalid('An object payload is required.');
|
|
27
|
+
const fields = endpoint === 'vcs/repositories' ? ['sessionId', 'subdirectory'] : ['sessionId', 'repositoryId', 'mode', ...(endpoint === 'vcs/compare' ? ['id'] : [])];
|
|
28
|
+
if (Object.keys(payload).some(key => !fields.includes(key))) throw invalid('Only Session-addressed repository requests are supported; unknown payload field.');
|
|
29
|
+
if (endpoint === 'vcs/repositories') {
|
|
30
|
+
if (payload.subdirectory !== undefined) {
|
|
31
|
+
const dir = payload.subdirectory;
|
|
32
|
+
if (typeof dir !== 'string' || !dir || dir.length > 4096 || /[\0:]/.test(dir) || /^[\/\\]/.test(dir) || dir.split(/[\/\\]/).includes('..')) throw invalid('subdirectory must be a relative path within the Session directory.');
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
if (typeof payload.repositoryId !== 'string' || !payload.repositoryId || payload.repositoryId.length > 1024) throw invalid('A discovered repositoryId is required.');
|
|
36
|
+
if (!['all', 'unstaged', 'staged'].includes(payload.mode ?? 'all')) throw invalid('Unsupported comparison mode.');
|
|
37
|
+
if (endpoint === 'vcs/compare' && (typeof payload.id !== 'string' || !payload.id || payload.id.length > 32768)) throw invalid('A change id is required.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Limit active adapter operations and retain only bounded, short-lived discovery grants. */
|
|
42
|
+
export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.now, ttlMs = 5 * 60_000, maxSessions = 32, maxRepositories = 512 } = {}) {
|
|
43
|
+
let active = 0;
|
|
44
|
+
const sessions = new Map();
|
|
45
|
+
function current(sessionId, cwd) {
|
|
46
|
+
for (const [id, entry] of sessions) if (entry.expiresAt <= now()) sessions.delete(id);
|
|
47
|
+
const entry = sessions.get(sessionId);
|
|
48
|
+
if (entry && entry.cwd !== cwd) { sessions.delete(sessionId); return undefined; }
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
async function assertCurrent(sessionId, cwd, entry, signal) {
|
|
52
|
+
const latest = await resolveSessionCwd(ctx, sessionId, signal);
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
if (current(sessionId, latest) !== entry || latest !== cwd) throw rediscover();
|
|
55
|
+
}
|
|
56
|
+
return async (endpoint, payload, signal) => {
|
|
57
|
+
if (!['vcs/repositories', 'vcs/status', 'vcs/compare'].includes(endpoint)) return failure('vcs/not-found', 'Unknown VCS endpoint.');
|
|
58
|
+
if (active >= maxActive) return failure('vcs/busy', 'Too many VCS requests. Please retry.');
|
|
59
|
+
active++;
|
|
60
|
+
try {
|
|
61
|
+
signal?.throwIfAborted();
|
|
62
|
+
validatePayload(endpoint, payload);
|
|
63
|
+
const cwd = await resolveSessionCwd(ctx, payload.sessionId, signal);
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
let entry = current(payload.sessionId, cwd);
|
|
66
|
+
if (endpoint === 'vcs/repositories') {
|
|
67
|
+
if (!entry) {
|
|
68
|
+
while (sessions.size >= maxSessions) sessions.delete(sessions.keys().next().value);
|
|
69
|
+
entry = { cwd, repositories: new Map(), expiresAt: now() + ttlMs, scanSequence: 0, fullScanSequence: 0 };
|
|
70
|
+
sessions.set(payload.sessionId, entry);
|
|
71
|
+
}
|
|
72
|
+
const targeted = payload.subdirectory !== undefined;
|
|
73
|
+
const sequence = ++entry.scanSequence;
|
|
74
|
+
if (!targeted) entry.fullScanSequence = sequence;
|
|
75
|
+
const fullSequence = entry.fullScanSequence;
|
|
76
|
+
const result = await api.discoverRepositories(cwd, { signal, subdirectory: payload.subdirectory, maxDepth: 1, shallow: true });
|
|
77
|
+
// A stale scan must never recreate/replace a grant invalidated by a cwd change or eviction.
|
|
78
|
+
await assertCurrent(payload.sessionId, cwd, entry, signal);
|
|
79
|
+
// Full scans replace grants. Never let an older full scan erase a newer scan,
|
|
80
|
+
// or an older targeted scan resurrect grants after a new full scan begins.
|
|
81
|
+
if ((!targeted && sequence !== entry.scanSequence) || (targeted && fullSequence !== entry.fullScanSequence)) throw rediscover();
|
|
82
|
+
const grants = targeted ? new Map(entry.repositories) : new Map();
|
|
83
|
+
let truncated = !!result.truncated;
|
|
84
|
+
const warnings = [...result.warnings];
|
|
85
|
+
const repositories = [];
|
|
86
|
+
for (const repository of result.repositories) {
|
|
87
|
+
if (!grants.has(repository.id) && grants.size >= maxRepositories) { truncated = true; continue; }
|
|
88
|
+
grants.set(repository.id, { ...repository });
|
|
89
|
+
repositories.push({ ...repository });
|
|
90
|
+
}
|
|
91
|
+
if (repositories.length !== result.repositories.length) warnings.push('Repository authorization limit reached; use a new Session or narrow its working directory.');
|
|
92
|
+
entry.repositories = grants;
|
|
93
|
+
entry.expiresAt = now() + ttlMs;
|
|
94
|
+
return { ok: true, value: { cwd, repositories, truncated, warnings } };
|
|
95
|
+
}
|
|
96
|
+
const repository = entry?.repositories.get(payload.repositoryId);
|
|
97
|
+
if (!repository) throw rediscover();
|
|
98
|
+
const mode = payload.mode ?? 'all';
|
|
99
|
+
if (repository.type === 'svn' && mode !== 'all') throw invalid('SVN supports only all mode.');
|
|
100
|
+
// Both adapter operations revalidate the canonical root; comparison also validates
|
|
101
|
+
// the change ID against fresh status, so do not duplicate a full status scan here.
|
|
102
|
+
const value = endpoint === 'vcs/status'
|
|
103
|
+
? { cwd, repository: { ...repository }, changes: await api.listChanges({ ...repository }, mode), mode }
|
|
104
|
+
: await api.getComparison({ ...repository }, { mode, id: payload.id });
|
|
105
|
+
await assertCurrent(payload.sessionId, cwd, entry, signal);
|
|
106
|
+
if (entry.repositories.get(payload.repositoryId) !== repository) throw rediscover();
|
|
107
|
+
return { ok: true, value };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (signal?.aborted || error?.name === 'AbortError') return failure('vcs/cancelled', 'The request was cancelled.');
|
|
110
|
+
return failure(typeof error?.code === 'string' && error.code.startsWith('vcs/') ? error.code : 'vcs/operation-failed', error instanceof Error ? error.message : String(error));
|
|
111
|
+
} finally { active--; }
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Three fixed authenticated assets; URL input never enters a filesystem path. */
|
|
116
|
+
export function registerAssets(ctx, load = filename => readFile(new URL('./dist/' + filename, import.meta.url))) {
|
|
117
|
+
for (const filename of ASSETS) {
|
|
118
|
+
ctx.connection.fetch.register({
|
|
119
|
+
path: '/api/vcs-assets/' + filename,
|
|
120
|
+
methods: ['GET', 'HEAD'],
|
|
121
|
+
requestBody: 'buffered',
|
|
122
|
+
async fetch(request) {
|
|
123
|
+
try {
|
|
124
|
+
const bytes = await load(filename);
|
|
125
|
+
return new Response(request.method === 'HEAD' ? null : bytes, { headers: {
|
|
126
|
+
'Content-Type': filename.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8',
|
|
127
|
+
'Content-Length': String(bytes.byteLength),
|
|
128
|
+
'Cache-Control': 'private, no-cache',
|
|
129
|
+
'X-Content-Type-Options': 'nosniff',
|
|
130
|
+
} });
|
|
131
|
+
} catch { return new Response('VCS editor assets unavailable. Build the plugin first.', { status: 503 }); }
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Dedicated static routes avoid dependence on the shared /api dispatcher. */
|
|
138
|
+
export function registerDirectAssets(ctx, load = filename => readFile(new URL('./dist/' + filename, import.meta.url))) {
|
|
139
|
+
for (const filename of ASSETS) ctx.effect(() => ctx.webServer.register({
|
|
140
|
+
kind: 'exact', path: '/vcs-assets/' + filename,
|
|
141
|
+
async handler(req, res) {
|
|
142
|
+
const admission = ctx.connection.admit(req);
|
|
143
|
+
if ('rejection' in admission) { res.writeHead(admission.rejection); res.end(admission.rejection === 401 ? 'unauthorized' : 'forbidden'); return; }
|
|
144
|
+
if (!['GET', 'HEAD'].includes(req.method)) { res.writeHead(405, {Allow:'GET, HEAD'}); res.end(); return; }
|
|
145
|
+
try {
|
|
146
|
+
const bytes = await load(filename);
|
|
147
|
+
res.writeHead(200, {'Content-Type': filename.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8', 'Content-Length': String(bytes.byteLength), 'Cache-Control':'private, no-cache', 'X-Content-Type-Options':'nosniff'});
|
|
148
|
+
res.end(req.method === 'HEAD' ? undefined : bytes);
|
|
149
|
+
} catch { res.writeHead(503, {'Content-Type':'text/plain; charset=utf-8'}); res.end('VCS editor assets unavailable.'); }
|
|
150
|
+
}
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function apply(ctx) {
|
|
155
|
+
// Pass the plugin owner explicitly: the nested rpc getter can retain the
|
|
156
|
+
// connection service context, which does not inject webServer.
|
|
157
|
+
ctx.connection.register(ctx, RPC_CHANNEL, createHandler(ctx));
|
|
158
|
+
registerAssets(ctx);
|
|
159
|
+
registerDirectAssets(ctx);
|
|
160
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feng0531/dsh-vcs",
|
|
3
|
+
"version": "0.3.12",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
6
|
+
"type": "module",
|
|
7
|
+
"description": "Read-only Git and SVN change preview for DSH",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.mjs",
|
|
10
|
+
"./client": "./dist/client.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./locale/*.json": "./locale/*.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.mjs",
|
|
16
|
+
"vcs.mjs",
|
|
17
|
+
"dist",
|
|
18
|
+
"locale",
|
|
19
|
+
"icon.svg",
|
|
20
|
+
"cordis.patch.yml",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"THIRD_PARTY_NOTICES.md",
|
|
23
|
+
"docs/RELEASING.md"
|
|
24
|
+
],
|
|
25
|
+
"icon": "./icon.svg",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node build.mjs",
|
|
28
|
+
"test": "node --test test/*.test.mjs",
|
|
29
|
+
"check": "npm run build && npm test",
|
|
30
|
+
"prepare": "npm run build",
|
|
31
|
+
"prepack": "npm run check",
|
|
32
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
33
|
+
"test:install": "node scripts/test-install.mjs"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@deepseek-ai/dsh": "0.1.7-rc.2"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@deepseek-ai/dsh": { "optional": true }
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"fast-xml-parser": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"esbuild": "^0.25.0",
|
|
46
|
+
"monaco-editor": "^0.52.2",
|
|
47
|
+
"react": "^19.2.0",
|
|
48
|
+
"react-dom": "^19.2.0"
|
|
49
|
+
},
|
|
50
|
+
"dsh": {
|
|
51
|
+
"bundle": {
|
|
52
|
+
"patch": "./cordis.patch.yml"
|
|
53
|
+
},
|
|
54
|
+
"client": {
|
|
55
|
+
"platform": "web",
|
|
56
|
+
"immediately": true,
|
|
57
|
+
"inject": [
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=22.0.0"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/f-e-n-g-0531/dsh-vcs.git"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/f-e-n-g-0531/dsh-vcs#readme",
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/f-e-n-g-0531/dsh-vcs/issues"
|
|
72
|
+
},
|
|
73
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
74
|
+
"keywords": [
|
|
75
|
+
"deepseek-harness",
|
|
76
|
+
"dsh-plugin",
|
|
77
|
+
"git",
|
|
78
|
+
"svn",
|
|
79
|
+
"diff"
|
|
80
|
+
]
|
|
81
|
+
}
|