fix(higgsfield): report upstream drift on every enable; final review fixes

This commit is contained in:
bastien
2026-09-30 16:35:45 +02:00
parent 142f73b08e
commit 4c7db8893b
6 changed files with 63 additions and 28 deletions
+1 -1
View File
@@ -401,7 +401,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
`verification-before-completion` to the verifier gates. `verification-before-completion` to the verifier gates.
### Security ### Security
- `settings.json` `permissions.deny` now refuses `npm i -g`, `npm install --global` and `npm i --global`: the rule matched `npm install -g` only, so the other spellings of the same global install went through. - `settings.json` `permissions.deny` now refuses three more spellings of a global npm install (`npm i -g`, `npm install --global`, `npm i --global`): the rule matched `npm install -g` only. Not a complete list: forms with the flag after the package name, such as `npm i <pkg> -g`, still pass.
- **Ten secret-reader deny rules added**: `sed`, `awk`, `cut`, `tr`, - **Ten secret-reader deny rules added**: `sed`, `awk`, `cut`, `tr`,
`sort`, `uniq`, `diff`, `od`, `xxd`, `strings` against `.env*`. Six of `sort`, `uniq`, `diff`, `od`, `xxd`, `strings` against `.env*`. Six of
those tools sat in `permissions.allow`, so reading a `.env` through those tools sat in `permissions.allow`, so reading a `.env` through
+9 -4
View File
@@ -380,11 +380,12 @@ bash lib/toggle-external.sh disable higgsfield
`higgsfield` links a fixed list of seven media skills: generate, soul-id, `higgsfield` links a fixed list of seven media skills: generate, soul-id,
product-photoshoot, brandkit, marketplace-cards, video-explainer and product-photoshoot, brandkit, marketplace-cards, video-explainer and
youtube-thumbnail. The list is `HIGGSFIELD_MEDIA_SKILLS` in youtube-thumbnail. The list is `HIGGSFIELD_MEDIA_SKILLS` in
`lib/toggle-external.sh`. A skill that upstream adds later is synced and `lib/toggle-external.sh`. A skill that upstream adds later is synced, and
reported, and stays unlinked until it is added there. every `enable higgsfield` names it, the pack being on or not. It stays
unlinked until it is added to the list.
`higgsfield-websites` is kept apart. Here it helps with landing pages inside `higgsfield-websites` is kept apart. It helps with landing pages inside the
the design stack (assets, references), and `higgsfield website design stack (assets, references), and `higgsfield website
create|deploy|publish` stays unused. Claude enables either toggle itself on create|deploy|publish` stays unused. Claude enables either toggle itself on
an explicit ask (Skill routing in `CLAUDE.global.md`) and checks the price an explicit ask (Skill routing in `CLAUDE.global.md`) and checks the price
with `higgsfield generate cost` before a paid run. with `higgsfield generate cost` before a paid run.
@@ -393,6 +394,10 @@ The skills are cloned, not installed with `npx skills add`: that installer
links every skill into `~/.claude/skills` on each refresh, which would undo links every skill into `~/.claude/skills` on each refresh, which would undo
the off-by-default state. the off-by-default state.
After the first login, select a workspace once: `higgsfield workspace list`,
then `higgsfield workspace set <id>`. Until then the account commands answer
"No workspace selected", even though the session is active.
The package ships its binary through a postinstall script. If npm holds that The package ships its binary through a postinstall script. If npm holds that
script back, `higgsfield` exists on PATH and fails at once; reinstall with script back, `higgsfield` exists on PATH and fails at once; reinstall with
`npm install -g --allow-scripts=@higgsfield/cli @higgsfield/cli`. `npm install -g --allow-scripts=@higgsfield/cli @higgsfield/cli`.
+11 -7
View File
@@ -59,15 +59,19 @@ higgsfield_sync_skills() {
} }
# _higgsfield_probe <args...> # _higgsfield_probe <args...>
# Run `higgsfield <args>` silently, 15 s at most when `timeout` exists. The # Run `higgsfield <args>` silently, 15 s at most when a timeout tool exists
# CLI is closed source: a probe must never hang an installer, and what it # (`timeout`, or `gtimeout` from Homebrew coreutils on macOS). The CLI is
# prints (a token, for `auth token`) must never reach a terminal or a log. # closed source: a probe must never hang an installer, and what it prints
# (a token, for `auth token`) must never reach a terminal or a log.
_higgsfield_probe() { _higgsfield_probe() {
if command -v timeout >/dev/null 2>&1; then local tool
timeout 15 higgsfield "$@" </dev/null >/dev/null 2>&1 for tool in timeout gtimeout; do
else if command -v "$tool" >/dev/null 2>&1; then
higgsfield "$@" </dev/null >/dev/null 2>&1 "$tool" 15 higgsfield "$@" </dev/null >/dev/null 2>&1
return
fi fi
done
higgsfield "$@" </dev/null >/dev/null 2>&1
} }
# higgsfield_cli_ok — 0 when the binary answers. `command -v` alone only # higgsfield_cli_ok — 0 when the binary answers. `command -v` alone only
+12
View File
@@ -110,6 +110,7 @@ probe() {
# ── sync ──────────────────────────────────────────────────── # ── sync ────────────────────────────────────────────────────
UP="$WORK/upstream"; mk_upstream "$UP" UP="$WORK/upstream"; mk_upstream "$UP"
# The repo path carries a space on purpose: every expansion must be quoted.
R1="$WORK/r 1"; mkdir -p "$R1/skills" "$R1/skills-disabled" R1="$WORK/r 1"; mkdir -p "$R1/skills" "$R1/skills-disabled"
EXT="$R1/skills-external" EXT="$R1/skills-external"
@@ -162,6 +163,12 @@ expect shim-only \
"$(FAKE_HF_BINARY=missing probe "$BIN:$PATH" higgsfield_cli_ok)" "rc=1" "$(FAKE_HF_BINARY=missing probe "$BIN:$PATH" higgsfield_cli_ok)" "rc=1"
expect no-cli "$(probe "$CLEAN" higgsfield_cli_ok)" "rc=127" expect no-cli "$(probe "$CLEAN" higgsfield_cli_ok)" "rc=127"
expect no-timeout "$(probe "$BIN:$CLEAN" higgsfield_cli_ok)" "rc=0" expect no-timeout "$(probe "$BIN:$CLEAN" higgsfield_cli_ok)" "rc=0"
# macOS spelling: only `gtimeout` exists. A wrapper, not a symlink: a
# multi-call coreutils binary dispatches on the name it is invoked under.
GT="$WORK/gtbin"; mkdir -p "$GT"
printf '#!/bin/sh\nexec %s "$@"\n' "$(command -v timeout)" > "$GT/gtimeout"
chmod +x "$GT/gtimeout"
expect gtimeout "$(probe "$BIN:$GT:$CLEAN" higgsfield_signed_in)" "rc=0"
verdict PROBES_SILENT verdict PROBES_SILENT
# ── toggle ────────────────────────────────────────────────── # ── toggle ──────────────────────────────────────────────────
@@ -227,6 +234,11 @@ expect brandkit-off "$(yn test -e "$F8/skills/higgsfield-brandkit")" no
expect_has reported "$out" "higgsfield-newcomer" expect_has reported "$out" "higgsfield-newcomer"
expect_not noskill-quiet "$out" "higgsfield-noskill" expect_not noskill-quiet "$out" "higgsfield-noskill"
expect links "$(entries "$F8/skills")" 2 expect links "$(entries "$F8/skills")" 2
# Enabled is the steady state: a re-run must still name the drift.
out="$(tog "$F8" enable higgsfield)"; rc=$?
expect again-rc "$rc" 0
expect_has again-state "$out" "higgsfield already enabled"
expect_has again-reported "$out" "higgsfield-newcomer"
verdict UNLISTED_NOT_LINKED verdict UNLISTED_NOT_LINKED
F3="$WORK/f3"; mk_toggle_fx "$F3" "${PACK[@]}" F3="$WORK/f3"; mk_toggle_fx "$F3" "${PACK[@]}"
+16 -9
View File
@@ -113,17 +113,21 @@ pack_skills() {
esac esac
} }
# bounded <cmd...> — run a CLI probe silently, 15 s at most when `timeout` # bounded <cmd...> — run a CLI probe silently, 15 s at most when a timeout
# exists: a closed-source binary must never hang a toggle, and what it # tool exists (`timeout`, or `gtimeout` from Homebrew coreutils on macOS):
# prints (a token) must never reach the terminal. Twin of # a closed-source binary must never hang a toggle, and what it prints (a
# _higgsfield_probe in lib/higgsfield-skills.sh, kept here because this # token) must never reach the terminal. Twin of _higgsfield_probe in
# script takes no extra `source` (the fixture suites copy it alone). # lib/higgsfield-skills.sh, kept here because this script takes no extra
# `source` (the fixture suites copy it alone).
bounded() { bounded() {
if command -v timeout >/dev/null 2>&1; then local tool
timeout 15 "$@" </dev/null >/dev/null 2>&1 for tool in timeout gtimeout; do
else if command -v "$tool" >/dev/null 2>&1; then
"$@" </dev/null >/dev/null 2>&1 "$tool" 15 "$@" </dev/null >/dev/null 2>&1
return
fi fi
done
"$@" </dev/null >/dev/null 2>&1
} }
# Post-enable notes for a pack. Its skills shell out to a CLI: without it # Post-enable notes for a pack. Its skills shell out to a CLI: without it
@@ -322,6 +326,9 @@ enable_tool() {
return 1 return 1
fi fi
warn "$tool already enabled" warn "$tool already enabled"
# Enabled is the steady state, and Claude re-runs this on every
# media ask: the hints (upstream drift, CLI, session) show here too.
if [ "$tool" = "higgsfield" ]; then pack_hints higgsfield; fi
return 0 return 0
fi fi
ok "$tool enabled ($((restored + linked)) skills: $restored restored, $linked linked)" ok "$tool enabled ($((restored + linked)) skills: $restored restored, $linked linked)"
+10 -3
View File
@@ -489,14 +489,21 @@ print(d.get('higgsfield',{}).get('version','latest'))
HF_PKG="@higgsfield/cli@latest" HF_PKG="@higgsfield/cli@latest"
[ -n "$HF_VER" ] && [ "$HF_VER" != "latest" ] \ [ -n "$HF_VER" ] && [ "$HF_VER" != "latest" ] \
&& HF_PKG="@higgsfield/cli@${HF_VER}" && HF_PKG="@higgsfield/cli@${HF_VER}"
HF_NPM_OK=true # npm updates only a copy npm installed: a CLI that came from Homebrew
npm install -g "$HF_PKG" 2>/dev/null || HF_NPM_OK=false # or the vendor's installer would otherwise gain a second, competing copy.
HF_NPM=skipped
if npm ls -g @higgsfield/cli >/dev/null 2>&1; then
HF_NPM=ok
npm install -g "$HF_PKG" 2>/dev/null || HF_NPM=failed
fi
# The probe first, then npm's status: an update that skips the package's # The probe first, then npm's status: an update that skips the package's
# postinstall script exits 0 and leaves the shim with no binary behind it. # postinstall script exits 0 and leaves the shim with no binary behind it.
if ! higgsfield_cli_ok; then if ! higgsfield_cli_ok; then
warn "Higgsfield CLI does not answer after the update — run: npm install -g --allow-scripts=@higgsfield/cli @higgsfield/cli" warn "Higgsfield CLI does not answer after the update — run: npm install -g --allow-scripts=@higgsfield/cli @higgsfield/cli"
elif [ "$HF_NPM_OK" = true ]; then elif [ "$HF_NPM" = ok ]; then
ok "Higgsfield CLI updated (${HF_VER:-latest})" ok "Higgsfield CLI updated (${HF_VER:-latest})"
elif [ "$HF_NPM" = skipped ]; then
info "Higgsfield CLI was not installed through npm — left to its own updater"
else else
warn "Higgsfield CLI update failed — existing binary kept" warn "Higgsfield CLI update failed — existing binary kept"
fi fi