@blundergoat/gruff-ts 0.1.0

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +87 -0
  3. package/LICENSE +21 -0
  4. package/README.md +303 -0
  5. package/SECURITY.md +45 -0
  6. package/bin/gruff-ts +25 -0
  7. package/docs/CONFIGURATION.md +220 -0
  8. package/docs/RELEASING.md +103 -0
  9. package/docs/REPORTS_AND_CI.md +156 -0
  10. package/fixtures/sample.ts +21 -0
  11. package/package.json +56 -0
  12. package/scripts/bump-version.sh +145 -0
  13. package/scripts/check.sh +4 -0
  14. package/scripts/npm-publish.sh +258 -0
  15. package/scripts/preflight-checks.sh +357 -0
  16. package/scripts/start-dev.sh +8 -0
  17. package/scripts/test-performance.sh +695 -0
  18. package/src/analyser.ts +461 -0
  19. package/src/baseline.ts +90 -0
  20. package/src/blocks.ts +687 -0
  21. package/src/class-rules.ts +326 -0
  22. package/src/cli-program.ts +326 -0
  23. package/src/cli.ts +19 -0
  24. package/src/comment-rules.ts +605 -0
  25. package/src/comment-scanner.ts +357 -0
  26. package/src/config.ts +622 -0
  27. package/src/constants.ts +4 -0
  28. package/src/context-doc-rules.ts +241 -0
  29. package/src/dashboard.ts +114 -0
  30. package/src/dead-code-rules.ts +183 -0
  31. package/src/discovery.ts +508 -0
  32. package/src/doc-rules.ts +368 -0
  33. package/src/findings-helpers.ts +108 -0
  34. package/src/findings.ts +45 -0
  35. package/src/fixture-purpose-rules.ts +334 -0
  36. package/src/fixtures/rule-catalogue-security-doctrine.ts +132 -0
  37. package/src/github-actions-rules.ts +413 -0
  38. package/src/line-rules.ts +538 -0
  39. package/src/naming-pushers.ts +191 -0
  40. package/src/project-config-rules.ts +555 -0
  41. package/src/project-rules.ts +545 -0
  42. package/src/report-renderers.ts +691 -0
  43. package/src/rule-list.ts +179 -0
  44. package/src/rules.ts +135 -0
  45. package/src/safety-rules.ts +355 -0
  46. package/src/scoring.ts +74 -0
  47. package/src/security-flow-rules.ts +112 -0
  48. package/src/sensitive-data-rules.ts +288 -0
  49. package/src/source-text.ts +722 -0
  50. package/src/test-block-rules.ts +347 -0
  51. package/src/test-fixtures.ts +621 -0
  52. package/src/text-scans.ts +193 -0
  53. package/src/types.ts +113 -0
  54. package/tsconfig.json +15 -0
@@ -0,0 +1,220 @@
1
+ # Configuration
2
+
3
+ `gruff-ts` can run without config. A config file is useful when adopting the
4
+ tool in a real project with generated files, local naming conventions, or rule
5
+ thresholds that need tuning.
6
+
7
+ ## Discovery Order
8
+
9
+ `analyse` auto-loads the first supported config file it finds in the project root:
10
+
11
+ 1. `.gruff-ts.yaml`
12
+ 2. `.gruff.json`
13
+ 3. `.gruff.yaml`
14
+ 4. `.gruff.yml`
15
+
16
+ Use an explicit path:
17
+
18
+ ```bash
19
+ gruff-ts analyse . --config .gruff-ts.yaml
20
+ ```
21
+
22
+ Skip config for a run:
23
+
24
+ ```bash
25
+ gruff-ts analyse . --no-config
26
+ ```
27
+
28
+ ## Shape
29
+
30
+ ```yaml
31
+ paths:
32
+ ignore:
33
+ - "generated/**"
34
+
35
+ allowlists:
36
+ acceptedAbbreviations:
37
+ - api
38
+ - cli
39
+ secretPreviews: []
40
+ bannedGenericNames: [process, handle, doit, run, execute, manage]
41
+ booleanPrefixes: [is, has, can, should, does, did, was, will, may, in, scan, supports, requires]
42
+ hungarianPrefixes: [str, obj, arr, bool, int, num]
43
+ placeholderNames: [foo, bar, baz, tmp, temp, thing, stuff, data, value, item]
44
+ abbreviationDenylist: [ctx, pkg, opts, fn, idx, cb]
45
+ negativeBooleanAllowed: [nostore, nofollow, noreferrer, noscript, noindex]
46
+ knownAcronyms: [url, http, https, id, xml, json, html, css, api, sql, db, io, ui, uuid, ip, tcp, udp, ast, cli, npm]
47
+
48
+ rules:
49
+ rule.id:
50
+ enabled: true
51
+ threshold: 10
52
+ severity: warning
53
+ ```
54
+
55
+ ## Ignored Paths
56
+
57
+ Recursive directory scans respect root and nested `.gitignore` files before
58
+ adding supported source and config files to a run. `paths.ignore` is an extra
59
+ project policy layer for paths that should remain out of normal scans even when
60
+ they are not ignored by Git.
61
+
62
+ `paths.ignore` accepts exact paths, prefix-style paths, and simple glob
63
+ patterns. Examples:
64
+
65
+ ```yaml
66
+ paths:
67
+ ignore:
68
+ - "generated/**"
69
+ - "fixtures/vendor/**"
70
+ - "src/generated-client.ts"
71
+ ```
72
+
73
+ Default ignored directories are matched by first path segment:
74
+
75
+ ```text
76
+ .git, .hg, .svn, .idea, .vscode, build, cache, coverage, dist,
77
+ generated, node_modules, target, tmp, vendor
78
+ ```
79
+
80
+ Use `--include-ignored` when you intentionally want to scan default ignored
81
+ directories and Git-ignored paths. Configured `paths.ignore` entries still
82
+ apply.
83
+
84
+ ## Allowlists
85
+
86
+ `allowlists.acceptedAbbreviations` lowers naming-rule noise for project-specific
87
+ short terms:
88
+
89
+ ```yaml
90
+ allowlists:
91
+ acceptedAbbreviations:
92
+ - api
93
+ - cli
94
+ - env
95
+ ```
96
+
97
+ `allowlists.secretPreviews` accepts redacted secret previews that are known false
98
+ positives:
99
+
100
+ ```yaml
101
+ allowlists:
102
+ secretPreviews:
103
+ - "abcd...wxyz (redacted, 32 chars)"
104
+ ```
105
+
106
+ Prefer fixing false positives with a narrow config entry instead of disabling an
107
+ entire sensitive-data rule.
108
+
109
+ Naming allowlists tune the 0.1.0 naming pack without changing rule ids or
110
+ fingerprints:
111
+
112
+ | Key | Used by | Default behavior |
113
+ | --- | --- | --- |
114
+ | `acceptedAbbreviations` | `naming.short-variable` | Adds short names that should not be flagged. |
115
+ | `bannedGenericNames` | `naming.generic-function` | Replaces the built-in generic function-name denylist. |
116
+ | `booleanPrefixes` | `naming.boolean-prefix` | Replaces the accepted boolean-name prefixes such as `is`, `has`, `should`, `may`, `supports`, and `requires`. |
117
+ | `hungarianPrefixes` | `naming.hungarian-notation` | Replaces type-style prefixes to flag. |
118
+ | `placeholderNames` | `naming.identifier-quality`, `naming.generic-parameter` | Replaces placeholder words; numbered suffix checks stay active. |
119
+ | `abbreviationDenylist` | `naming.abbreviation` | Replaces the opt-in abbreviation denylist. |
120
+ | `negativeBooleanAllowed` | `naming.negative-boolean` | Replaces domain terms allowed to start with `no`. |
121
+ | `knownAcronyms` | `naming.acronym-case` | Replaces acronyms checked for mixed casing. |
122
+
123
+ For replace-style allowlists, use an empty list (`[]`) when you intentionally
124
+ want no entries.
125
+
126
+ ## Rule Controls
127
+
128
+ Disable a rule:
129
+
130
+ ```yaml
131
+ rules:
132
+ docs.missing-public-doc:
133
+ enabled: false
134
+ ```
135
+
136
+ Set one threshold and one emitted severity for a metric rule:
137
+
138
+ ```yaml
139
+ rules:
140
+ complexity.cyclomatic:
141
+ threshold: 10
142
+ severity: warning
143
+ size.file-length:
144
+ threshold: 400
145
+ severity: error
146
+ ```
147
+
148
+ Rules with extra tuning knobs use `options` for those knobs while the primary
149
+ metric still uses `threshold` and `severity`:
150
+
151
+ ```yaml
152
+ rules:
153
+ design.large-module-concentration:
154
+ threshold: 55
155
+ severity: advisory
156
+ options:
157
+ minFiles: 4
158
+ minLines: 80
159
+ ```
160
+
161
+ List supported thresholds and options:
162
+
163
+ ```bash
164
+ gruff-ts list-rules
165
+ gruff-ts list-rules --format=json
166
+ ```
167
+
168
+ ## Example Project Config
169
+
170
+ ```yaml
171
+ paths:
172
+ ignore:
173
+ - "generated/**"
174
+ - "fixtures/**"
175
+
176
+ allowlists:
177
+ acceptedAbbreviations:
178
+ - api
179
+ - cli
180
+ - env
181
+ - id
182
+ secretPreviews: []
183
+
184
+ rules:
185
+ complexity.cognitive:
186
+ threshold: 15
187
+ severity: warning
188
+ complexity.cyclomatic:
189
+ threshold: 10
190
+ severity: warning
191
+ design.deep-relative-import:
192
+ threshold: 2
193
+ severity: advisory
194
+ sensitive-data.high-entropy-string:
195
+ threshold: 32
196
+ severity: error
197
+ size.function-length:
198
+ threshold: 30
199
+ severity: warning
200
+ test-quality.setup-bloat:
201
+ threshold: 12
202
+ severity: advisory
203
+ ```
204
+
205
+ ## Adoption Defaults
206
+
207
+ Two noisy-by-nature rules are present in the public catalogue but disabled by
208
+ default in this repo config:
209
+
210
+ ```yaml
211
+ rules:
212
+ docs.todo-density:
213
+ enabled: false
214
+ naming.abbreviation:
215
+ enabled: false
216
+ ```
217
+
218
+ `docs.todo-without-tracking` remains enabled because it checks whether a TODO
219
+ has owner, issue, date, ADR, or task context instead of counting raw TODO
220
+ markers.
@@ -0,0 +1,103 @@
1
+ # Releasing
2
+
3
+ This checklist prepares the public `@blundergoat/gruff-ts@0.1.0` release and
4
+ subsequent `0.1.x` patch releases.
5
+
6
+ ## Bump The Version
7
+
8
+ `scripts/bump-version.sh <semver>` updates `package.json` and
9
+ `src/constants.ts` together so the CLI `--version` output and the published
10
+ `@blundergoat/gruff-ts` package version cannot drift apart. For the initial
11
+ `0.1.0` release, the version should already be `0.1.0`; use `--check` instead
12
+ of bumping unless the release version changes.
13
+
14
+ ```bash
15
+ scripts/bump-version.sh --check
16
+ scripts/bump-version.sh 0.1.1
17
+ scripts/bump-version.sh --check
18
+ ```
19
+
20
+ The script edits files in place and does not commit or tag. After running it,
21
+ update `CHANGELOG.md` and run `npm run check`.
22
+
23
+ ## Before Publishing
24
+
25
+ - [ ] `scripts/bump-version.sh --check` reports the intended version.
26
+ - [ ] `CHANGELOG.md` has an entry for the new version with today's date.
27
+ - [ ] `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, and docs under `docs/`
28
+ reflect any user-visible changes.
29
+ - [ ] `.goat-flow/tasks/0.1/` has no unresolved release blockers beyond
30
+ explicitly accepted `human-verification-pending` milestones.
31
+ - [ ] `LICENSE` is present and `package.json` `license` field matches.
32
+ - [ ] `npm run check` passes.
33
+ - [ ] `scripts/preflight-checks.sh` passes (runs `npm run check`, a full
34
+ `gruff-ts` self-scan, and `shellcheck` on `scripts/*.sh` when
35
+ `shellcheck` is installed).
36
+ - [ ] `npm pack --dry-run` shows only publishable runtime, docs, scripts, and
37
+ metadata files.
38
+ - [ ] Local smoke scan succeeds:
39
+
40
+ ```bash
41
+ ./bin/gruff-ts
42
+ ./bin/gruff-ts analyse fixtures/sample.ts --fail-on=none
43
+ ./bin/gruff-ts summary fixtures/sample.ts --fail-on=none
44
+ ./bin/gruff-ts report fixtures/sample.ts --output /tmp/gruff-ts-report.html
45
+ ./bin/gruff-ts list-rules
46
+ ```
47
+
48
+ ## Package Review
49
+
50
+ Preview `@blundergoat/gruff-ts` package contents:
51
+
52
+ ```bash
53
+ npm pack --dry-run
54
+ ```
55
+
56
+ The package should include:
57
+
58
+ - `bin/gruff-ts`
59
+ - `src/` (all runtime `.ts` files; `src/**/*.test.ts` files are excluded by
60
+ `.npmignore`)
61
+ - `scripts/` (`bump-version.sh`, `check.sh`, `preflight-checks.sh`,
62
+ `npm-publish.sh`, `start-dev.sh`, `test-performance.sh`)
63
+ - `fixtures/sample.ts`
64
+ - `README.md`
65
+ - `CHANGELOG.md`
66
+ - `CONTRIBUTING.md`
67
+ - `SECURITY.md`
68
+ - `LICENSE`
69
+ - `docs/`
70
+ - `package.json`
71
+ - `tsconfig.json`
72
+
73
+ The package must exclude:
74
+
75
+ - `node_modules/`
76
+ - `coverage/`
77
+ - `.agents/`, `.claude/`, `.codex/`, `.github/`, and `.goat-flow/`
78
+ - `AGENTS.md` and `CLAUDE.md`
79
+ - `.gruff-ts.yaml` (this repo's local config)
80
+ - env and secret files (`.env`, `.env.*` except `.env.example`)
81
+ - local scratchpad or log artifacts
82
+ - `src/**/*.test.ts`
83
+
84
+ ## Publish
85
+
86
+ ```bash
87
+ bash scripts/npm-publish.sh
88
+ ```
89
+
90
+ The script verifies npm auth, checks version lockstep, runs
91
+ `scripts/preflight-checks.sh`, prints an `npm publish --dry-run` summary, and
92
+ prompts before publishing.
93
+
94
+ ## After Publishing
95
+
96
+ - [ ] Install `@blundergoat/gruff-ts` in a temporary project.
97
+ - [ ] Run `gruff-ts --help`.
98
+ - [ ] Run `gruff-ts analyse . --fail-on=none`.
99
+ - [ ] Run `gruff-ts summary . --fail-on=none`.
100
+ - [ ] Run `gruff-ts list-rules`.
101
+ - [ ] Verify `README.md` install instructions from a clean checkout.
102
+ - [ ] Tag the release in git (`git tag v0.1.0 && git push --tags`) and create
103
+ or update public release notes from `CHANGELOG.md`.
@@ -0,0 +1,156 @@
1
+ # Reports And CI
2
+
3
+ This guide covers output formats, exit codes, baselines, GitHub annotations, and
4
+ the local dashboard.
5
+
6
+ ## Exit Codes
7
+
8
+ `analyse` exits with:
9
+
10
+ - `0` when the scan completed and no finding met `--fail-on`.
11
+ - `1` when at least one finding met `--fail-on`.
12
+ - `2` when diagnostics were produced, such as missing inputs or parse/config
13
+ diagnostics.
14
+
15
+ Examples:
16
+
17
+ ```bash
18
+ gruff-ts analyse . --fail-on=none
19
+ gruff-ts analyse . --fail-on=error
20
+ gruff-ts analyse . --fail-on=warning
21
+ ```
22
+
23
+ ## Machine Output
24
+
25
+ Full JSON report:
26
+
27
+ ```bash
28
+ gruff-ts analyse . --format=json --fail-on=none > gruff-report.json
29
+ ```
30
+
31
+ Hotspot summary:
32
+
33
+ ```bash
34
+ gruff-ts analyse . --format=hotspot --fail-on=none > gruff-hotspots.json
35
+ ```
36
+
37
+ Human scan digest:
38
+
39
+ ```bash
40
+ gruff-ts summary . --fail-on=none
41
+ ```
42
+
43
+ The summary output includes the scanned path, elapsed duration, total findings,
44
+ per-pillar counts, top rules, and top file offenders.
45
+
46
+ Schema strings:
47
+
48
+ - `gruff.analysis.v1` for full analysis reports.
49
+ - `gruff.baseline.v1` for baselines.
50
+ - `gruff.hotspot.v1` for hotspot output.
51
+
52
+ ## GitHub Actions
53
+
54
+ Use GitHub annotation output in a workflow step:
55
+
56
+ ```bash
57
+ gruff-ts analyse . --format=github --fail-on=warning
58
+ ```
59
+
60
+ Changed-file modes:
61
+
62
+ ```bash
63
+ gruff-ts analyse . --diff=working-tree --format=github --fail-on=warning
64
+ gruff-ts analyse . --diff=staged --format=github --fail-on=warning
65
+ gruff-ts analyse . --diff=origin/main --format=github --fail-on=warning
66
+ ```
67
+
68
+ `--diff` filters findings to changed files after analysis.
69
+
70
+ For SARIF consumers, write SARIF output from `analyse` and upload the generated
71
+ file with your platform's code-scanning upload step:
72
+
73
+ ```bash
74
+ gruff-ts analyse . --format=sarif --fail-on=none > gruff.sarif
75
+ ```
76
+
77
+ For a strict security-oriented gate, bypass baselines and fail on error-severity
78
+ findings:
79
+
80
+ ```bash
81
+ gruff-ts analyse . --no-baseline --fail-on=error
82
+ ```
83
+
84
+ This is useful when an adoption baseline exists for general quality debt but
85
+ security and sensitive-data errors should still break CI.
86
+
87
+ ## Baselines
88
+
89
+ Generate an adoption baseline:
90
+
91
+ ```bash
92
+ gruff-ts analyse . --generate-baseline gruff-baseline.json --fail-on=none
93
+ ```
94
+
95
+ Apply it in CI:
96
+
97
+ ```bash
98
+ gruff-ts analyse . --baseline gruff-baseline.json --fail-on=warning
99
+ ```
100
+
101
+ Skip automatic baseline discovery:
102
+
103
+ ```bash
104
+ gruff-ts analyse . --no-baseline --fail-on=none
105
+ ```
106
+
107
+ Review baseline diffs carefully. A baseline suppresses matching fingerprints, so
108
+ unexpected additions can hide findings.
109
+
110
+ `report` intentionally renders raw scan results and does not accept a
111
+ `--baseline` option. Use `analyse` for baseline-aware machine output.
112
+
113
+ ## HTML Reports
114
+
115
+ Write a dark self-contained report:
116
+
117
+ ```bash
118
+ gruff-ts report . --output gruff-report.html
119
+ ```
120
+
121
+ Write report JSON:
122
+
123
+ ```bash
124
+ gruff-ts report . --format=json --output gruff-report.json
125
+ ```
126
+
127
+ `report` defaults to `--fail-on none`, making it suitable for local inspection
128
+ and scheduled reporting.
129
+
130
+ ## Dashboard
131
+
132
+ Start the dashboard:
133
+
134
+ ```bash
135
+ gruff-ts dashboard --host 127.0.0.1 --port 8767 --project-root .
136
+ ```
137
+
138
+ The dashboard serves:
139
+
140
+ - `/` - iframe shell and controls panel.
141
+ - `/health` - plain `ok`.
142
+ - `/scan?projectRoot=<path>&path=<path>` - report HTML for the selected scan.
143
+
144
+ Keep the dashboard on loopback unless the network is trusted. The scan endpoint
145
+ accepts filesystem paths through request parameters.
146
+
147
+ ## Score History
148
+
149
+ Append score history to a JSON file:
150
+
151
+ ```bash
152
+ gruff-ts analyse . --history-file .gruff-history.json --fail-on=none
153
+ ```
154
+
155
+ History files are local artifacts. Commit them only if your project explicitly
156
+ wants trend data in version control.
@@ -0,0 +1,21 @@
1
+ export class SampleAnalyzer {
2
+ public name = "demo";
3
+ private secretUrl = "mysql://demo:password123@example.test/app";
4
+
5
+ public process(a: boolean, b: string[], c: string, d: string, e: string, f: string): void {
6
+ if (a) {
7
+ for (const item of b) {
8
+ if (item === c) {
9
+ eval(item);
10
+ }
11
+ }
12
+ }
13
+
14
+ const apiKey = "AKIA1111111111111111";
15
+ console.log(apiKey, this.secretUrl, d, e, f);
16
+ }
17
+ }
18
+
19
+ test("sleeps without assertion", async () => {
20
+ await new Promise((resolve) => setTimeout(resolve, 1));
21
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@blundergoat/gruff-ts",
3
+ "version": "0.1.0",
4
+ "description": "Static analyzer for TypeScript and JavaScript projects - 121 rules across 11 quality pillars, SARIF output, baselines, and a local dashboard.",
5
+ "license": "MIT",
6
+ "author": "Matthew Hansen (https://www.blundergoat.com/about)",
7
+ "homepage": "https://github.com/blundergoat/gruff-ts#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/blundergoat/gruff-ts.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/blundergoat/gruff-ts/issues"
14
+ },
15
+ "keywords": [
16
+ "typescript",
17
+ "javascript",
18
+ "static-analysis",
19
+ "code-quality",
20
+ "linter",
21
+ "analyzer",
22
+ "cli",
23
+ "sarif",
24
+ "baseline",
25
+ "fingerprint",
26
+ "complexity",
27
+ "dead-code",
28
+ "security",
29
+ "naming",
30
+ "dashboard",
31
+ "ci",
32
+ "github-actions",
33
+ "nodejs"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "type": "module",
39
+ "bin": {
40
+ "gruff-ts": "bin/gruff-ts"
41
+ },
42
+ "scripts": {
43
+ "check": "tsc --noEmit && npm test",
44
+ "test": "node --import tsx --test src/**/*.test.ts",
45
+ "start-dev": "tsx src/cli.ts dashboard"
46
+ },
47
+ "dependencies": {
48
+ "commander": "^14.0.2",
49
+ "tsx": "^4.21.0"
50
+ },
51
+ "devDependencies": {
52
+ "@blundergoat/goat-flow": "^1.6.4",
53
+ "@types/node": "^25.0.0",
54
+ "typescript": "^5.9.3"
55
+ }
56
+ }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Bump gruff-ts to a new semver in package.json and src/constants.ts in one step.
5
+ # The CLI surfaces VERSION from src/constants.ts; package.json drives `npm publish`.
6
+ # Keeping them in lockstep is a release invariant.
7
+
8
+ usage() {
9
+ cat <<'USAGE'
10
+ Usage:
11
+ scripts/bump-version.sh <new-version>
12
+ scripts/bump-version.sh --check
13
+
14
+ Arguments:
15
+ <new-version> Target semver, e.g. 0.1.1, 0.2.0, 1.0.0-rc.1.
16
+
17
+ Options:
18
+ --check Verify package.json and src/constants.ts already agree.
19
+ --help, -h Show this help.
20
+
21
+ Notes:
22
+ Edits files in place. Does not commit or tag. Run `npm run check` afterwards.
23
+ USAGE
24
+ }
25
+
26
+ die() {
27
+ printf 'bump-version: %s\n' "$*" >&2
28
+ exit 1
29
+ }
30
+
31
+ repo_root() {
32
+ local script_dir
33
+ script_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
34
+ CDPATH='' cd -- "$script_dir/.." && pwd
35
+ }
36
+
37
+ read_package_version() {
38
+ awk -F'"' '/^[[:space:]]*"version"[[:space:]]*:/ { print $4; exit }' package.json
39
+ }
40
+
41
+ read_constants_version() {
42
+ awk -F'"' '/^const VERSION = "/ { print $2; exit }' src/constants.ts
43
+ }
44
+
45
+ write_package_version() {
46
+ local next_version="$1"
47
+ awk -v target="$next_version" '
48
+ BEGIN { done = 0 }
49
+ /^[[:space:]]*"version"[[:space:]]*:/ && !done {
50
+ sub(/"version"[[:space:]]*:[[:space:]]*"[^"]+"/, "\"version\": \"" target "\"")
51
+ done = 1
52
+ }
53
+ { print }
54
+ ' package.json > package.json.tmp
55
+ mv package.json.tmp package.json
56
+ }
57
+
58
+ write_constants_version() {
59
+ local next_version="$1"
60
+ awk -v target="$next_version" '
61
+ BEGIN { done = 0 }
62
+ /^const VERSION = "/ && !done {
63
+ print "const VERSION = \"" target "\";"
64
+ done = 1
65
+ next
66
+ }
67
+ { print }
68
+ ' src/constants.ts > src/constants.ts.tmp
69
+ mv src/constants.ts.tmp src/constants.ts
70
+ }
71
+
72
+ validate_semver() {
73
+ local value="$1"
74
+ # Standard semver: MAJOR.MINOR.PATCH with optional prerelease/build metadata.
75
+ local pattern='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
76
+ [[ "$value" =~ $pattern ]] || die "not a valid semver: $value"
77
+ }
78
+
79
+ main() {
80
+ if [[ "$#" -eq 0 ]]; then
81
+ usage >&2
82
+ exit 2
83
+ fi
84
+
85
+ cd "$(repo_root)"
86
+ [[ -f package.json ]] || die "package.json not found"
87
+ [[ -f src/constants.ts ]] || die "src/constants.ts not found"
88
+
89
+ case "$1" in
90
+ -h|--help)
91
+ usage
92
+ exit 0
93
+ ;;
94
+ --check)
95
+ local pkg const_
96
+ pkg="$(read_package_version)" || die "failed to read package.json version"
97
+ const_="$(read_constants_version)" || die "failed to read src/constants.ts VERSION"
98
+ [[ -n "$pkg" ]] || die "package.json has no \"version\" field"
99
+ [[ -n "$const_" ]] || die "src/constants.ts has no VERSION constant"
100
+ if [[ "$pkg" != "$const_" ]]; then
101
+ printf 'package.json (%s) and src/constants.ts (%s) disagree\n' "$pkg" "$const_" >&2
102
+ exit 1
103
+ fi
104
+ printf 'package.json and src/constants.ts agree on %s\n' "$pkg"
105
+ exit 0
106
+ ;;
107
+ esac
108
+
109
+ local next="$1"
110
+ validate_semver "$next"
111
+
112
+ local current
113
+ current="$(read_package_version)" || die "failed to read package.json version"
114
+ [[ -n "$current" ]] || die "package.json has no \"version\" field"
115
+
116
+ local current_const
117
+ current_const="$(read_constants_version)" || die "failed to read src/constants.ts VERSION"
118
+ [[ -n "$current_const" ]] || die "src/constants.ts has no VERSION constant"
119
+
120
+ if [[ "$current" != "$current_const" ]]; then
121
+ die "current versions diverge: package.json=$current src/constants.ts=$current_const (resolve manually first)"
122
+ fi
123
+
124
+ if [[ "$current" == "$next" ]]; then
125
+ printf 'already at %s; nothing to do\n' "$next"
126
+ exit 0
127
+ fi
128
+
129
+ write_package_version "$next"
130
+ write_constants_version "$next"
131
+
132
+ local check_pkg check_const
133
+ check_pkg="$(read_package_version)"
134
+ check_const="$(read_constants_version)"
135
+ [[ "$check_pkg" == "$next" ]] || die "package.json did not update cleanly (read back: ${check_pkg:-empty})"
136
+ [[ "$check_const" == "$next" ]] || die "src/constants.ts did not update cleanly (read back: ${check_const:-empty})"
137
+
138
+ printf 'bumped %s -> %s\n' "$current" "$next"
139
+ printf ' package.json\n'
140
+ printf ' src/constants.ts\n'
141
+ # shellcheck disable=SC2016
142
+ printf 'next: update CHANGELOG.md and run `npm run check`\n'
143
+ }
144
+
145
+ main "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ npm run check