@netcatty/dsh-guard 0.0.0-stage → 0.2.1
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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +595 -2
- package/cordis.patch.yml +3 -0
- package/lib/audit-log.js +64 -0
- package/lib/client.js +679 -0
- package/lib/diff-view.js +155 -0
- package/lib/index.js +1958 -0
- package/lib/jsonc.js +140 -0
- package/lib/mask.js +22 -0
- package/lib/match.js +119 -0
- package/lib/params.js +105 -0
- package/lib/paths.js +140 -0
- package/lib/policy-edit.js +74 -0
- package/lib/presets.js +50 -0
- package/lib/preview.js +152 -0
- package/lib/shell.js +127 -0
- package/lib/snapshots.js +198 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +72 -4
- package/policy.json +289 -0
- package/policy.schema.json +317 -0
package/lib/diff-view.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// A side-by-side diff page for one approval.
|
|
2
|
+
//
|
|
3
|
+
// The approval card itself cannot render a custom component: `detail` is
|
|
4
|
+
// Markdown, and the question item has no attachment slot. So the card carries a
|
|
5
|
+
// link to this page, which is ordinary HTML/CSS and can therefore show a real
|
|
6
|
+
// two-column comparison instead of a text approximation.
|
|
7
|
+
//
|
|
8
|
+
// Entries are ephemeral: a bounded in-memory map, expired on read. Nothing is
|
|
9
|
+
// written to disk, and the page never exposes a path that was not already
|
|
10
|
+
// shown in the approval card.
|
|
11
|
+
|
|
12
|
+
const MAX_ENTRIES = 24;
|
|
13
|
+
const TTL_MS = 10 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
/** @type {Map<string, {path: string, before: string, after: string, at: number, rows: Array<{left: string|null, right: string|null, kind: string}>}>} */
|
|
16
|
+
const entries = new Map();
|
|
17
|
+
|
|
18
|
+
function escapeHtml(text) {
|
|
19
|
+
return String(text)
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Line-level pairing: common prefix and suffix are aligned unchanged, and the
|
|
28
|
+
* middle is paired positionally. It is not a full LCS, but for a file edit it
|
|
29
|
+
* puts the old and new version of each changed line next to each other.
|
|
30
|
+
*/
|
|
31
|
+
export function pairLines(before, after) {
|
|
32
|
+
const a = before.split('\n');
|
|
33
|
+
const b = after.split('\n');
|
|
34
|
+
let head = 0;
|
|
35
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head += 1;
|
|
36
|
+
let tailA = a.length - 1;
|
|
37
|
+
let tailB = b.length - 1;
|
|
38
|
+
while (tailA >= head && tailB >= head && a[tailA] === b[tailB]) {
|
|
39
|
+
tailA -= 1;
|
|
40
|
+
tailB -= 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rows = [];
|
|
44
|
+
for (let index = 0; index < head; index += 1) rows.push({ left: a[index], right: b[index], kind: 'same' });
|
|
45
|
+
const middleA = a.slice(head, tailA + 1);
|
|
46
|
+
const middleB = b.slice(head, tailB + 1);
|
|
47
|
+
const middle = Math.max(middleA.length, middleB.length);
|
|
48
|
+
for (let index = 0; index < middle; index += 1) {
|
|
49
|
+
const left = index < middleA.length ? middleA[index] : null;
|
|
50
|
+
const right = index < middleB.length ? middleB[index] : null;
|
|
51
|
+
const kind = left === right ? 'same' : left === null ? 'add' : right === null ? 'del' : 'change';
|
|
52
|
+
rows.push({ left, right, kind });
|
|
53
|
+
}
|
|
54
|
+
for (let index = 0; index < a.length - 1 - tailA; index += 1) {
|
|
55
|
+
const left = a[tailA + 1 + index];
|
|
56
|
+
const right = b[tailB + 1 + index];
|
|
57
|
+
rows.push({ left, right, kind: left === right ? 'same' : 'change' });
|
|
58
|
+
}
|
|
59
|
+
return rows;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Store one preview and return the id used in the approval link. */
|
|
63
|
+
export function remember(before, after, path) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
for (const [key, value] of entries) {
|
|
66
|
+
if (now - value.at > TTL_MS) entries.delete(key);
|
|
67
|
+
}
|
|
68
|
+
while (entries.size >= MAX_ENTRIES) {
|
|
69
|
+
const oldest = entries.keys().next().value;
|
|
70
|
+
entries.delete(oldest);
|
|
71
|
+
}
|
|
72
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
73
|
+
entries.set(id, { path, before, after, at: now, rows: pairLines(before, after) });
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function lookup(id) {
|
|
78
|
+
const entry = entries.get(id);
|
|
79
|
+
if (entry === undefined) return undefined;
|
|
80
|
+
if (Date.now() - entry.at > TTL_MS) {
|
|
81
|
+
entries.delete(id);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Full HTML page for one entry. */
|
|
88
|
+
export function renderPage(id, entry, options = {}) {
|
|
89
|
+
const embed = options.embed === true;
|
|
90
|
+
const rows = entry.rows.slice(0, 400);
|
|
91
|
+
const changed = entry.rows.filter((row) => row.kind !== 'same').length;
|
|
92
|
+
const body = rows.map((row) => {
|
|
93
|
+
const cls = row.kind;
|
|
94
|
+
const left = row.left === null ? '' : escapeHtml(row.left);
|
|
95
|
+
const right = row.right === null ? '' : escapeHtml(row.right);
|
|
96
|
+
return `<tr class="${cls}"><td class="num"></td><td class="left">${left}</td><td class="right">${right}</td></tr>`;
|
|
97
|
+
}).join('\n');
|
|
98
|
+
|
|
99
|
+
let number = 0;
|
|
100
|
+
const numbered = body.replace(/<td class="num"><\/td>/g, () => {
|
|
101
|
+
number += 1;
|
|
102
|
+
return `<td class="num">${number}</td>`;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return `<!doctype html>
|
|
106
|
+
<html lang="zh-CN">
|
|
107
|
+
<head>
|
|
108
|
+
<meta charset="utf-8">
|
|
109
|
+
<title>改动对比 · ${escapeHtml(entry.path)}</title>
|
|
110
|
+
<style>
|
|
111
|
+
:root { color-scheme: light; }
|
|
112
|
+
* { box-sizing: border-box; }
|
|
113
|
+
body { margin: 0; padding: 20px 24px 40px; font: 13px/1.6 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; color: #111827; background: #fff; }
|
|
114
|
+
h1 { font-size: 15px; font-weight: 650; margin: 0 0 4px; }
|
|
115
|
+
.meta { color: #6b7280; font-size: 12px; margin-bottom: 14px; }
|
|
116
|
+
.meta code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
117
|
+
.hint { color: #9ca3af; font-size: 11px; margin-bottom: 12px; }
|
|
118
|
+
body.embed { padding: 0; }
|
|
119
|
+
body.embed table { font-size: 11px; }
|
|
120
|
+
body.embed td { padding: 0 8px; }
|
|
121
|
+
body.embed th { padding: 4px 8px; }
|
|
122
|
+
body.embed td.num { width: 36px; }
|
|
123
|
+
table { width: 100%; border-collapse: collapse; table-layout: fixed; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
124
|
+
th { text-align: left; font-weight: 600; font-size: 11px; color: #6b7280; padding: 6px 10px; border-bottom: 1px solid #e5e7eb; background: #f9fafb; }
|
|
125
|
+
td { padding: 1px 10px; vertical-align: top; white-space: pre-wrap; word-break: break-word; border-bottom: 1px solid #f3f4f6; }
|
|
126
|
+
td.num { width: 44px; text-align: right; color: #9ca3af; background: #fafafa; user-select: none; }
|
|
127
|
+
td.left, td.right { width: calc(50% - 22px); }
|
|
128
|
+
tr.same td.left, tr.same td.right { color: #6b7280; }
|
|
129
|
+
tr.del td.left { background: #fdecea; color: #8a1f1a; }
|
|
130
|
+
tr.del td.right { background: #fafafa; }
|
|
131
|
+
tr.add td.right { background: #e7f6ee; color: #0f5c37; }
|
|
132
|
+
tr.add td.left { background: #fafafa; }
|
|
133
|
+
tr.change td.left { background: #fdecea; color: #8a1f1a; }
|
|
134
|
+
tr.change td.right { background: #e7f6ee; color: #0f5c37; }
|
|
135
|
+
.empty { color: #9ca3af; padding: 40px 0; text-align: center; }
|
|
136
|
+
</style>
|
|
137
|
+
</head>
|
|
138
|
+
<body class="${embed ? 'embed' : 'standalone'}">
|
|
139
|
+
${embed ? '' : '<h1>改动对比</h1>'}
|
|
140
|
+
<div class="meta"><code>${escapeHtml(entry.path)}</code> · 共 ${entry.rows.length} 行 · ${changed} 处变化</div>
|
|
141
|
+
${embed ? '' : '<div class="hint">这是独立对比页,看完关闭此标签即可回到会话。</div>'}
|
|
142
|
+
<table>
|
|
143
|
+
<thead><tr><th></th><th>原</th><th>新</th></tr></thead>
|
|
144
|
+
<tbody>
|
|
145
|
+
${numbered}
|
|
146
|
+
</tbody>
|
|
147
|
+
</table>
|
|
148
|
+
</body>
|
|
149
|
+
</html>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Reset for tests. */
|
|
153
|
+
export function clearAll() {
|
|
154
|
+
entries.clear();
|
|
155
|
+
}
|