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>
2pkgname=diffstalker-git
3# Placeholder: pkgver() rewrites this from `git describe` on every build.
4pkgver=0.9.0.r9.g8df9bfd
5pkgrel=1
6pkgdesc="Terminal UI for git staging, committing, and reviewing changes"
7arch=('any')
8url="https://github.com/yogh-io/diffstalker"
9license=('MIT')
10depends=('nodejs' 'git')
11makedepends=('bun' 'git')
12provides=('diffstalker' 'diffstalkerd')
13conflicts=('diffstalker' 'diffstalkerd')
14source=("${pkgname}::git+${url}.git")
15sha256sums=('SKIP')
16
17pkgver() {
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
92prepare() {
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
128build() {
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
163package() {
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
179import('/usr/lib/diffstalker/cli/dist/index.js').catch((e) => {
180 console.error(e);
181 process.exit(1);
182});
183EOF
184 cat > "$pkgdir/usr/bin/diffstalkerd" << 'EOF'
185#!/usr/bin/env node
186import('/usr/lib/diffstalker/daemon/dist/index.js').catch((e) => {
187 console.error(e);
188 process.exit(1);
189});
190EOF
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)SeverityRules
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

Report a package

Reports go to the AURWatch maintainer (one person) and are read by hand. No login required.

0 / 4000
Your suggestion