diffstalker-git
maintainer yogh-io
· 0 votes
· scanned 2026-08-03 00:08:14.047287
LOW
View on AUR ↗
Why flagged
The 'bun install' in build() is part of building the project from its own source, which is a normal AUR practice; the flagged external package install refers to dependency resolution within the project's workspace, not an arbitrary remote code execution.
Triggered rules
LOW
AI review downgraded a static finding
llm_review
The static rules flagged this MEDIUM, but an AI model (qwen/qwen3-235b-a22b-2507) reviewed the full PKGBUILD and judged it LOW (confidence 90%): The 'bun install' in build() is part of building the project from its own source, which is a normal AUR practice; the flagged external package install refers to dependency resolution within the project's workspace, not an arbitrary remote code execution.
1 higher static finding superseded - not the current verdict (shown for transparency)
MEDIUM
bun install of an undeclared external package
bun_install_external
`bun add` / `bun install <package>` fetches an external package outside source=(). Severity downgraded: the package declares/looks like a Node.js consumer.
-
PKGBUILD:125
( cd "$stage" && bun install --production --linker hoisted )
PKGBUILD
1 offending line(s) highlighted
1
# Maintainer: yogh-io <info@yogh.nl>
2
pkgname=diffstalker-git
3
# Placeholder: pkgver() rewrites this from `git describe` on every build.
4
pkgver=0.9.0.r9.g8df9bfd
5
pkgrel=1
6
pkgdesc="Terminal UI for git staging, committing, and reviewing changes"
7
arch=('any')
8
url="https://github.com/yogh-io/diffstalker"
9
license=('MIT')
10
depends=('nodejs' 'git')
11
makedepends=('bun' 'git')
12
provides=('diffstalker' 'diffstalkerd')
13
conflicts=('diffstalker' 'diffstalkerd')
14
source=("${pkgname}::git+${url}.git")
15
sha256sums=('SKIP')
16
17
pkgver() {
18
cd "$pkgname"
19
git describe --long --tags --abbrev=7 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' ||
20
printf "0.1.0.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short=7 HEAD)"
21
}
22
23
# The paths this package puts on PATH. `npm install -g diffstalker` and
24
# `npm link` both plant unowned files here - npm's prefix on Arch is /usr - and
25
# pacman aborts the whole transaction on any file it does not own ("exists in
26
# filesystem"). That check runs ahead of every install scriptlet and hook, so a
27
# .install file cannot clear the way; build time is the only point where this
28
# package still gets to say something, and it is at least ahead of the failure.
29
_pathbins=(/usr/bin/diffstalker /usr/bin/diffstalkerd)
30
31
_warn() {
32
if declare -F warning >/dev/null; then
33
warning '%s' "$1"
34
else
35
printf '==> WARNING: %s\n' "$1" >&2
36
fi
37
}
38
39
_check_foreign_bins() {
40
local p target foreign=()
41
for p in "${_pathbins[@]}"; do
42
# -e alone is false for a dangling symlink, which is exactly what a
43
# stale `npm link` leaves once its target moves. Test -L as well or the
44
# most common case slips through unnoticed.
45
[[ -e $p || -L $p ]] || continue
46
pacman -Qo -- "$p" &>/dev/null || foreign+=("$p")
47
done
48
(( ${#foreign[@]} )) || return 0
49
50
_warn "No package owns these paths, so pacman will refuse to install over them:"
51
for p in "${foreign[@]}"; do
52
if target=$(readlink -- "$p"); then _warn " $p -> $target"; else _warn " $p"; fi
53
done
54
_warn "Almost always a leftover npm global install or 'npm link'. Clear it with:"
55
_warn " sudo npm rm -g diffstalker diffstalkerd # tidies node_modules too"
56
_warn " sudo rm ${foreign[*]}"
57
_warn "Or let pacman take the paths over: --overwrite '/usr/bin/diffstalker*'"
58
return 0
59
}
60
61
# The quieter half of the same problem: an install that is not in /usr/bin at
62
# all, but ahead of it on PATH. `bun link`, and any npm prefix under $HOME or
63
# /usr/local, put their bins in a directory most shells search first, so pacman
64
# installs without a single complaint and the OLD build keeps answering
65
# `diffstalker`. That is worse than the file conflict above, which at least
66
# fails loudly - here nothing reports anything and the package looks broken or,
67
# worse, looks fine while running week-old code. Only entries BEFORE /usr/bin
68
# can shadow us; anything after is already shadowed by us and is harmless.
69
_check_path_shadow() {
70
local p name dir shadow=() parts=()
71
IFS=: read -r -a parts <<< "$PATH"
72
for p in "${_pathbins[@]}"; do
73
name=${p##*/}
74
for dir in "${parts[@]}"; do
75
[[ $dir == /usr/bin ]] && break
76
[[ -n $dir && -x $dir/$name ]] && { shadow+=("$dir/$name"); break; }
77
done
78
done
79
(( ${#shadow[@]} )) || return 0
80
81
_warn "These come before /usr/bin on PATH and will run INSTEAD of this package:"
82
for p in "${shadow[@]}"; do
83
if dir=$(readlink -- "$p"); then _warn " $p -> $dir"; else _warn " $p"; fi
84
done
85
_warn "Usually 'bun link' from a source checkout, or an npm prefix in \$HOME."
86
_warn "Clear it, or this install has no visible effect:"
87
_warn " bun unlink # run in packages/cli and packages/daemon"
88
_warn " npm rm -g diffstalker diffstalkerd"
89
return 0
90
}
91
92
prepare() {
93
# Once here, before the multi-minute build, and once more at the end of
94
# package() where it is the last thing printed before pacman's transaction.
95
_check_foreign_bins
96
_check_path_shadow
97
}
98
99
# Runtime dependencies, staged as a tree of real directories.
100
#
101
# The workspace install links every package into a shared store
102
# (node_modules/.bun/<pkg>@<ver>), so packages/*/node_modules holds symlinks
103
# that would land in $pkgdir dangling. A separate production install of the
104
# package's own declared dependencies - the same set npm consumers get - with
105
# the hoisted linker produces real directories instead, transitive deps
106
# included. Versions are pinned to whatever the workspace install resolved, so
107
# what ships matches what this build compiled against.
108
_stage_runtime_deps() {
109
local pkg="$1" stage="$srcdir/runtime/$(basename "$1")"
110
install -dm755 "$stage"
111
node -e '
112
const fs = require("node:fs");
113
const [pkgDir, outDir] = process.argv.slice(1);
114
const read = (p) => JSON.parse(fs.readFileSync(p, "utf-8"));
115
const dependencies = {};
116
for (const [name, range] of Object.entries(read(pkgDir + "/package.json").dependencies ?? {})) {
117
// Workspace siblings are bundled into dist/ (core, client) or shipped
118
// as their own bin (diffstalkerd) - never installed as a dependency.
119
if (range.startsWith("workspace:")) continue;
120
dependencies[name] = read(pkgDir + "/node_modules/" + name + "/package.json").version;
121
}
122
fs.writeFileSync(outDir + "/package.json",
123
JSON.stringify({ name: "diffstalker-runtime", version: "0.0.0", private: true, dependencies }));
124
' "$PWD/$pkg" "$stage"
125
( cd "$stage" && bun install --production --linker hoisted )
126
}
127
128
build() {
129
cd "$pkgname"
130
bun install
131
# Two published packages: the terminal UI (diffstalker) and the git-state
132
# daemon it spawns (diffstalkerd). Ship both, each from its build:prod
133
# bundle (dist/index.js). No divergent second bun build - the same output
134
# npm consumers get.
135
( cd packages/cli && bun run build:prod )
136
( cd packages/daemon && bun run build:prod )
137
138
_stage_runtime_deps packages/cli
139
_stage_runtime_deps packages/daemon
140
}
141
142
# Install one built component in the layout npm publishes: dist/index.js beside
143
# a package.json, node_modules alongside. That layout is load-bearing - the
144
# bundles are ESM, so Node needs "type": "module" in a package.json above them;
145
# the daemon reads ../package.json for the version it reports to clients, and
146
# serves the web UI from web/ next to its own module. Flattening dist/ away
147
# breaks all three.
148
_install_component() {
149
local pkg="$1" dir="$2" name="$3"
150
local dest="$pkgdir/usr/lib/diffstalker/$dir" version
151
version=$(node -p "require('$PWD/$pkg/package.json').version")
152
153
install -Dm644 "$pkg/dist/index.js" "$dest/dist/index.js"
154
printf '{\n "name": "%s",\n "version": "%s",\n "type": "module",\n "private": true\n}\n' \
155
"$name" "$version" > "$dest/package.json"
156
chmod 644 "$dest/package.json"
157
158
install -dm755 "$dest/node_modules"
159
cp -r "$srcdir/runtime/$dir/node_modules/." "$dest/node_modules/"
160
rm -rf "$dest/node_modules/.bin" "$dest/node_modules/.cache"
161
}
162
163
package() {
164
cd "$pkgname"
165
166
_install_component packages/cli cli diffstalker
167
_install_component packages/daemon daemon diffstalkerd
168
169
# Web UI assets: the daemon serves the SPA at GET / from web/ next to its
170
# own module, which build:prod placed at dist/web.
171
cp -r packages/daemon/dist/web "$pkgdir/usr/lib/diffstalker/daemon/dist/"
172
173
# Wrapper bins on PATH. The TUI cannot resolve diffstalkerd from its own
174
# node_modules here (it is a separate bin, not a bundled dependency), so it
175
# falls through to PATH and spawns this wrapper on a unix socket.
176
install -dm755 "$pkgdir/usr/bin"
177
cat > "$pkgdir/usr/bin/diffstalker" << 'EOF'
178
#!/usr/bin/env node
179
import('/usr/lib/diffstalker/cli/dist/index.js').catch((e) => {
180
console.error(e);
181
process.exit(1);
182
});
183
EOF
184
cat > "$pkgdir/usr/bin/diffstalkerd" << 'EOF'
185
#!/usr/bin/env node
186
import('/usr/lib/diffstalker/daemon/dist/index.js').catch((e) => {
187
console.error(e);
188
process.exit(1);
189
});
190
EOF
191
chmod 755 "$pkgdir/usr/bin/diffstalker" "$pkgdir/usr/bin/diffstalkerd"
192
193
# systemd USER unit, never a system one: the socket lives under
194
# $XDG_RUNTIME_DIR (per-user, 0700) and every git call runs as the
195
# invoking user, with their config, ssh keys and worktrees. A system
196
# service would be the wrong uid for all three.
197
#
198
# Not socket-activated, deliberately. The CLI health-probes the socket
199
# with a 250ms budget before falling back to spawning its own daemon
200
# (DaemonLifecycle.ts), and a cold activated start overruns that - the
201
# TUI would then try to spawn a second daemon and hit "already running".
202
# An always-warm service answers the probe immediately.
203
install -Dm644 "$srcdir/$pkgname/packaging/systemd/diffstalkerd.service" \
204
"$pkgdir/usr/lib/systemd/user/diffstalkerd.service"
205
206
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
207
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
208
209
# Last word before pacman commits, so the remedy is still on screen when
210
# the "exists in filesystem" error lands a few lines further down - and so
211
# a PATH shadow, which produces no error at all, is the final thing said.
212
_check_foreign_bins
213
_check_path_shadow
214
}
215
Changes since previous scan
--- PKGBUILD @ 2026-07-28 19:39+++ PKGBUILD @ 2026-08-03 00:08@@ -1,15 +1,16 @@ # Maintainer: yogh-io <info@yogh.nl> pkgname=diffstalker-git-pkgver=0.1.4.r0.g592e42c+# Placeholder: pkgver() rewrites this from `git describe` on every build.+pkgver=0.9.0.r9.g8df9bfd pkgrel=1 pkgdesc="Terminal UI for git staging, committing, and reviewing changes" arch=('any') url="https://github.com/yogh-io/diffstalker" license=('MIT')-depends=('nodejs')-makedepends=('npm' 'git')-provides=('diffstalker')-conflicts=('diffstalker')+depends=('nodejs' 'git')+makedepends=('bun' 'git')+provides=('diffstalker' 'diffstalkerd')+conflicts=('diffstalker' 'diffstalkerd') source=("${pkgname}::git+${url}.git") sha256sums=('SKIP') @@ -19,29 +20,196 @@ printf "0.1.0.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short=7 HEAD)" } +# The paths this package puts on PATH. `npm install -g diffstalker` and+# `npm link` both plant unowned files here - npm's prefix on Arch is /usr - and+# pacman aborts the whole transaction on any file it does not own ("exists in+# filesystem"). That check runs ahead of every install scriptlet and hook, so a+# .install file cannot clear the way; build time is the only point where this+# package still gets to say something, and it is at least ahead of the failure.+_pathbins=(/usr/bin/diffstalker /usr/bin/diffstalkerd)++_warn() {+ if declare -F warning >/dev/null; then+ warning '%s' "$1"+ else+ printf '==> WARNING: %s\n' "$1" >&2+ fi+}++_check_foreign_bins() {+ local p target foreign=()+ for p in "${_pathbins[@]}"; do+ # -e alone is false for a dangling symlink, which is exactly what a+ # stale `npm link` leaves once its target moves. Test -L as well or the+ # most common case slips through unnoticed.+ [[ -e $p || -L $p ]] || continue+ pacman -Qo -- "$p" &>/dev/null || foreign+=("$p")+ done+ (( ${#foreign[@]} )) || return 0++ _warn "No package owns these paths, so pacman will refuse to install over them:"+ for p in "${foreign[@]}"; do+ if target=$(readlink -- "$p"); then _warn " $p -> $target"; else _warn " $p"; fi+ done+ _warn "Almost always a leftover npm global install or 'npm link'. Clear it with:"+ _warn " sudo npm rm -g diffstalker diffstalkerd # tidies node_modules too"+ _warn " sudo rm ${foreign[*]}"+ _warn "Or let pacman take the paths over: --overwrite '/usr/bin/diffstalker*'"+ return 0+}++# The quieter half of the same problem: an install that is not in /usr/bin at+# all, but ahead of it on PATH. `bun link`, and any npm prefix under $HOME or+# /usr/local, put their bins in a directory most shells search first, so pacman+# installs without a single complaint and the OLD build keeps answering+# `diffstalker`. That is worse than the file conflict above, which at least+# fails loudly - here nothing reports anything and the package looks broken or,+# worse, looks fine while running week-old code. Only entries BEFORE /usr/bin+# can shadow us; anything after is already shadowed by us and is harmless.+_check_path_shadow() {+ local p name dir shadow=() parts=()+ IFS=: read -r -a parts <<< "$PATH"+ for p in "${_pathbins[@]}"; do+ name=${p##*/}+ for dir in "${parts[@]}"; do+ [[ $dir == /usr/bin ]] && break+ [[ -n $dir && -x $dir/$name ]] && { shadow+=("$dir/$name"); break; }+ done+ done+ (( ${#shadow[@]} )) || return 0++ _warn "These come before /usr/bin on PATH and will run INSTEAD of this package:"+ for p in "${shadow[@]}"; do+ if dir=$(readlink -- "$p"); then _warn " $p -> $dir"; else _warn " $p"; fi+ done+ _warn "Usually 'bun link' from a source checkout, or an npm prefix in \$HOME."+ _warn "Clear it, or this install has no visible effect:"+ _warn " bun unlink # run in packages/cli and packages/daemon"+ _warn " npm rm -g diffstalker diffstalkerd"+ return 0+}++prepare() {+ # Once here, before the multi-minute build, and once more at the end of+ # package() where it is the last thing printed before pacman's transaction.+ _check_foreign_bins+ _check_path_shadow+}++# Runtime dependencies, staged as a tree of real directories.+#+# The workspace install links every package into a shared store+# (node_modules/.bun/<pkg>@<ver>), so packages/*/node_modules holds symlinks+# that would land in $pkgdir dangling. A separate production install of the+# package's own declared dependencies - the same set npm consumers get - with+# the hoisted linker produces real directories instead, transitive deps+# included. Versions are pinned to whatever the workspace install resolved, so+# what ships matches what this build compiled against.+_stage_runtime_deps() {+ local pkg="$1" stage="$srcdir/runtime/$(basename "$1")"+ install -dm755 "$stage"+ node -e '+ const fs = require("node:fs");+ const [pkgDir, outDir] = process.argv.slice(1);+ const read = (p) => JSON.parse(fs.readFileSync(p, "utf-8"));+ const dependencies = {};+ for (const [name, range] of Object.entries(read(pkgDir + "/package.json").dependencies ?? {})) {+ // Workspace siblings are bundled into dist/ (core, client) or shipped+ // as their own bin (diffstalkerd) - never installed as a dependency.+ if (range.startsWith("workspace:")) continue;+ dependencies[name] = read(pkgDir + "/node_modules/" + name + "/package.json").version;+ }+ fs.writeFileSync(outDir + "/package.json",+ JSON.stringify({ name: "diffstalker-runtime", version: "0.0.0", private: true, dependencies }));+ ' "$PWD/$pkg" "$stage"+ ( cd "$stage" && bun install --production --linker hoisted )+}+ build() { cd "$pkgname"- npm ci --ignore-scripts- npm run bundle+ bun install+ # Two published packages: the terminal UI (diffstalker) and the git-state+ # daemon it spawns (diffstalkerd). Ship both, each from its build:prod+ # bundle (dist/index.js). No divergent second bun build - the same output+ # npm consumers get.+ ( cd packages/cli && bun run build:prod )+ ( cd packages/daemon && bun run build:prod )++ _stage_runtime_deps packages/cli+ _stage_runtime_deps packages/daemon+}++# Install one built component in the layout npm publishes: dist/index.js beside+# a package.json, node_modules alongside. That layout is load-bearing - the+# bundles are ESM, so Node needs "type": "module" in a package.json above them;+# the daemon reads ../package.json for the version it reports to clients, and+# serves the web UI from web/ next to its own module. Flattening dist/ away+# breaks all three.+_install_component() {+ local pkg="$1" dir="$2" name="$3"+ local dest="$pkgdir/usr/lib/diffstalker/$dir" version+ version=$(node -p "require('$PWD/$pkg/package.json').version")++ install -Dm644 "$pkg/dist/index.js" "$dest/dist/index.js"+ printf '{\n "name": "%s",\n "version": "%s",\n "type": "module",\n "private": true\n}\n' \+ "$name" "$version" > "$dest/package.json"+ chmod 644 "$dest/package.json"++ install -dm755 "$dest/node_modules"+ cp -r "$srcdir/runtime/$dir/node_modules/." "$dest/node_modules/"+ rm -rf "$dest/node_modules/.bin" "$dest/node_modules/.cache" } package() { cd "$pkgname" - # Install bundled files to /usr/lib/diffstalker- install -dm755 "$pkgdir/usr/lib/diffstalker"- cp -r dist/bundle/* "$pkgdir/usr/lib/diffstalker/"-- # Create wrapper script+ _install_component packages/cli cli diffstalker+ _install_component packages/daemon daemon diffstalkerd++ # Web UI assets: the daemon serves the SPA at GET / from web/ next to its+ # own module, which build:prod placed at dist/web.+ cp -r packages/daemon/dist/web "$pkgdir/usr/lib/diffstalker/daemon/dist/"++ # Wrapper bins on PATH. The TUI cannot resolve diffstalkerd from its own+ # node_modules here (it is a separate bin, not a bundled dependency), so it+ # falls through to PATH and spawns this wrapper on a unix socket. install -dm755 "$pkgdir/usr/bin" cat > "$pkgdir/usr/bin/diffstalker" << 'EOF'-#!/bin/sh-exec node /usr/lib/diffstalker/index.js "$@"+#!/usr/bin/env node+import('/usr/lib/diffstalker/cli/dist/index.js').catch((e) => {+ console.error(e);+ process.exit(1);+}); EOF- chmod 755 "$pkgdir/usr/bin/diffstalker"-- # Install license and documentation+ cat > "$pkgdir/usr/bin/diffstalkerd" << 'EOF'+#!/usr/bin/env node+import('/usr/lib/diffstalker/daemon/dist/index.js').catch((e) => {+ console.error(e);+ process.exit(1);+});+EOF+ chmod 755 "$pkgdir/usr/bin/diffstalker" "$pkgdir/usr/bin/diffstalkerd"++ # systemd USER unit, never a system one: the socket lives under+ # $XDG_RUNTIME_DIR (per-user, 0700) and every git call runs as the+ # invoking user, with their config, ssh keys and worktrees. A system+ # service would be the wrong uid for all three.+ #+ # Not socket-activated, deliberately. The CLI health-probes the socket+ # with a 250ms budget before falling back to spawning its own daemon+ # (DaemonLifecycle.ts), and a cold activated start overruns that - the+ # TUI would then try to spawn a second daemon and hit "already running".+ # An always-warm service answers the probe immediately.+ install -Dm644 "$srcdir/$pkgname/packaging/systemd/diffstalkerd.service" \+ "$pkgdir/usr/lib/systemd/user/diffstalkerd.service"+ install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"-}-++ # Last word before pacman commits, so the remedy is still on screen when+ # the "exists in filesystem" error lands a few lines further down - and so+ # a PATH shadow, which produces no error at all, is the final thing said.+ _check_foreign_bins+ _check_path_shadow+}+Scan history
| Scanned at (UTC) | Severity | Rules |
|---|---|---|
| 2026-08-03 00:08:14 | LOW | 2 |
| 2026-08-02 00:16:08 | LOW | 2 |
| 2026-08-01 00:11:18 | LOW | 2 |
| 2026-07-31 15:18:17 | MEDIUM | 1 |
| 2026-07-28 19:39:35 | CLEAN | 0 |
| 2026-06-19 22:34:54 | CLEAN | 0 |