@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,258 @@
1
+ #!/usr/bin/env bash
2
+ # npm-publish.sh
3
+ #
4
+ # Purpose:
5
+ # Release gruff-ts to npm with local preflight checks and human confirmation.
6
+ #
7
+ # Usage:
8
+ # bash scripts/npm-publish.sh
9
+ #
10
+ # Behavior:
11
+ # 1) reads package.json name/version
12
+ # 2) verifies npm authentication
13
+ # 3) checks version lockstep and runs the release preflight gate
14
+ # 4) prints a dry-run publish summary
15
+ # 5) asks for manual confirmation before `npm publish`
16
+ #
17
+ # Exit:
18
+ # 0 if published or explicitly aborted, non-zero on failed auth/preflight/publish.
19
+ #
20
+ # Requirements:
21
+ # - node, npm
22
+ # - npm publish authentication:
23
+ # npm package publishing requires either account 2FA for the publish
24
+ # prompt or a granular access token with Bypass 2FA enabled.
25
+ #
26
+ # Preferred token setup for this script:
27
+ # 1. In npmjs.com, open Account -> Access Tokens.
28
+ # 2. Generate a Granular Access Token.
29
+ # 3. Grant read/write access to gruff-ts, or to all packages for the
30
+ # publishing account.
31
+ # 4. Enable Bypass 2FA for write actions.
32
+ # 5. Either store it in your npm user config:
33
+ # npm config set //registry.npmjs.org/:_authToken=npm_...
34
+ # bash scripts/npm-publish.sh
35
+ #
36
+ # Or keep it out of ~/.npmrc and pass it for this shell only:
37
+ # export NPM_TOKEN="npm_..."
38
+ # bash scripts/npm-publish.sh
39
+ #
40
+ # NODE_AUTH_TOKEN is also accepted for the temporary-token path. The
41
+ # script writes env tokens to a temporary npm config file and removes it
42
+ # on exit. Do not commit an .npmrc containing a real token.
43
+ set -euo pipefail
44
+
45
+ REGISTRY_URL="https://registry.npmjs.org/"
46
+ AUTH_SOURCE=""
47
+ TEMP_NPMRC=""
48
+ PACKAGE_NAME=""
49
+
50
+ usage() {
51
+ cat <<'USAGE'
52
+ Usage:
53
+ bash scripts/npm-publish.sh
54
+ bash scripts/npm-publish.sh --help
55
+
56
+ Publishes the current package version to npm after:
57
+ - npm auth/token validation
58
+ - scripts/bump-version.sh --check
59
+ - scripts/preflight-checks.sh
60
+ - npm publish --dry-run
61
+
62
+ The final publish still requires an interactive y/N confirmation.
63
+ USAGE
64
+ }
65
+
66
+ die() {
67
+ printf 'npm-publish: %s\n' "$*" >&2
68
+ exit 1
69
+ }
70
+
71
+ repo_root() {
72
+ local script_dir
73
+ script_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
74
+ CDPATH='' cd -- "$script_dir/.." && pwd
75
+ }
76
+
77
+ read_package_name() {
78
+ node -p "require('./package.json').name"
79
+ }
80
+
81
+ read_package_version() {
82
+ node -p "require('./package.json').version"
83
+ }
84
+
85
+ cleanup() {
86
+ if [[ -n "$TEMP_NPMRC" && -f "$TEMP_NPMRC" ]]; then
87
+ rm -f -- "$TEMP_NPMRC"
88
+ fi
89
+ }
90
+ trap cleanup EXIT
91
+
92
+ print_token_instructions() {
93
+ local reason="$1"
94
+
95
+ printf 'Error: %s\n' "$reason" >&2
96
+ cat >&2 <<EOF
97
+
98
+ Publish token setup:
99
+ 1. Go to npmjs.com -> Account -> Access Tokens.
100
+ 2. Generate a Granular Access Token.
101
+ 3. Grant read/write access to ${PACKAGE_NAME}, or to all packages for the
102
+ publishing account.
103
+ 4. Enable Bypass 2FA for write actions.
104
+ 5. Store the token in your npm user config:
105
+
106
+ npm config set //registry.npmjs.org/:_authToken=npm_...
107
+ bash scripts/npm-publish.sh
108
+
109
+ This is simple, but it persists the token in ~/.npmrc.
110
+
111
+ To avoid storing the token, pass it for this shell only:
112
+
113
+ export NPM_TOKEN="npm_..."
114
+ bash scripts/npm-publish.sh
115
+
116
+ NODE_AUTH_TOKEN works too:
117
+
118
+ export NODE_AUTH_TOKEN="npm_..."
119
+ bash scripts/npm-publish.sh
120
+
121
+ Do not commit an .npmrc containing a real token.
122
+ EOF
123
+ }
124
+
125
+ configure_token_from_env() {
126
+ local token_source=""
127
+ local token_value=""
128
+
129
+ if [[ -n "${NPM_TOKEN:-}" ]]; then
130
+ token_source="NPM_TOKEN"
131
+ token_value="$NPM_TOKEN"
132
+ elif [[ -n "${NODE_AUTH_TOKEN:-}" ]]; then
133
+ token_source="NODE_AUTH_TOKEN"
134
+ token_value="$NODE_AUTH_TOKEN"
135
+ else
136
+ return 1
137
+ fi
138
+
139
+ TEMP_NPMRC=$(mktemp)
140
+ chmod 0600 "$TEMP_NPMRC"
141
+ {
142
+ printf 'registry=%s\n' "$REGISTRY_URL"
143
+ printf '//registry.npmjs.org/:_authToken=%s\n' "$token_value"
144
+ } >"$TEMP_NPMRC"
145
+
146
+ export NPM_CONFIG_USERCONFIG="$TEMP_NPMRC"
147
+ AUTH_SOURCE="$token_source"
148
+ }
149
+
150
+ has_configured_registry_token() {
151
+ npm config list 2>/dev/null | grep -Eq '^//registry\.npmjs\.org/:_authToken = \(protected\)$'
152
+ }
153
+
154
+ trim_output() {
155
+ tr -d '\r' | awk '{$1=$1; print}'
156
+ }
157
+
158
+ verify_publish_auth() {
159
+ local npm_user
160
+ local tfa_mode
161
+
162
+ echo "--- Auth check ---"
163
+ if configure_token_from_env; then
164
+ echo "Using ${AUTH_SOURCE} via temporary npm config."
165
+ fi
166
+
167
+ if ! npm_user=$(npm whoami --registry="$REGISTRY_URL" 2>/dev/null); then
168
+ print_token_instructions "npm is not authenticated for ${REGISTRY_URL}."
169
+ exit 1
170
+ fi
171
+
172
+ echo "Logged in as: ${npm_user}"
173
+
174
+ if [[ -n "$AUTH_SOURCE" ]]; then
175
+ echo "Publish token source: ${AUTH_SOURCE}"
176
+ echo "Token must be granular, read/write for ${PACKAGE_NAME}, and Bypass 2FA enabled."
177
+ echo ""
178
+ return 0
179
+ fi
180
+
181
+ if has_configured_registry_token; then
182
+ AUTH_SOURCE="npm user config"
183
+ echo "Publish token source: ${AUTH_SOURCE}"
184
+ echo "Token must be granular, read/write for ${PACKAGE_NAME}, and Bypass 2FA enabled."
185
+ echo ""
186
+ return 0
187
+ fi
188
+
189
+ if ! tfa_mode=$(npm profile get "two-factor auth" --registry="$REGISTRY_URL" 2>/dev/null | trim_output); then
190
+ print_token_instructions "unable to verify npm 2FA state and no publish token was supplied."
191
+ exit 1
192
+ fi
193
+
194
+ if [[ -z "$tfa_mode" ]]; then
195
+ print_token_instructions "npm did not report an account 2FA mode and no publish token was supplied."
196
+ exit 1
197
+ fi
198
+
199
+ echo "Account 2FA mode: ${tfa_mode}"
200
+ if [[ "$tfa_mode" == "disabled" ]]; then
201
+ print_token_instructions "npm account 2FA is disabled and no publish token was supplied."
202
+ exit 1
203
+ fi
204
+
205
+ echo "No explicit publish token supplied; npm must complete the publish with an interactive OTP."
206
+ echo ""
207
+ }
208
+
209
+ main() {
210
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
211
+ usage
212
+ exit 0
213
+ fi
214
+
215
+ if [[ "$#" -ne 0 ]]; then
216
+ usage >&2
217
+ die "unknown argument: $1"
218
+ fi
219
+
220
+ cd "$(repo_root)"
221
+ [[ -f package.json ]] || die "package.json not found"
222
+
223
+ PACKAGE_NAME="$(read_package_name)" || die "failed to read package.json name"
224
+ local version
225
+ version="$(read_package_version)" || die "failed to read package.json version"
226
+ [[ -n "$PACKAGE_NAME" ]] || die "package.json has no name field"
227
+ [[ -n "$version" ]] || die "package.json has no version field"
228
+
229
+ local publish_args=("--registry=$REGISTRY_URL")
230
+ if [[ "$PACKAGE_NAME" == @*/* ]]; then
231
+ publish_args+=(--access public)
232
+ fi
233
+
234
+ echo "Publishing ${PACKAGE_NAME}@${version}"
235
+ verify_publish_auth
236
+
237
+ echo "--- Preflight ---"
238
+ bash scripts/bump-version.sh --check
239
+ bash scripts/preflight-checks.sh
240
+ echo ""
241
+
242
+ echo "--- Dry run ---"
243
+ npm publish --dry-run "${publish_args[@]}"
244
+ echo ""
245
+
246
+ local confirm
247
+ read -rp "Publish ${PACKAGE_NAME}@${version} to npm? (y/N) " confirm
248
+ if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
249
+ echo "Aborted."
250
+ exit 0
251
+ fi
252
+
253
+ npm publish "${publish_args[@]}"
254
+ echo ""
255
+ echo "Published: https://www.npmjs.com/package/${PACKAGE_NAME}/v/${version}"
256
+ }
257
+
258
+ main "$@"
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+
4
+ SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5
+ REPO_ROOT="$(CDPATH='' cd -- "$SCRIPT_DIR/.." && pwd)"
6
+
7
+ if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
8
+ BOLD=$'\033[1m'
9
+ DIM=$'\033[2m'
10
+ GREEN=$'\033[32m'
11
+ RED=$'\033[31m'
12
+ YELLOW=$'\033[33m'
13
+ BLUE=$'\033[34m'
14
+ RESET=$'\033[0m'
15
+ else
16
+ BOLD=''
17
+ DIM=''
18
+ GREEN=''
19
+ RED=''
20
+ YELLOW=''
21
+ BLUE=''
22
+ RESET=''
23
+ fi
24
+
25
+ PASS="${GREEN}OK${RESET}"
26
+ FAIL="${RED}FAIL${RESET}"
27
+ SKIP="${YELLOW}SKIP${RESET}"
28
+ ARROW="${BLUE}->${RESET}"
29
+
30
+ TOTAL=0
31
+ PASSED=0
32
+ FAILED=0
33
+ FAILURES=()
34
+ TMP_FILES=()
35
+ START_TIME=$(date +%s%N)
36
+
37
+ usage() {
38
+ cat <<'USAGE'
39
+ Usage:
40
+ scripts/preflight-checks.sh
41
+
42
+ Runs the local preflight gate:
43
+ - npm run check (TypeScript compile plus unit tests)
44
+ - gruff-ts full-project scan
45
+ - shellcheck for scripts/*.sh when shellcheck is installed
46
+
47
+ Environment:
48
+ GRUFF_TS_FAIL_ON gruff-ts severity that fails static analysis (default: advisory)
49
+ USAGE
50
+ }
51
+
52
+ cleanup() {
53
+ local temp_file
54
+
55
+ for temp_file in "${TMP_FILES[@]}"; do
56
+ [[ -f "$temp_file" ]] && rm -f -- "$temp_file"
57
+ done
58
+ }
59
+
60
+ trap cleanup EXIT
61
+
62
+ rule() {
63
+ printf ' %s\n' "${DIM}--------------------------------------------${RESET}"
64
+ }
65
+
66
+ elapsed_since() {
67
+ local started_at="$1"
68
+ local finished_at
69
+ local elapsed_ms
70
+ local seconds
71
+ local minutes
72
+ local remainder
73
+ local frac
74
+
75
+ finished_at=$(date +%s%N)
76
+ elapsed_ms=$(((finished_at - started_at) / 1000000))
77
+
78
+ if ((elapsed_ms < 1000)); then
79
+ printf '%dms' "$elapsed_ms"
80
+ return
81
+ fi
82
+
83
+ seconds=$((elapsed_ms / 1000))
84
+ frac=$(((elapsed_ms % 1000) / 100))
85
+
86
+ if ((seconds < 60)); then
87
+ printf '%d.%ds' "$seconds" "$frac"
88
+ return
89
+ fi
90
+
91
+ minutes=$((seconds / 60))
92
+ remainder=$((seconds % 60))
93
+ printf '%dm %02d.%ds' "$minutes" "$remainder" "$frac"
94
+ }
95
+
96
+ header() {
97
+ printf '\n'
98
+ printf ' %sPreflight Check%s\n' "$BOLD" "$RESET"
99
+ printf ' %s%s%s\n' "$DIM" "$(date '+%Y-%m-%d %H:%M:%S')" "$RESET"
100
+ rule
101
+ printf '\n'
102
+ }
103
+
104
+ step() {
105
+ local label="$1"
106
+
107
+ TOTAL=$((TOTAL + 1))
108
+ printf ' %s %-36s' "$ARROW" "$label"
109
+ }
110
+
111
+ pass() {
112
+ local detail="${1:-}"
113
+
114
+ PASSED=$((PASSED + 1))
115
+ if [[ -n "$detail" ]]; then
116
+ printf '%s %s%s%s\n' "$PASS" "$DIM" "$detail" "$RESET"
117
+ else
118
+ printf '%s\n' "$PASS"
119
+ fi
120
+ }
121
+
122
+ fail() {
123
+ local label="$1"
124
+
125
+ FAILED=$((FAILED + 1))
126
+ FAILURES+=("$label")
127
+ printf '%s\n' "$FAIL"
128
+ }
129
+
130
+ skip() {
131
+ local reason="${1:-skipped}"
132
+
133
+ printf '%s %s%s%s\n' "$SKIP" "$DIM" "$reason" "$RESET"
134
+ }
135
+
136
+ indent_output() {
137
+ while IFS= read -r line; do
138
+ printf ' %s%s%s\n' "$DIM" "$line" "$RESET"
139
+ done
140
+ }
141
+
142
+ run_step() {
143
+ local label="$1"
144
+ shift
145
+
146
+ local started_at
147
+ local output
148
+ local status
149
+ local elapsed
150
+
151
+ step "$label"
152
+ started_at=$(date +%s%N)
153
+ output=$("$@" 2>&1)
154
+ status=$?
155
+ elapsed=$(elapsed_since "$started_at")
156
+
157
+ if ((status == 0)); then
158
+ pass "${output:+$output }$elapsed"
159
+ else
160
+ fail "$label"
161
+ if [[ -n "$output" ]]; then
162
+ printf '%s\n' "$output" | tail -20 | indent_output
163
+ fi
164
+ printf ' %sexit %d after %s%s\n' "$DIM" "$status" "$elapsed" "$RESET"
165
+ fi
166
+
167
+ return "$status"
168
+ }
169
+
170
+ make_temp_file() {
171
+ local suffix="$1"
172
+ local temp_file
173
+
174
+ temp_file=$(mktemp "${TMPDIR:-/tmp}/gruff-ts-preflight.XXXXXX.$suffix") || return 1
175
+ TMP_FILES+=("$temp_file")
176
+ printf '%s\n' "$temp_file"
177
+ }
178
+
179
+ npm_check() {
180
+ local output
181
+ local status
182
+ local tests
183
+ local passed
184
+ local failed
185
+
186
+ output=$(npm run check 2>&1)
187
+ status=$?
188
+
189
+ if ((status != 0)); then
190
+ printf '%s\n' "$output"
191
+ return "$status"
192
+ fi
193
+
194
+ tests=$(printf '%s\n' "$output" | awk '/^# tests / { print $3; exit }')
195
+ passed=$(printf '%s\n' "$output" | awk '/^# pass / { print $3; exit }')
196
+ failed=$(printf '%s\n' "$output" | awk '/^# fail / { print $3; exit }')
197
+
198
+ if [[ -n "$tests" && -n "$passed" ]]; then
199
+ printf '%s/%s tests passed' "$passed" "$tests"
200
+ if [[ -n "$failed" && "$failed" != "0" ]]; then
201
+ printf ', %s failed' "$failed"
202
+ fi
203
+ else
204
+ printf 'completed'
205
+ fi
206
+
207
+ return 0
208
+ }
209
+
210
+ gruff_report_summary() {
211
+ local report_path="$1"
212
+
213
+ # shellcheck disable=SC2016
214
+ node --input-type=module -e '
215
+ import { readFileSync } from "node:fs";
216
+
217
+ const report = JSON.parse(readFileSync(process.argv[1], "utf8"));
218
+ const summary = report.summary ?? {};
219
+ const score = report.score ?? {};
220
+ const paths = report.paths ?? {};
221
+ const total = Number(summary.total ?? 0);
222
+ const advisory = Number(summary.advisory ?? 0);
223
+ const warning = Number(summary.warning ?? 0);
224
+ const error = Number(summary.error ?? 0);
225
+ const grade = String(score.grade ?? "n/a");
226
+ const composite = Number(score.composite ?? 0).toFixed(1);
227
+ const analysedFiles = Number(paths.analysedFiles ?? 0);
228
+
229
+ console.log(`${total} findings (advisory=${advisory}, warning=${warning}, error=${error}), ${grade} ${composite}/100, ${analysedFiles} files`);
230
+ ' "$report_path"
231
+ }
232
+
233
+ gruff_ts_check() {
234
+ local gruff_fail_on="${GRUFF_TS_FAIL_ON:-advisory}"
235
+ local report_path
236
+ local error_path
237
+ local status
238
+ local summary_status=0
239
+ local printed=0
240
+
241
+ report_path=$(make_temp_file json) || return 1
242
+ error_path=$(make_temp_file err) || return 1
243
+
244
+ ./bin/gruff-ts analyse . --format=json --fail-on="$gruff_fail_on" --no-baseline >"$report_path" 2>"$error_path"
245
+ status=$?
246
+
247
+ if [[ -s "$report_path" ]]; then
248
+ gruff_report_summary "$report_path"
249
+ summary_status=$?
250
+ printed=1
251
+ fi
252
+
253
+ if [[ -s "$error_path" ]]; then
254
+ if ((printed)); then
255
+ printf '\n'
256
+ fi
257
+ cat "$error_path"
258
+ fi
259
+
260
+ if ((status != 0)); then
261
+ return "$status"
262
+ fi
263
+ return "$summary_status"
264
+ }
265
+
266
+ shellcheck_check() {
267
+ local scripts=()
268
+ local script_path
269
+ local output
270
+ local status
271
+
272
+ while IFS= read -r -d '' script_path; do
273
+ scripts+=("$script_path")
274
+ done < <(find scripts -maxdepth 1 -type f -name '*.sh' -print0 | sort -z)
275
+
276
+ if [[ "${#scripts[@]}" -eq 0 ]]; then
277
+ printf 'no scripts/*.sh files found'
278
+ return 0
279
+ fi
280
+
281
+ output=$(shellcheck "${scripts[@]}" 2>&1)
282
+ status=$?
283
+
284
+ if ((status == 0)); then
285
+ printf '%d scripts checked' "${#scripts[@]}"
286
+ else
287
+ printf '%s\n' "$output"
288
+ fi
289
+
290
+ return "$status"
291
+ }
292
+
293
+ summary() {
294
+ local elapsed
295
+
296
+ elapsed=$(elapsed_since "$START_TIME")
297
+ printf '\n'
298
+ rule
299
+ printf '\n'
300
+
301
+ if ((FAILED == 0)); then
302
+ printf ' %sAll %d/%d checks passed%s %s(%s)%s\n' "$GREEN$BOLD" "$PASSED" "$TOTAL" "$RESET" "$DIM" "$elapsed" "$RESET"
303
+ printf '\n'
304
+ return 0
305
+ fi
306
+
307
+ printf ' %s%d/%d checks failed%s %s(%s)%s\n' "$RED$BOLD" "$FAILED" "$TOTAL" "$RESET" "$DIM" "$elapsed" "$RESET"
308
+ printf '\n'
309
+
310
+ local failure
311
+ for failure in "${FAILURES[@]}"; do
312
+ printf ' %s %s\n' "$FAIL" "$failure"
313
+ done
314
+ printf '\n'
315
+
316
+ return 1
317
+ }
318
+
319
+ main() {
320
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
321
+ usage
322
+ return 0
323
+ fi
324
+
325
+ if [[ "$#" -ne 0 ]]; then
326
+ printf '%sUnknown argument:%s %s\n' "$RED" "$RESET" "$1" >&2
327
+ usage >&2
328
+ return 64
329
+ fi
330
+
331
+ cd "$REPO_ROOT" || return 1
332
+
333
+ header
334
+
335
+ if [[ ! -x ./bin/gruff-ts ]]; then
336
+ step "gruff-ts binary"
337
+ fail "gruff-ts binary"
338
+ printf ' %s./bin/gruff-ts is missing or not executable%s\n' "$DIM" "$RESET"
339
+ summary
340
+ return 127
341
+ fi
342
+
343
+ run_step "TypeScript + tests" npm_check
344
+
345
+ run_step "Gruff full-project scan" gruff_ts_check
346
+
347
+ if command -v shellcheck >/dev/null 2>&1; then
348
+ run_step "Shell scripts (shellcheck)" shellcheck_check
349
+ else
350
+ step "Shell scripts (shellcheck)"
351
+ skip "shellcheck not found"
352
+ fi
353
+
354
+ summary
355
+ }
356
+
357
+ main "$@"
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ HOST="${GRUFF_HOST:-127.0.0.1}"
5
+ PORT="${GRUFF_PORT:-8767}"
6
+ PROJECT_ROOT="${GRUFF_PROJECT_ROOT:-$(pwd)}"
7
+
8
+ npm run start-dev -- --host "$HOST" --port "$PORT" --project-root "$PROJECT_ROOT"