@young1lin/dsh-ui-gitworkbench 0.1.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/AGENTS.md +70 -0
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/README_EN.md +74 -0
- package/cordis.patch.yml +7 -0
- package/lib/atomic-json.js +48 -0
- package/lib/client.js +15297 -0
- package/lib/commit-cache.js +68 -0
- package/lib/git-log.js +79 -0
- package/lib/git-ops.js +409 -0
- package/lib/index.js +1143 -0
- package/lib/style-store.js +123 -0
- package/lib/worktree.js +112 -0
- package/package.json +86 -0
- package/scripts/install.ps1 +240 -0
- package/scripts/install.sh +231 -0
- package/src/atomic-json.ts +55 -0
- package/src/client/GitWorkbenchPanel.module.css +1512 -0
- package/src/client/GitWorkbenchPanel.tsx +3446 -0
- package/src/client/commit-graph.ts +140 -0
- package/src/client/diff-model.ts +193 -0
- package/src/client/highlight.ts +257 -0
- package/src/client/index.ts +198 -0
- package/src/client/locales.ts +270 -0
- package/src/client/op-feedback.ts +65 -0
- package/src/client/stage-tree.ts +178 -0
- package/src/client/themes.ts +181 -0
- package/src/client/worktree-view.ts +193 -0
- package/src/commit-cache.ts +69 -0
- package/src/git-log.ts +92 -0
- package/src/git-ops.ts +490 -0
- package/src/index.ts +1172 -0
- package/src/style-store.ts +144 -0
- package/src/types/dsh-client-shim.d.ts +100 -0
- package/src/types/dsh-shim.d.ts +77 -0
- package/src/worktree.ts +142 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# dsh-ui-gitworkbench one-shot installer (official CLI path; macOS / Linux /
|
|
4
|
+
# Windows Git Bash)
|
|
5
|
+
#
|
|
6
|
+
# Installs the npm package and mounts it through the official plugin command:
|
|
7
|
+
# dsh plugin --profile web add @young1lin/dsh-ui-gitworkbench@<version>
|
|
8
|
+
#
|
|
9
|
+
# The package declares dsh.bundle.patch (cordis.patch.yml), so the CLI's bundle
|
|
10
|
+
# coordination registers it into the profile's dsh.profile.bundles and mounts
|
|
11
|
+
# the host half on next start -- no manual cordis.patch.yml mount entry.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bash scripts/install.sh [version] [--restart] [--dry-run]
|
|
15
|
+
#
|
|
16
|
+
# version npm version/range, default latest (resolved to newest).
|
|
17
|
+
# Examples: 0.1.0, ^0.1.0, latest
|
|
18
|
+
# --restart After install, try `pm2 restart dsh-web` (hint only when pm2
|
|
19
|
+
# is absent). Restarting drops the current page session.
|
|
20
|
+
# --dry-run Print the planned steps, write nothing.
|
|
21
|
+
# -h/--help Print this help.
|
|
22
|
+
#
|
|
23
|
+
# Environment (all optional; auto-detected):
|
|
24
|
+
# DSH_HOME default ~/.dsh ($USERPROFILE/.dsh under Git Bash)
|
|
25
|
+
# REGISTRY default https://registry.npmjs.org
|
|
26
|
+
# DSH_CMD default: dsh on PATH, else npx -y --package @deepseek-ai/dsh
|
|
27
|
+
#
|
|
28
|
+
# Notes:
|
|
29
|
+
# - pnpm 11 minimumReleaseAge rejects versions published <24h ago; the script
|
|
30
|
+
# pre-writes minimumReleaseAgeExclude for this package (idempotent) so a
|
|
31
|
+
# fresh release installs on first try.
|
|
32
|
+
# - Older installs used a manual link: dependency + cordis.patch.yml mount
|
|
33
|
+
# entry. The script removes both idempotently (double mounting = two host
|
|
34
|
+
# halves and two chips).
|
|
35
|
+
# - Rollback: dsh plugin --profile web remove @young1lin/dsh-ui-gitworkbench
|
|
36
|
+
# =============================================================================
|
|
37
|
+
set -euo pipefail
|
|
38
|
+
|
|
39
|
+
for arg in "$@"; do
|
|
40
|
+
if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
|
|
41
|
+
cat <<'EOF'
|
|
42
|
+
dsh-ui-gitworkbench one-shot installer
|
|
43
|
+
|
|
44
|
+
Usage: bash scripts/install.sh [version] [--restart] [--dry-run]
|
|
45
|
+
|
|
46
|
+
version npm version/range, default latest. Examples: 0.1.0, ^0.1.0
|
|
47
|
+
--restart try `pm2 restart dsh-web` after install (hint when pm2 absent)
|
|
48
|
+
--dry-run print planned steps, write nothing
|
|
49
|
+
|
|
50
|
+
Environment (optional): DSH_HOME (~/.dsh), REGISTRY (npmjs), DSH_CMD (dsh)
|
|
51
|
+
EOF
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}"
|
|
57
|
+
PROFILE_DIR="$DSH_HOME/profiles/web"
|
|
58
|
+
WS_YML="$PROFILE_DIR/pnpm-workspace.yaml"
|
|
59
|
+
PATCH_YML="$PROFILE_DIR/cordis.patch.yml"
|
|
60
|
+
REGISTRY="${REGISTRY:-https://registry.npmjs.org}"
|
|
61
|
+
PKG="@young1lin/dsh-ui-gitworkbench"
|
|
62
|
+
DSH_CMD="${DSH_CMD:-dsh}"
|
|
63
|
+
|
|
64
|
+
RESTART=false
|
|
65
|
+
DRY_RUN=false
|
|
66
|
+
VERSION_SPEC=""
|
|
67
|
+
for arg in "$@"; do
|
|
68
|
+
case "$arg" in
|
|
69
|
+
--restart) RESTART=true ;;
|
|
70
|
+
--dry-run) DRY_RUN=true ;;
|
|
71
|
+
-h|--help) : ;;
|
|
72
|
+
-*) echo "unknown flag: $arg (use -h for usage)" >&2; exit 2 ;;
|
|
73
|
+
*) VERSION_SPEC="$arg" ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
say() { printf '\033[32m[install]\033[0m %s\n' "$*"; }
|
|
78
|
+
warn() { printf '\033[33m[warn]\033[0m %s\n' "$*" >&2; }
|
|
79
|
+
die() { printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
80
|
+
|
|
81
|
+
resolve_spec() {
|
|
82
|
+
local given="${1:-latest}"
|
|
83
|
+
case "$given" in
|
|
84
|
+
latest)
|
|
85
|
+
local v=""
|
|
86
|
+
if command -v npm >/dev/null 2>&1; then
|
|
87
|
+
v="$(npm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
|
|
88
|
+
fi
|
|
89
|
+
if [ -z "$v" ] && command -v pnpm >/dev/null 2>&1; then
|
|
90
|
+
v="$(pnpm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
|
|
91
|
+
fi
|
|
92
|
+
if [ -n "$v" ]; then
|
|
93
|
+
printf '%s' "$v"
|
|
94
|
+
else
|
|
95
|
+
warn "cannot resolve latest version (npm/pnpm query failed); falling back to latest"
|
|
96
|
+
warn "if you know the version, pass it explicitly: bash scripts/install.sh 0.1.0"
|
|
97
|
+
printf 'latest'
|
|
98
|
+
fi
|
|
99
|
+
;;
|
|
100
|
+
*) printf '%s' "$given" ;;
|
|
101
|
+
esac
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
dsh_cli() {
|
|
105
|
+
if command -v "$DSH_CMD" >/dev/null 2>&1; then
|
|
106
|
+
printf '%s' "$DSH_CMD"
|
|
107
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
108
|
+
printf 'npx -y --package @deepseek-ai/dsh dsh'
|
|
109
|
+
else
|
|
110
|
+
die "neither dsh nor npx found. install DSH (and Node/npm), or set DSH_CMD."
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
command -v node >/dev/null 2>&1 || die "node not found (DSH needs Node.js >= 20)."
|
|
115
|
+
[ -d "$PROFILE_DIR" ] || die "profile directory not found: ${PROFILE_DIR} (run dsh web once first)"
|
|
116
|
+
[ -f "$WS_YML" ] || die "${WS_YML} not found (initialize the web profile first)"
|
|
117
|
+
|
|
118
|
+
SPEC="$(resolve_spec "$VERSION_SPEC")"
|
|
119
|
+
CLI="$(dsh_cli)"
|
|
120
|
+
say "target: $CLI plugin --profile web add $PKG@${SPEC} (profile: ${PROFILE_DIR})"
|
|
121
|
+
|
|
122
|
+
if [ "$DRY_RUN" = true ]; then
|
|
123
|
+
say "[dry-run] step 1: ensure $WS_YML has minimumReleaseAgeExclude (${PKG})"
|
|
124
|
+
say "[dry-run] step 2: run $CLI plugin --profile web add $PKG@${SPEC} (install + bundle registration)"
|
|
125
|
+
say "[dry-run] step 3: verify dsh.profile.bundles contains ${PKG}"
|
|
126
|
+
say "[dry-run] step 4: idempotently drop the old manual gitworkbench mount entry in $PATCH_YML (if any)"
|
|
127
|
+
if [ "$RESTART" = true ]; then say "[dry-run] step 5: pm2 restart dsh-web"; else say "[dry-run] step 5: prompt for manual DSH restart"; fi
|
|
128
|
+
exit 0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Step 1: pre-write workspace settings (idempotent) so pnpm 11 accepts a
|
|
132
|
+
# version published less than 24h ago.
|
|
133
|
+
WS_RESULT="$(node -e '
|
|
134
|
+
const fs = require("fs");
|
|
135
|
+
const p = process.argv[1];
|
|
136
|
+
const pkg = process.argv[2];
|
|
137
|
+
let t = fs.readFileSync(p, "utf8");
|
|
138
|
+
const before = t;
|
|
139
|
+
const re = new RegExp("^\\s*-\\s+" + pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*$", "m");
|
|
140
|
+
if (!re.test(t)) {
|
|
141
|
+
if (/^\s*minimumReleaseAgeExclude:\s*$/m.test(t)) {
|
|
142
|
+
t = t.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - " + pkg);
|
|
143
|
+
} else {
|
|
144
|
+
t += "\nminimumReleaseAgeExclude:\n - " + pkg + "\n";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (t !== before) fs.writeFileSync(p, t);
|
|
148
|
+
console.log(t === before ? "unchanged" : "updated");
|
|
149
|
+
' "$WS_YML" "$PKG")"
|
|
150
|
+
[ "$WS_RESULT" = "updated" ] \
|
|
151
|
+
&& say "ensured ${WS_YML}: minimumReleaseAgeExclude (${PKG})" \
|
|
152
|
+
|| say 'workspace settings already fine, skipped'
|
|
153
|
+
|
|
154
|
+
# Step 2: official CLI install + bundle registration (mount included)
|
|
155
|
+
say "running $CLI plugin --profile web add $PKG@$SPEC ..."
|
|
156
|
+
if ! $CLI plugin --profile web add "$PKG@$SPEC" 2>&1 | tail -n +1; then
|
|
157
|
+
warn 'dsh plugin add failed. likely causes:'
|
|
158
|
+
warn ' - network/auth: npm registry unreachable or auth required.'
|
|
159
|
+
warn " - dependency conflict: retry manually: cd $PROFILE_DIR && pnpm install"
|
|
160
|
+
exit 1
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# Step 3: verify the bundle got registered (the sign mounting took effect)
|
|
164
|
+
if ! node -e '
|
|
165
|
+
const fs = require("fs");
|
|
166
|
+
const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
167
|
+
const bundles = p.dsh?.profile?.bundles ?? [];
|
|
168
|
+
process.exit(bundles.includes(process.argv[2]) ? 0 : 1);
|
|
169
|
+
' "$PROFILE_DIR/package.json" "$PKG"; then
|
|
170
|
+
warn "${PKG} missing from dsh.profile.bundles -- bundle registration failed."
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
say "bundle registered: dsh.profile.bundles contains ${PKG} (mounts on next start)"
|
|
174
|
+
|
|
175
|
+
# Step 4: idempotently remove an old manual mount entry (double-mount guard)
|
|
176
|
+
if [ -f "$PATCH_YML" ]; then
|
|
177
|
+
MOUNT_RESULT="$(node -e '
|
|
178
|
+
const fs = require("fs");
|
|
179
|
+
const p = process.argv[1];
|
|
180
|
+
const lines = fs.readFileSync(p, "utf8").split("\n");
|
|
181
|
+
const out = [];
|
|
182
|
+
let i = 0;
|
|
183
|
+
let removed = false;
|
|
184
|
+
while (i < lines.length) {
|
|
185
|
+
const line = lines[i];
|
|
186
|
+
if (/^[ \t]*- insert:\s*$/.test(line)) {
|
|
187
|
+
const block = [line];
|
|
188
|
+
let j = i + 1;
|
|
189
|
+
while (j < lines.length && lines[j].trim() !== "" && !/^-\s/.test(lines[j])) {
|
|
190
|
+
block.push(lines[j]);
|
|
191
|
+
j++;
|
|
192
|
+
}
|
|
193
|
+
if (block.some((l) => /id:\s*ui-gitworkbench\b/.test(l) || /name:\s*'\''\@young1lin\/dsh-ui-gitworkbench'\''/.test(l))) {
|
|
194
|
+
while (out.length && /^[ \t]*#/.test(out[out.length - 1])) out.pop();
|
|
195
|
+
i = j;
|
|
196
|
+
removed = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
out.push(line);
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
if (!removed) {
|
|
204
|
+
console.log("none");
|
|
205
|
+
} else {
|
|
206
|
+
const t = out.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
207
|
+
fs.writeFileSync(p, t);
|
|
208
|
+
console.log("removed");
|
|
209
|
+
}
|
|
210
|
+
' "$PATCH_YML")"
|
|
211
|
+
[ "$MOUNT_RESULT" = "removed" ] \
|
|
212
|
+
&& say "removed the old manual gitworkbench mount entry from $PATCH_YML (bundle channel takes over)" \
|
|
213
|
+
|| say 'no stale manual mount entry, skipped'
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
say "install complete: $PKG@$SPEC"
|
|
217
|
+
|
|
218
|
+
# Step 5: restart guidance
|
|
219
|
+
if [ "$RESTART" = true ]; then
|
|
220
|
+
if command -v pm2 >/dev/null 2>&1; then
|
|
221
|
+
say 'restarting dsh-web (pm2)...'
|
|
222
|
+
pm2 restart dsh-web || warn 'pm2 restart failed; restart DSH manually'
|
|
223
|
+
else
|
|
224
|
+
warn 'pm2 not found; restart DSH manually (e.g. pm2 restart dsh-web, or dsh web)'
|
|
225
|
+
fi
|
|
226
|
+
else
|
|
227
|
+
say 'next: restart DSH and hard-refresh (Cmd/Ctrl+Shift+R).'
|
|
228
|
+
if command -v pm2 >/dev/null 2>&1; then
|
|
229
|
+
say 'available here: pm2 restart dsh-web (briefly drops the current page session)'
|
|
230
|
+
fi
|
|
231
|
+
fi
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash-safe JSON writes for this plugin's small state files under `~/.dsh`.
|
|
3
|
+
*
|
|
4
|
+
* IO is passed in rather than imported so the callers stay testable without a
|
|
5
|
+
* real filesystem, and so the one Windows-specific retry lives in a single place
|
|
6
|
+
* instead of once per state file.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
/** Backoff before each rename retry, in ms. */
|
|
11
|
+
const RENAME_RETRY_DELAYS = [25, 50, 100, 200, 400]
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param ms - milliseconds to wait.
|
|
15
|
+
* @returns a promise resolving after that delay.
|
|
16
|
+
*/
|
|
17
|
+
function delay(ms: number): Promise<void> {
|
|
18
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Write a JSON value so a crash can never leave a truncated file behind.
|
|
23
|
+
*
|
|
24
|
+
* The value is staged into `<path>.tmp` and renamed over the destination, which
|
|
25
|
+
* is atomic within a filesystem. On Windows that rename fails with EPERM while
|
|
26
|
+
* another handle briefly holds the destination open (a concurrent read, an
|
|
27
|
+
* antivirus scan, the search indexer); those locks are short-lived, so the
|
|
28
|
+
* rename is retried with backoff before the error is surfaced.
|
|
29
|
+
* @param ensureDir - creates the containing directory, recursively.
|
|
30
|
+
* @param writeText - writes a file's whole text.
|
|
31
|
+
* @param rename - renames a path over another.
|
|
32
|
+
* @param path - destination path.
|
|
33
|
+
* @param value - JSON-serializable value.
|
|
34
|
+
* @throws whatever `rename` threw, once the retries are exhausted.
|
|
35
|
+
*/
|
|
36
|
+
export async function saveJsonAtomic(
|
|
37
|
+
ensureDir: (dir: string) => Promise<void>,
|
|
38
|
+
writeText: (path: string, text: string) => Promise<void>,
|
|
39
|
+
rename: (from: string, to: string) => Promise<void>,
|
|
40
|
+
path: string,
|
|
41
|
+
value: unknown,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
await ensureDir(join(path, '..'))
|
|
44
|
+
const tmp = `${path}.tmp`
|
|
45
|
+
await writeText(tmp, `${JSON.stringify(value, null, 2)}\n`)
|
|
46
|
+
for (let attempt = 0; ; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
await rename(tmp, path)
|
|
49
|
+
return
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (attempt >= RENAME_RETRY_DELAYS.length) throw error
|
|
52
|
+
await delay(RENAME_RETRY_DELAYS[attempt])
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|