@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.
@@ -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
+ }