@biffo/cli 0.228.5 → 0.228.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,13 +4,114 @@
4
4
  root=$(git rev-parse --show-toplevel) || exit 0
5
5
  cd "$root" || exit 0
6
6
 
7
- # commitlint only where it is configured. The release derives its version bump
8
- # from the squash-merge subject (ADR-0006), so this matters most in repos that
9
- # cut releases -- but a repo without the config must not be blocked by it.
10
- if [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; then
7
+ # The authoritative type list is commitlint.config.js's `type-enum` (AGENTS.md
8
+ # §3). Mirrored here for the fallback below, and `cli/src/lib/commit-msg-fallback.test.ts`
9
+ # asserts the two never drift apart -- a second copy of a list is a second copy
10
+ # of a decision, and this one is load-bearing: ADR-0006 derives the release bump
11
+ # from the subject.
12
+ BIFFO_COMMIT_TYPES='feat fix chore docs test infra security refactor perf ci'
13
+
14
+ # Subjects commitlint ignores by default (`defaultIgnores`): git's own generated
15
+ # merge/revert subjects and the markers rebase writes. The fallback must ignore
16
+ # them too, or it blocks ordinary git operations that commitlint would allow.
17
+ _is_ignored_subject() {
18
+ case "$1" in
19
+ "Merge "* | "Revert "* | "Reverts "* | "fixup! "* | "squash! "* | "amend! "*) return 0 ;;
20
+ *) return 1 ;;
21
+ esac
22
+ }
23
+
24
+ _fallback_explain() {
25
+ echo "commit-msg: $2" >&2
26
+ echo " subject: $1" >&2
27
+ echo " expected: type(scope): summary e.g. feat(api): add run history endpoint" >&2
28
+ echo " types: $BIFFO_COMMIT_TYPES" >&2
29
+ echo " (checked without commitlint -- this repo has no runnable config; see local-gates.md)" >&2
30
+ }
31
+
32
+ # A dependency-free Conventional Commits check, for repos where commitlint
33
+ # cannot run.
34
+ #
35
+ # ## Why this exists
36
+ #
37
+ # This hook used to print
38
+ #
39
+ # commit-msg: no commitlint config -- subject not checked
40
+ #
41
+ # and exit 0. That reads like information; it is a gate abstaining. It abstained
42
+ # in 13 of 17 estate repos (#1193): it looked for a root commitlint config, and
43
+ # ran it with `pnpm exec`, which needs a root package.json. Most satellites have
44
+ # neither -- tabsii-crm's frontend is its own pnpm workspace under
45
+ # apps/frontend/, so the repo root holds no package.json at all. AGENTS.md §3
46
+ # was binding everywhere and enforced in three places.
47
+ #
48
+ # Shipping a config would not have fixed it: a config with no resolvable
49
+ # commitlint binary still cannot run. Resolving the toolchain from whichever
50
+ # workspace happens to hold it is fragile across six different layouts. A check
51
+ # needing nothing but `sh` works in all of them and cannot abstain.
52
+ #
53
+ # ## What it deliberately does NOT check
54
+ #
55
+ # Only the load-bearing rules: the `type(scope): subject` shape, the type list,
56
+ # and the 100-character header limit. `subject-case` and the
57
+ # `body-no-ci-skip-token` plugin stay commitlint's job where commitlint runs. A
58
+ # fallback STRICTER than the tool it stands in for would reject commits CI
59
+ # accepts, and a local gate that is wrong gets bypassed on reflex.
60
+ _fallback_check_subject() {
61
+ _subject=$(sed -n '1p' "$1")
62
+
63
+ # Empty or comment-only: git aborts the commit itself, so there is nothing to
64
+ # judge and refusing here would just produce a confusing second error.
65
+ case "$_subject" in '' | '#'*) return 0 ;; esac
66
+
67
+ _is_ignored_subject "$_subject" && return 0
68
+
69
+ # `${#var}` counts characters in POSIX sh. Matches header-max-length.
70
+ if [ "${#_subject}" -gt 100 ]; then
71
+ _fallback_explain "$_subject" "subject is ${#_subject} characters, limit is 100"
72
+ return 1
73
+ fi
74
+
75
+ case "$_subject" in
76
+ *': '*) ;;
77
+ *)
78
+ _fallback_explain "$_subject" 'no ": " separator'
79
+ return 1
80
+ ;;
81
+ esac
82
+
83
+ _head=${_subject%%': '*} # everything before the first ": "
84
+ _summary=${_subject#*': '}
85
+
86
+ if [ -z "$_summary" ]; then
87
+ _fallback_explain "$_subject" 'the summary after ": " is empty'
88
+ return 1
89
+ fi
90
+
91
+ # Strip an optional "(scope)" and an optional breaking-change "!", in either
92
+ # order: both `feat!: x` and `feat(api)!: x` are valid Conventional Commits.
93
+ _type=${_head%%'('*} # drop "(scope)" when present
94
+ _type=${_type%'!'} # drop a trailing "!"
95
+
96
+ for _known in $BIFFO_COMMIT_TYPES; do
97
+ if [ "$_type" = "$_known" ]; then
98
+ return 0
99
+ fi
100
+ done
101
+
102
+ _fallback_explain "$_subject" "unknown type \"$_type\""
103
+ return 1
104
+ }
105
+
106
+ # commitlint where it is configured AND runnable; the built-in check everywhere
107
+ # else. Both conditions matter: a config with no root package.json cannot be run
108
+ # by `pnpm exec` from here, and taking that branch anyway is what produced the
109
+ # silent abstention this replaces.
110
+ if [ -f package.json ] &&
111
+ { [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; }; then
11
112
  pnpm exec commitlint --edit "$1" || exit 1
12
113
  else
13
- echo "commit-msg: no commitlint config — subject not checked (see local-gates.md)" >&2
114
+ _fallback_check_subject "$1" || exit 1
14
115
  fi
15
116
 
16
117
  # Refuse commits that edit template-owned paths (#370). Inert in the template
@@ -9,13 +9,31 @@
9
9
  root=$(git rev-parse --show-toplevel) || exit 0
10
10
  cd "$root" || exit 0
11
11
 
12
+ # The two abstention messages below are deliberately phrased as abstentions
13
+ # rather than as status (#1193). The previous wording -- "nothing staged-file-
14
+ # level to run", "skipping" -- read like the hook working, which is why 13 of 17
15
+ # repos ran a formatting gate that formats nothing without anyone noticing.
16
+ #
17
+ # Unlike commit-msg, which now has a built-in fallback because Conventional
18
+ # Commits is a RULE that can be checked with nothing but `sh`, this hook runs a
19
+ # TOOLCHAIN. Checked 2026-08-03: no satellite carries `lint-staged` anywhere --
20
+ # not at the root, not in any workspace -- so there is genuinely nothing to
21
+ # resolve and nothing to fall back to. Making it fatal would block all work in
22
+ # those repos, which is how a safety measure gets ripped out.
23
+ #
24
+ # So it still exits 0. It just no longer implies it checked anything, and it
25
+ # says what would make it real.
12
26
  if [ ! -f package.json ]; then
13
- echo "pre-commit: no package.json nothing staged-file-level to run" >&2
27
+ echo "pre-commit: ABSTAINED — no root package.json, so lint-staged cannot run here." >&2
28
+ echo " Staged files were NOT linted or formatted. CI still checks them." >&2
29
+ echo " To make this real: add lint-staged to a root package.json (see local-gates.md)." >&2
14
30
  exit 0
15
31
  fi
16
32
  if ! node -e "const p=require('./package.json');process.exit(p['lint-staged']||p.config?.['lint-staged']?0:1)" 2>/dev/null \
17
- && [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
18
- echo "pre-commit: no lint-staged config — skipping (see local-gates.md)" >&2
33
+ && [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
34
+ echo "pre-commit: ABSTAINED — package.json exists but declares no lint-staged config." >&2
35
+ echo " Staged files were NOT linted or formatted. CI still checks them." >&2
36
+ echo " To make this real: add a lint-staged config (see local-gates.md)." >&2
19
37
  exit 0
20
38
  fi
21
39
  exec pnpm exec lint-staged
@@ -4,13 +4,114 @@
4
4
  root=$(git rev-parse --show-toplevel) || exit 0
5
5
  cd "$root" || exit 0
6
6
 
7
- # commitlint only where it is configured. The release derives its version bump
8
- # from the squash-merge subject (ADR-0006), so this matters most in repos that
9
- # cut releases -- but a repo without the config must not be blocked by it.
10
- if [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; then
7
+ # The authoritative type list is commitlint.config.js's `type-enum` (AGENTS.md
8
+ # §3). Mirrored here for the fallback below, and `cli/src/lib/commit-msg-fallback.test.ts`
9
+ # asserts the two never drift apart -- a second copy of a list is a second copy
10
+ # of a decision, and this one is load-bearing: ADR-0006 derives the release bump
11
+ # from the subject.
12
+ BIFFO_COMMIT_TYPES='feat fix chore docs test infra security refactor perf ci'
13
+
14
+ # Subjects commitlint ignores by default (`defaultIgnores`): git's own generated
15
+ # merge/revert subjects and the markers rebase writes. The fallback must ignore
16
+ # them too, or it blocks ordinary git operations that commitlint would allow.
17
+ _is_ignored_subject() {
18
+ case "$1" in
19
+ "Merge "* | "Revert "* | "Reverts "* | "fixup! "* | "squash! "* | "amend! "*) return 0 ;;
20
+ *) return 1 ;;
21
+ esac
22
+ }
23
+
24
+ _fallback_explain() {
25
+ echo "commit-msg: $2" >&2
26
+ echo " subject: $1" >&2
27
+ echo " expected: type(scope): summary e.g. feat(api): add run history endpoint" >&2
28
+ echo " types: $BIFFO_COMMIT_TYPES" >&2
29
+ echo " (checked without commitlint -- this repo has no runnable config; see local-gates.md)" >&2
30
+ }
31
+
32
+ # A dependency-free Conventional Commits check, for repos where commitlint
33
+ # cannot run.
34
+ #
35
+ # ## Why this exists
36
+ #
37
+ # This hook used to print
38
+ #
39
+ # commit-msg: no commitlint config -- subject not checked
40
+ #
41
+ # and exit 0. That reads like information; it is a gate abstaining. It abstained
42
+ # in 13 of 17 estate repos (#1193): it looked for a root commitlint config, and
43
+ # ran it with `pnpm exec`, which needs a root package.json. Most satellites have
44
+ # neither -- tabsii-crm's frontend is its own pnpm workspace under
45
+ # apps/frontend/, so the repo root holds no package.json at all. AGENTS.md §3
46
+ # was binding everywhere and enforced in three places.
47
+ #
48
+ # Shipping a config would not have fixed it: a config with no resolvable
49
+ # commitlint binary still cannot run. Resolving the toolchain from whichever
50
+ # workspace happens to hold it is fragile across six different layouts. A check
51
+ # needing nothing but `sh` works in all of them and cannot abstain.
52
+ #
53
+ # ## What it deliberately does NOT check
54
+ #
55
+ # Only the load-bearing rules: the `type(scope): subject` shape, the type list,
56
+ # and the 100-character header limit. `subject-case` and the
57
+ # `body-no-ci-skip-token` plugin stay commitlint's job where commitlint runs. A
58
+ # fallback STRICTER than the tool it stands in for would reject commits CI
59
+ # accepts, and a local gate that is wrong gets bypassed on reflex.
60
+ _fallback_check_subject() {
61
+ _subject=$(sed -n '1p' "$1")
62
+
63
+ # Empty or comment-only: git aborts the commit itself, so there is nothing to
64
+ # judge and refusing here would just produce a confusing second error.
65
+ case "$_subject" in '' | '#'*) return 0 ;; esac
66
+
67
+ _is_ignored_subject "$_subject" && return 0
68
+
69
+ # `${#var}` counts characters in POSIX sh. Matches header-max-length.
70
+ if [ "${#_subject}" -gt 100 ]; then
71
+ _fallback_explain "$_subject" "subject is ${#_subject} characters, limit is 100"
72
+ return 1
73
+ fi
74
+
75
+ case "$_subject" in
76
+ *': '*) ;;
77
+ *)
78
+ _fallback_explain "$_subject" 'no ": " separator'
79
+ return 1
80
+ ;;
81
+ esac
82
+
83
+ _head=${_subject%%': '*} # everything before the first ": "
84
+ _summary=${_subject#*': '}
85
+
86
+ if [ -z "$_summary" ]; then
87
+ _fallback_explain "$_subject" 'the summary after ": " is empty'
88
+ return 1
89
+ fi
90
+
91
+ # Strip an optional "(scope)" and an optional breaking-change "!", in either
92
+ # order: both `feat!: x` and `feat(api)!: x` are valid Conventional Commits.
93
+ _type=${_head%%'('*} # drop "(scope)" when present
94
+ _type=${_type%'!'} # drop a trailing "!"
95
+
96
+ for _known in $BIFFO_COMMIT_TYPES; do
97
+ if [ "$_type" = "$_known" ]; then
98
+ return 0
99
+ fi
100
+ done
101
+
102
+ _fallback_explain "$_subject" "unknown type \"$_type\""
103
+ return 1
104
+ }
105
+
106
+ # commitlint where it is configured AND runnable; the built-in check everywhere
107
+ # else. Both conditions matter: a config with no root package.json cannot be run
108
+ # by `pnpm exec` from here, and taking that branch anyway is what produced the
109
+ # silent abstention this replaces.
110
+ if [ -f package.json ] &&
111
+ { [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; }; then
11
112
  pnpm exec commitlint --edit "$1" || exit 1
12
113
  else
13
- echo "commit-msg: no commitlint config — subject not checked (see local-gates.md)" >&2
114
+ _fallback_check_subject "$1" || exit 1
14
115
  fi
15
116
 
16
117
  # Refuse commits that edit template-owned paths (#370). Inert in the template
@@ -9,13 +9,31 @@
9
9
  root=$(git rev-parse --show-toplevel) || exit 0
10
10
  cd "$root" || exit 0
11
11
 
12
+ # The two abstention messages below are deliberately phrased as abstentions
13
+ # rather than as status (#1193). The previous wording -- "nothing staged-file-
14
+ # level to run", "skipping" -- read like the hook working, which is why 13 of 17
15
+ # repos ran a formatting gate that formats nothing without anyone noticing.
16
+ #
17
+ # Unlike commit-msg, which now has a built-in fallback because Conventional
18
+ # Commits is a RULE that can be checked with nothing but `sh`, this hook runs a
19
+ # TOOLCHAIN. Checked 2026-08-03: no satellite carries `lint-staged` anywhere --
20
+ # not at the root, not in any workspace -- so there is genuinely nothing to
21
+ # resolve and nothing to fall back to. Making it fatal would block all work in
22
+ # those repos, which is how a safety measure gets ripped out.
23
+ #
24
+ # So it still exits 0. It just no longer implies it checked anything, and it
25
+ # says what would make it real.
12
26
  if [ ! -f package.json ]; then
13
- echo "pre-commit: no package.json nothing staged-file-level to run" >&2
27
+ echo "pre-commit: ABSTAINED — no root package.json, so lint-staged cannot run here." >&2
28
+ echo " Staged files were NOT linted or formatted. CI still checks them." >&2
29
+ echo " To make this real: add lint-staged to a root package.json (see local-gates.md)." >&2
14
30
  exit 0
15
31
  fi
16
32
  if ! node -e "const p=require('./package.json');process.exit(p['lint-staged']||p.config?.['lint-staged']?0:1)" 2>/dev/null \
17
- && [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
18
- echo "pre-commit: no lint-staged config — skipping (see local-gates.md)" >&2
33
+ && [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
34
+ echo "pre-commit: ABSTAINED — package.json exists but declares no lint-staged config." >&2
35
+ echo " Staged files were NOT linted or formatted. CI still checks them." >&2
36
+ echo " To make this real: add a lint-staged config (see local-gates.md)." >&2
19
37
  exit 0
20
38
  fi
21
39
  exec pnpm exec lint-staged
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.228.5",
3
+ "version": "0.228.7",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",