diffstalker-git

LOW
maintainer yogh-io 0 votes scanned 2026-09-17 00:27:14.276658
View on AUR
Why flagged

The 'bun install' is part of building the project from its own source in a git repository, installing declared dependencies for a legitimate build; it does not install arbitrary external packages or execute untrusted remote code.

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' is part of building the project from its own source in a git repository, installing declared dependencies for a legitimate build; it does not install arbitrary external packages or execute untrusted remote code.

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:131 ( 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')
12# In-file outlines are opt-in: diffstalkerd works without the grammars and
13# simply reports no outline capability, which the UI shows rather than guesses.
14# Never point Arch users at `npm i -g diffstalkerd-grammars` - npm's prefix here
15# is /usr, which is precisely the unowned-files problem _check_foreign_bins
16# below exists to warn about.
17optdepends=('diffstalker-grammars: in-file symbol outlines (the `o` key)')
18provides=('diffstalker' 'diffstalkerd')
19conflicts=('diffstalker' 'diffstalkerd')
20source=("${pkgname}::git+${url}.git")
21sha256sums=('SKIP')
22
23pkgver() {
24 cd "$pkgname"
25 git describe --long --tags --abbrev=7 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' ||
26 printf "0.1.0.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short=7 HEAD)"
27}
28
29# The paths this package puts on PATH. `npm install -g diffstalker` and
30# `npm link` both plant unowned files here - npm's prefix on Arch is /usr - and
31# pacman aborts the whole transaction on any file it does not own ("exists in
32# filesystem"). That check runs ahead of every install scriptlet and hook, so a
33# .install file cannot clear the way; build time is the only point where this
34# package still gets to say something, and it is at least ahead of the failure.
35_pathbins=(/usr/bin/diffstalker /usr/bin/diffstalkerd)
36
37_warn() {
38 if declare -F warning >/dev/null; then
39 warning '%s' "$1"
40 else
41 printf '==> WARNING: %s\n' "$1" >&2
42 fi
43}
44
45_check_foreign_bins() {
46 local p target foreign=()
47 for p in "${_pathbins[@]}"; do
48 # -e alone is false for a dangling symlink, which is exactly what a
49 # stale `npm link` leaves once its target moves. Test -L as well or the
50 # most common case slips through unnoticed.
51 [[ -e $p || -L $p ]] || continue
52 pacman -Qo -- "$p" &>/dev/null || foreign+=("$p")
53 done
54 (( ${#foreign[@]} )) || return 0
55
56 _warn "No package owns these paths, so pacman will refuse to install over them:"
57 for p in "${foreign[@]}"; do
58 if target=$(readlink -- "$p"); then _warn " $p -> $target"; else _warn " $p"; fi
59 done
60 _warn "Almost always a leftover npm global install or 'npm link'. Clear it with:"
61 _warn " sudo npm rm -g diffstalker diffstalkerd # tidies node_modules too"
62 _warn " sudo rm ${foreign[*]}"
63 _warn "Or let pacman take the paths over: --overwrite '/usr/bin/diffstalker*'"
64 return 0
65}
66
67# The quieter half of the same problem: an install that is not in /usr/bin at
68# all, but ahead of it on PATH. `bun link`, and any npm prefix under $HOME or
69# /usr/local, put their bins in a directory most shells search first, so pacman
70# installs without a single complaint and the OLD build keeps answering
71# `diffstalker`. That is worse than the file conflict above, which at least
72# fails loudly - here nothing reports anything and the package looks broken or,
73# worse, looks fine while running week-old code. Only entries BEFORE /usr/bin
74# can shadow us; anything after is already shadowed by us and is harmless.
75_check_path_shadow() {
76 local p name dir shadow=() parts=()
77 IFS=: read -r -a parts <<< "$PATH"
78 for p in "${_pathbins[@]}"; do
79 name=${p##*/}
80 for dir in "${parts[@]}"; do
81 [[ $dir == /usr/bin ]] && break
82 [[ -n $dir && -x $dir/$name ]] && { shadow+=("$dir/$name"); break; }
83 done
84 done
85 (( ${#shadow[@]} )) || return 0
86
87 _warn "These come before /usr/bin on PATH and will run INSTEAD of this package:"
88 for p in "${shadow[@]}"; do
89 if dir=$(readlink -- "$p"); then _warn " $p -> $dir"; else _warn " $p"; fi
90 done
91 _warn "Usually 'bun link' from a source checkout, or an npm prefix in \$HOME."
92 _warn "Clear it, or this install has no visible effect:"
93 _warn " bun unlink # run in packages/cli and packages/daemon"
94 _warn " npm rm -g diffstalker diffstalkerd"
95 return 0
96}
97
98prepare() {
99 # Once here, before the multi-minute build, and once more at the end of
100 # package() where it is the last thing printed before pacman's transaction.
101 _check_foreign_bins
102 _check_path_shadow
103}
104
105# Runtime dependencies, staged as a tree of real directories.
106#
107# The workspace install links every package into a shared store
108# (node_modules/.bun/<pkg>@<ver>), so packages/*/node_modules holds symlinks
109# that would land in $pkgdir dangling. A separate production install of the
110# package's own declared dependencies - the same set npm consumers get - with
111# the hoisted linker produces real directories instead, transitive deps
112# included. Versions are pinned to whatever the workspace install resolved, so
113# what ships matches what this build compiled against.
114_stage_runtime_deps() {
115 local pkg="$1" stage="$srcdir/runtime/$(basename "$1")"
116 install -dm755 "$stage"
117 node -e '
118 const fs = require("node:fs");
119 const [pkgDir, outDir] = process.argv.slice(1);
120 const read = (p) => JSON.parse(fs.readFileSync(p, "utf-8"));
121 const dependencies = {};
122 for (const [name, range] of Object.entries(read(pkgDir + "/package.json").dependencies ?? {})) {
123 // Workspace siblings are bundled into dist/ (core, client) or shipped
124 // as their own bin (diffstalkerd) - never installed as a dependency.
125 if (range.startsWith("workspace:")) continue;
126 dependencies[name] = read(pkgDir + "/node_modules/" + name + "/package.json").version;
127 }
128 fs.writeFileSync(outDir + "/package.json",
129 JSON.stringify({ name: "diffstalker-runtime", version: "0.0.0", private: true, dependencies }));
130 ' "$PWD/$pkg" "$stage"
131 ( cd "$stage" && bun install --production --linker hoisted )
132}
133
134build() {
135 cd "$pkgname"
136 bun install
137 # Two published packages: the terminal UI (diffstalker) and the git-state
138 # daemon it spawns (diffstalkerd). Ship both, each from its build:prod
139 # bundle (dist/index.js). No divergent second bun build - the same output
140 # npm consumers get.
141 ( cd packages/cli && bun run build:prod )
142 ( cd packages/daemon && bun run build:prod )
143
144 _stage_runtime_deps packages/cli
145 _stage_runtime_deps packages/daemon
146}
147
148# Install one built component in the layout npm publishes: dist/index.js beside
149# a package.json, node_modules alongside. That layout is load-bearing - the
150# bundles are ESM, so Node needs "type": "module" in a package.json above them;
151# the daemon reads ../package.json for the version it reports to clients, and
152# serves the web UI from web/ next to its own module. Flattening dist/ away
153# breaks all three.
154_install_component() {
155 local pkg="$1" dir="$2" name="$3"
156 local dest="$pkgdir/usr/lib/diffstalker/$dir" version
157 version=$(node -p "require('$PWD/$pkg/package.json').version")
158
159 install -Dm644 "$pkg/dist/index.js" "$dest/dist/index.js"
160 printf '{\n "name": "%s",\n "version": "%s",\n "type": "module",\n "private": true\n}\n' \
161 "$name" "$version" > "$dest/package.json"
162 chmod 644 "$dest/package.json"
163
164 install -dm755 "$dest/node_modules"
165 cp -r "$srcdir/runtime/$dir/node_modules/." "$dest/node_modules/"
166 rm -rf "$dest/node_modules/.bin" "$dest/node_modules/.cache"
167}
168
169package() {
170 cd "$pkgname"
171
172 _install_component packages/cli cli diffstalker
173 _install_component packages/daemon daemon diffstalkerd
174
175 # The symbol worker, beside the daemon bundle. NOT optional and NOT part
176 # of _install_component: only the daemon has one, and the pool resolves it
177 # as a sibling of its own module (new URL('./symbolWorker.js', ...)), which
178 # is exactly the layout npm publishes. Omit it and outlines do not fail
179 # loudly - every extraction returns `unavailable/error`, so an install with
180 # diffstalker-grammars present still shows no symbols and looks like a
181 # broken grammars package rather than a missing file here.
182 install -Dm644 packages/daemon/dist/symbolWorker.js \
183 "$pkgdir/usr/lib/diffstalker/daemon/dist/symbolWorker.js"
184
185 # Web UI assets: the daemon serves the SPA at GET / from web/ next to its
186 # own module, which build:prod placed at dist/web.
187 cp -r packages/daemon/dist/web "$pkgdir/usr/lib/diffstalker/daemon/dist/"
188
189 # Wrapper bins on PATH. The TUI cannot resolve diffstalkerd from its own
190 # node_modules here (it is a separate bin, not a bundled dependency), so it
191 # falls through to PATH and spawns this wrapper on a unix socket.
192 install -dm755 "$pkgdir/usr/bin"
193 cat > "$pkgdir/usr/bin/diffstalker" << 'EOF'
194#!/usr/bin/env node
195import('/usr/lib/diffstalker/cli/dist/index.js').catch((e) => {
196 console.error(e);
197 process.exit(1);
198});
199EOF
200 cat > "$pkgdir/usr/bin/diffstalkerd" << 'EOF'
201#!/usr/bin/env node
202import('/usr/lib/diffstalker/daemon/dist/index.js').catch((e) => {
203 console.error(e);
204 process.exit(1);
205});
206EOF
207 chmod 755 "$pkgdir/usr/bin/diffstalker" "$pkgdir/usr/bin/diffstalkerd"
208
209 # systemd USER unit, never a system one: the socket lives under
210 # $XDG_RUNTIME_DIR (per-user, 0700) and every git call runs as the
211 # invoking user, with their config, ssh keys and worktrees. A system
212 # service would be the wrong uid for all three.
213 #
214 # Not socket-activated, deliberately. The CLI health-probes the socket
215 # with a 250ms budget before falling back to spawning its own daemon
216 # (DaemonLifecycle.ts), and a cold activated start overruns that - the
217 # TUI would then try to spawn a second daemon and hit "already running".
218 # An always-warm service answers the probe immediately.
219 install -Dm644 "$srcdir/$pkgname/packaging/systemd/diffstalkerd.service" \
220 "$pkgdir/usr/lib/systemd/user/diffstalkerd.service"
221
222 install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
223 install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
224
225 # Last word before pacman commits, so the remedy is still on screen when
226 # the "exists in filesystem" error lands a few lines further down - and so
227 # a PATH shadow, which produces no error at all, is the final thing said.
228 _check_foreign_bins
229 _check_path_shadow
230}
231

Scan history

Scanned at (UTC)SeverityRules
2026-09-17 00:27:14 Low 2
2026-09-16 00:03:17 Low 2
2026-09-15 00:25:31 Low 2
2026-09-14 00:27:57 Low 2
2026-09-13 00:19:54 Low 2
2026-09-12 00:25:17 Low 2
2026-09-11 00:19:22 Low 2
2026-09-10 00:22:44 Low 2
2026-09-09 00:04:09 Low 2
2026-09-08 00:18:08 Low 2
2026-09-07 00:30:15 Low 2
2026-09-06 00:17:06 Low 2
2026-09-05 00:16:27 Low 2
2026-09-04 00:03:13 Low 2
2026-09-03 00:15:47 Low 2
2026-09-02 00:02:31 Low 2
2026-09-01 00:11:19 Low 2
2026-08-31 00:19:57 Low 2
2026-08-30 00:04:14 Low 2
2026-08-29 00:29:17 Low 2

Report a package

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

0 / 4000
Your suggestion