diff --git a/CHANGELOG.md b/CHANGELOG.md index 60287f5..9060b84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -401,7 +401,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). `verification-before-completion` to the verifier gates. ### 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 -g`, still pass. - **Ten secret-reader deny rules added**: `sed`, `awk`, `cut`, `tr`, `sort`, `uniq`, `diff`, `od`, `xxd`, `strings` against `.env*`. Six of those tools sat in `permissions.allow`, so reading a `.env` through diff --git a/README.md b/README.md index 8ae41bd..28ac719 100644 --- a/README.md +++ b/README.md @@ -380,11 +380,12 @@ bash lib/toggle-external.sh disable higgsfield `higgsfield` links a fixed list of seven media skills: generate, soul-id, product-photoshoot, brandkit, marketplace-cards, video-explainer and youtube-thumbnail. The list is `HIGGSFIELD_MEDIA_SKILLS` in -`lib/toggle-external.sh`. A skill that upstream adds later is synced and -reported, and stays unlinked until it is added there. +`lib/toggle-external.sh`. A skill that upstream adds later is synced, and +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 -the design stack (assets, references), and `higgsfield website +`higgsfield-websites` is kept apart. It helps with landing pages inside the +design stack (assets, references), and `higgsfield website create|deploy|publish` stays unused. Claude enables either toggle itself on an explicit ask (Skill routing in `CLAUDE.global.md`) and checks the price 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 the off-by-default state. +After the first login, select a workspace once: `higgsfield workspace list`, +then `higgsfield workspace set `. 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 script back, `higgsfield` exists on PATH and fails at once; reinstall with `npm install -g --allow-scripts=@higgsfield/cli @higgsfield/cli`. diff --git a/lib/higgsfield-skills.sh b/lib/higgsfield-skills.sh index 51de21c..6721a2e 100644 --- a/lib/higgsfield-skills.sh +++ b/lib/higgsfield-skills.sh @@ -59,15 +59,19 @@ higgsfield_sync_skills() { } # _higgsfield_probe -# Run `higgsfield ` silently, 15 s at most when `timeout` exists. The -# CLI is 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. +# Run `higgsfield ` silently, 15 s at most when a timeout tool exists +# (`timeout`, or `gtimeout` from Homebrew coreutils on macOS). The CLI is +# 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() { - if command -v timeout >/dev/null 2>&1; then - timeout 15 higgsfield "$@" /dev/null 2>&1 - else - higgsfield "$@" /dev/null 2>&1 - fi + local tool + for tool in timeout gtimeout; do + if command -v "$tool" >/dev/null 2>&1; then + "$tool" 15 higgsfield "$@" /dev/null 2>&1 + return + fi + done + higgsfield "$@" /dev/null 2>&1 } # higgsfield_cli_ok — 0 when the binary answers. `command -v` alone only diff --git a/lib/tests/higgsfield.test.sh b/lib/tests/higgsfield.test.sh index 4751d4c..f6ab348 100644 --- a/lib/tests/higgsfield.test.sh +++ b/lib/tests/higgsfield.test.sh @@ -110,7 +110,8 @@ probe() { # ── sync ──────────────────────────────────────────────────── UP="$WORK/upstream"; mk_upstream "$UP" -R1="$WORK/r1"; mkdir -p "$R1/skills" "$R1/skills-disabled" +# The repo path carries a space on purpose: every expansion must be quoted. +R1="$WORK/r 1"; mkdir -p "$R1/skills" "$R1/skills-disabled" EXT="$R1/skills-external" expect fixture "$(yn test -f "$UP/.git/HEAD")" yes @@ -162,6 +163,12 @@ expect shim-only \ "$(FAKE_HF_BINARY=missing probe "$BIN:$PATH" higgsfield_cli_ok)" "rc=1" expect no-cli "$(probe "$CLEAN" higgsfield_cli_ok)" "rc=127" 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 # ── toggle ────────────────────────────────────────────────── @@ -216,7 +223,7 @@ verdict ENABLE_PACK_EXCLUDES_WEBSITES # The media pack is an allowlist: a synced skill nobody listed is reported, # never linked; neither is a listed name whose directory holds no SKILL.md. -F8="$WORK/f8"; mk_toggle_fx "$F8" "${PACK[@]}" higgsfield-newcomer +F8="$WORK/f 8"; mk_toggle_fx "$F8" "${PACK[@]}" higgsfield-newcomer mkdir -p "$F8/skills-external/higgsfield-brandkit" \ "$F8/skills-external/higgsfield-noskill" out="$(tog "$F8" enable higgsfield)"; rc=$? @@ -227,6 +234,11 @@ expect brandkit-off "$(yn test -e "$F8/skills/higgsfield-brandkit")" no expect_has reported "$out" "higgsfield-newcomer" expect_not noskill-quiet "$out" "higgsfield-noskill" 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 F3="$WORK/f3"; mk_toggle_fx "$F3" "${PACK[@]}" diff --git a/lib/toggle-external.sh b/lib/toggle-external.sh index 185a62f..a87ba60 100755 --- a/lib/toggle-external.sh +++ b/lib/toggle-external.sh @@ -113,17 +113,21 @@ pack_skills() { esac } -# bounded — run a CLI probe silently, 15 s at most when `timeout` -# exists: a closed-source binary must never hang a toggle, and what it -# prints (a token) must never reach the terminal. Twin of -# _higgsfield_probe in lib/higgsfield-skills.sh, kept here because this -# script takes no extra `source` (the fixture suites copy it alone). +# bounded — run a CLI probe silently, 15 s at most when a timeout +# tool exists (`timeout`, or `gtimeout` from Homebrew coreutils on macOS): +# a closed-source binary must never hang a toggle, and what it prints (a +# token) must never reach the terminal. Twin of _higgsfield_probe in +# lib/higgsfield-skills.sh, kept here because this script takes no extra +# `source` (the fixture suites copy it alone). bounded() { - if command -v timeout >/dev/null 2>&1; then - timeout 15 "$@" /dev/null 2>&1 - else - "$@" /dev/null 2>&1 - fi + local tool + for tool in timeout gtimeout; do + if command -v "$tool" >/dev/null 2>&1; then + "$tool" 15 "$@" /dev/null 2>&1 + return + fi + done + "$@" /dev/null 2>&1 } # Post-enable notes for a pack. Its skills shell out to a CLI: without it @@ -322,6 +326,9 @@ enable_tool() { return 1 fi 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 fi ok "$tool enabled ($((restored + linked)) skills: $restored restored, $linked linked)" diff --git a/update-all.sh b/update-all.sh index 3ea25f5..87fb47d 100644 --- a/update-all.sh +++ b/update-all.sh @@ -489,14 +489,21 @@ print(d.get('higgsfield',{}).get('version','latest')) HF_PKG="@higgsfield/cli@latest" [ -n "$HF_VER" ] && [ "$HF_VER" != "latest" ] \ && HF_PKG="@higgsfield/cli@${HF_VER}" - HF_NPM_OK=true - npm install -g "$HF_PKG" 2>/dev/null || HF_NPM_OK=false + # npm updates only a copy npm installed: a CLI that came from Homebrew + # 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 # postinstall script exits 0 and leaves the shim with no binary behind it. if ! higgsfield_cli_ok; then 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})" + elif [ "$HF_NPM" = skipped ]; then + info "Higgsfield CLI was not installed through npm — left to its own updater" else warn "Higgsfield CLI update failed — existing binary kept" fi