@zairakai/js-dev-tools 1.0.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 (69) hide show
  1. package/.editorconfig +86 -0
  2. package/.gitlab/ci/pipeline-js-app.yml +75 -0
  3. package/.gitlab/ci/pipeline-js-package.yml +360 -0
  4. package/LICENSE +21 -0
  5. package/README.md +206 -0
  6. package/config/.markdownlint.json +7 -0
  7. package/config/.markdownlintignore +5 -0
  8. package/config/.prettierignore +11 -0
  9. package/config/.stylelintignore +7 -0
  10. package/config/eslint.config.js +198 -0
  11. package/config/knip.config.js +40 -0
  12. package/config/prettier.config.js +121 -0
  13. package/config/stylelint.config.js +56 -0
  14. package/config/tsconfig.base.json +20 -0
  15. package/config/vitest.config.js +25 -0
  16. package/index.js +24 -0
  17. package/package.json +148 -0
  18. package/scripts/build.sh +58 -0
  19. package/scripts/ci-quality.sh +51 -0
  20. package/scripts/config.sh +254 -0
  21. package/scripts/doctor.sh +74 -0
  22. package/scripts/eslint-fix.sh +38 -0
  23. package/scripts/eslint.sh +49 -0
  24. package/scripts/install-bats.sh +227 -0
  25. package/scripts/install-hooks.sh +167 -0
  26. package/scripts/install-markdownlint.sh +31 -0
  27. package/scripts/install-shellcheck.sh +232 -0
  28. package/scripts/knip.sh +40 -0
  29. package/scripts/markdownlint-fix.sh +8 -0
  30. package/scripts/markdownlint.sh +84 -0
  31. package/scripts/outdated.sh +70 -0
  32. package/scripts/prettier-fix.sh +52 -0
  33. package/scripts/prettier.sh +53 -0
  34. package/scripts/setup-project.sh +816 -0
  35. package/scripts/stylelint-fix.sh +64 -0
  36. package/scripts/stylelint.sh +66 -0
  37. package/scripts/test.sh +40 -0
  38. package/scripts/typecheck.sh +41 -0
  39. package/scripts/validate-shellcheck.sh +70 -0
  40. package/stubs/githooks/commit-msg +34 -0
  41. package/stubs/githooks/pre-commit +58 -0
  42. package/stubs/githooks/pre-push +57 -0
  43. package/stubs/githooks/prepare-commit-msg +39 -0
  44. package/stubs/gitlab-ci/gitlab-ci.yml.stub +18 -0
  45. package/stubs/gitlab-ci/gitlab-pipeline-js-app.yml.stub +16 -0
  46. package/stubs/governance/CODE_OF_CONDUCT.md.stub +8 -0
  47. package/stubs/governance/CONTRIBUTING.md.stub +39 -0
  48. package/stubs/governance/SECURITY.md.stub +41 -0
  49. package/stubs/quality/eslint.config.js.stub +18 -0
  50. package/stubs/quality/knip.config.js.stub +14 -0
  51. package/stubs/style/prettier.config.js.stub +16 -0
  52. package/stubs/style/stylelint.config.js.stub +17 -0
  53. package/stubs/testing/vitest.config.js.stub +18 -0
  54. package/stubs/typescript/tsconfig.json.stub +10 -0
  55. package/tools/make/bats.mk +49 -0
  56. package/tools/make/code-style.mk +20 -0
  57. package/tools/make/core.mk +56 -0
  58. package/tools/make/help.mk +32 -0
  59. package/tools/make/knip.mk +7 -0
  60. package/tools/make/markdownlint.mk +17 -0
  61. package/tools/make/quality.mk +19 -0
  62. package/tools/make/shellcheck.mk +14 -0
  63. package/tools/make/stylelint.mk +11 -0
  64. package/tools/make/test.mk +19 -0
  65. package/tools/make/typescript.mk +10 -0
  66. package/tools/make/utils.mk +44 -0
  67. package/tools/make/variables.mk +42 -0
  68. package/tools/scripts/git-cleanup.sh +53 -0
  69. package/tools/scripts/git-update.sh +37 -0
@@ -0,0 +1,816 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Zairakai NPM Dev Tools - Project Setup Script
4
+ #
5
+ # Usage:
6
+ # bash setup-project.sh # Normal setup (Makefile + .editorconfig + eslint baseline)
7
+ # bash setup-project.sh --publish # Publish all configs to config/dev-tools/
8
+ # bash setup-project.sh --publish=quality # Publish quality group (eslint)
9
+ # bash setup-project.sh --publish=style # Publish style group (prettier, stylelint, ignore files)
10
+ # bash setup-project.sh --publish=testing # Publish vitest config
11
+ # bash setup-project.sh --publish=gitlab-ci # Publish GitLab CI (auto-detects project type)
12
+ # bash setup-project.sh --publish=eslint # Publish specific config
13
+ # bash setup-project.sh --with-makefile # Also generate/inject Makefile
14
+ # bash setup-project.sh --force # Force overwrite existing files
15
+ # bash setup-project.sh --silent # Suppress output (errors only)
16
+ # bash setup-project.sh --help # Show this help
17
+ #
18
+
19
+ set -euo pipefail
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ # shellcheck disable=SC1091
23
+ source "${SCRIPT_DIR}/config.sh"
24
+
25
+ # ============================================================================
26
+ # CI Mode Detection — delegates to IS_CI exported by config.sh
27
+ # ============================================================================
28
+ CI_MODE="$IS_CI"
29
+
30
+ # ============================================================================
31
+ # Publishable configs registry
32
+ # Format: [key]="source_relative_to_dev_tools|target_relative_to_project|group"
33
+ # Note: gitlab-ci has a special handler (auto-detects project type) — not in this registry.
34
+ # ============================================================================
35
+
36
+ declare -A PUBLISHABLE=(
37
+ ["eslint"]="stubs/quality/eslint.config.js.stub|config/dev-tools/eslint.config.js|quality"
38
+ ["knip"]="stubs/quality/knip.config.js.stub|config/dev-tools/knip.config.js|quality"
39
+ ["prettier"]="stubs/style/prettier.config.js.stub|config/dev-tools/prettier.config.js|style"
40
+ ["stylelint"]="stubs/style/stylelint.config.js.stub|config/dev-tools/stylelint.config.js|style"
41
+ ["prettierignore"]="config/.prettierignore|config/dev-tools/.prettierignore|style"
42
+ ["stylelintignore"]="config/.stylelintignore|config/dev-tools/.stylelintignore|style"
43
+ ["markdownlint"]="config/.markdownlint.json|config/dev-tools/.markdownlint.json|style"
44
+ ["markdownlintignore"]="config/.markdownlintignore|config/dev-tools/.markdownlintignore|style"
45
+ ["vitest"]="stubs/testing/vitest.config.js.stub|config/dev-tools/vitest.config.js|testing"
46
+ ["tsconfig"]="stubs/typescript/tsconfig.json.stub|tsconfig.json|typescript"
47
+ )
48
+
49
+ declare -A PUBLISH_GROUPS=(
50
+ ["quality"]="eslint knip"
51
+ ["style"]="prettier stylelint prettierignore stylelintignore markdownlint markdownlintignore"
52
+ ["testing"]="vitest"
53
+ ["typescript"]="tsconfig"
54
+ ["hooks"]="hooks"
55
+ ["governance"]="governance"
56
+ ["all"]="eslint knip prettier stylelint prettierignore stylelintignore markdownlint markdownlintignore vitest tsconfig hooks"
57
+ )
58
+
59
+ # ============================================================================
60
+ # Argument Parsing
61
+ # ============================================================================
62
+
63
+ FORCE_OVERWRITE=false
64
+ SILENT_MODE=false
65
+ PUBLISH_TARGET=""
66
+ WITH_MAKEFILE=false
67
+
68
+ for arg in "$@"; do
69
+ case "$arg" in
70
+ --force|-f) FORCE_OVERWRITE=true ;;
71
+ --silent|-s) SILENT_MODE=true ;;
72
+ --with-makefile) WITH_MAKEFILE=true ;;
73
+ --publish) PUBLISH_TARGET="all" ;;
74
+ --publish=*) PUBLISH_TARGET="${arg#--publish=}" ;;
75
+ --help|-h)
76
+ echo ""
77
+ echo "Usage: bash setup-project.sh [options]"
78
+ echo ""
79
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
80
+ echo " Options"
81
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
82
+ echo " (no options) Normal setup (Makefile + .editorconfig + eslint baseline)"
83
+ echo " --publish Publish ALL configs to config/dev-tools/"
84
+ echo " --publish=<group> Publish a config group"
85
+ echo " --publish=<key> Publish a specific config"
86
+ echo " --with-makefile Generate or inject Makefile"
87
+ echo " --force, -f Overwrite existing files"
88
+ echo " --silent, -s Suppress output (for postinstall)"
89
+ echo " --help, -h Show this help"
90
+ echo ""
91
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
92
+ echo " Groups (--publish=<group>)"
93
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
94
+ echo " quality eslint"
95
+ echo " style prettier, stylelint, markdownlint, .prettierignore, .stylelintignore, .markdownlintignore"
96
+ echo " testing vitest"
97
+ echo " typescript tsconfig"
98
+ echo " all all configs (except gitlab-ci)"
99
+ echo ""
100
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
101
+ echo " Specific configs (--publish=<key>)"
102
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
103
+ echo " eslint → config/dev-tools/eslint.config.js"
104
+ echo " prettier → config/dev-tools/prettier.config.js"
105
+ echo " stylelint → config/dev-tools/stylelint.config.js"
106
+ echo " prettierignore → config/dev-tools/.prettierignore"
107
+ echo " stylelintignore → config/dev-tools/.stylelintignore"
108
+ echo " markdownlint → config/dev-tools/.markdownlint.json"
109
+ echo " markdownlintignore → config/dev-tools/.markdownlintignore"
110
+ echo " vitest → config/dev-tools/vitest.config.js"
111
+ echo " tsconfig → tsconfig.json (extends bundled base)"
112
+ echo " gitlab-ci → auto-detect: .gitlab-ci.yml or .gitlab/pipeline-js-app.yml"
113
+ echo " governance → SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md"
114
+ echo ""
115
+ exit 0
116
+ ;;
117
+ *)
118
+ echo "ERROR: Unknown option: $arg" >&2
119
+ echo "Use --help for usage information" >&2
120
+ exit 1
121
+ ;;
122
+ esac
123
+ done
124
+
125
+ # ============================================================================
126
+ # Silent Mode Override
127
+ # ============================================================================
128
+
129
+ if [[ "$SILENT_MODE" == "true" ]]; then
130
+ log_header() { :; }
131
+ log_step() { :; }
132
+ log_info() { :; }
133
+ log_success() { :; }
134
+ log_warning() { :; }
135
+ fi
136
+
137
+ # ============================================================================
138
+ # Optional Dependencies Check
139
+ # Runs after setup — shows informative messages even in silent mode (postinstall)
140
+ # ============================================================================
141
+
142
+ check_optional_deps() {
143
+ local -a missing=()
144
+
145
+ # zod — runtime type validation (validates API responses / form inputs at runtime)
146
+ [[ ! -d "${PROJECT_ROOT}/node_modules/zod" ]] \
147
+ && missing+=("zod|Runtime type validation for JS/Vue (validates API responses, forms)|${PM} install zod")
148
+
149
+ # typescript — enables make typecheck and make build
150
+ [[ ! -d "${PROJECT_ROOT}/node_modules/typescript" ]] \
151
+ && missing+=("typescript|TypeScript compiler — enables make typecheck + make build|${PM} install --save-dev typescript")
152
+
153
+ # tsup — fast TS bundler (recommended for npm packages to publish)
154
+ [[ ! -d "${PROJECT_ROOT}/node_modules/tsup" ]] \
155
+ && missing+=("tsup|Fast TypeScript bundler for npm packages (esbuild-based)|${PM} install --save-dev tsup")
156
+
157
+ # knip — unused exports, files and dependencies detector
158
+ [[ ! -d "${PROJECT_ROOT}/node_modules/knip" ]] \
159
+ && missing+=("knip|Unused exports and dependencies detector (make knip)|${PM} install --save-dev knip")
160
+
161
+ [[ ${#missing[@]} -eq 0 ]] && return 0
162
+
163
+ if [[ "$SILENT_MODE" == "true" ]]; then
164
+ echo ""
165
+ echo " ┌─ @zairakai/js-dev-tools — optional packages not installed ─┐"
166
+ for entry in "${missing[@]}"; do
167
+ local pkg="${entry%%|*}"
168
+ local rest="${entry#*|}"
169
+ local install_cmd="${rest#*|}"
170
+ printf " │ %-12s %s\n" "${pkg}" "${install_cmd}"
171
+ done
172
+ echo " └─ Run: bash setup-project.sh (without --silent) for details ┘"
173
+ echo ""
174
+ return 0
175
+ fi
176
+
177
+ echo ""
178
+ echo -e "${YELLOW}┌──────────────────────────────────────────────────────┐${NC}"
179
+ echo -e "${YELLOW}│${NC} ${YELLOW}Recommended Optional Packages${NC} ${YELLOW}│${NC}"
180
+ echo -e "${YELLOW}├──────────────────────────────────────────────────────┤${NC}"
181
+ for entry in "${missing[@]}"; do
182
+ local pkg="${entry%%|*}"
183
+ local rest="${entry#*|}"
184
+ local reason="${rest%%|*}"
185
+ local install_cmd="${rest#*|}"
186
+ echo -e "${YELLOW}│${NC} ${CYAN}${pkg}${NC}"
187
+ echo -e "${YELLOW}│${NC} ${reason}"
188
+ echo -e "${YELLOW}│${NC} ${GREEN}${install_cmd}${NC}"
189
+ echo -e "${YELLOW}│${NC}"
190
+ done
191
+ echo -e "${YELLOW}└──────────────────────────────────────────────────────┘${NC}"
192
+ echo ""
193
+ }
194
+
195
+ # ============================================================================
196
+ # Result Tracking
197
+ # ============================================================================
198
+
199
+ declare -a CREATED_FILES=()
200
+ declare -a SKIPPED_FILES=()
201
+ declare -a BACKED_UP_FILES=()
202
+ declare -a PUBLISHED_FILES=()
203
+
204
+ track_created() { CREATED_FILES+=("$1"); }
205
+ track_skipped() { SKIPPED_FILES+=("$1"); }
206
+ track_backed_up() { BACKED_UP_FILES+=("$1"); }
207
+ track_published() { PUBLISHED_FILES+=("$1"); }
208
+
209
+ # ============================================================================
210
+ # File Helpers
211
+ # ============================================================================
212
+
213
+ # Publish a config to config/dev-tools/ (user-facing, hash-protected)
214
+ publish_file() {
215
+ local source="$1"
216
+ local target="$2"
217
+ local name="$3"
218
+
219
+ if [[ ! -f "$source" ]]; then
220
+ log_warning "Source not found for ${name}: ${source#"$DEV_TOOLS_ROOT"/}"
221
+ return 0
222
+ fi
223
+
224
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" != "true" ]]; then
225
+ local src_hash tgt_hash
226
+ src_hash="$(file_hash "$source")"
227
+ tgt_hash="$(file_hash "$target")"
228
+
229
+ if [[ -n "$src_hash" ]] && [[ "$src_hash" == "$tgt_hash" ]]; then
230
+ cp "$source" "$target"
231
+ track_published "${name} → ${target#"$PROJECT_ROOT"/} (refreshed)"
232
+ else
233
+ track_skipped "$name"
234
+ log_info "Skipping modified: ${name} (use --force to overwrite)"
235
+ fi
236
+ return 0
237
+ fi
238
+
239
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" == "true" ]]; then
240
+ if backup_file "$target" 2>/dev/null; then
241
+ track_backed_up "$name"
242
+ fi
243
+ fi
244
+
245
+ mkdir -p "$(dirname "$target")"
246
+ cp "$source" "$target"
247
+ track_published "${name} → ${target#"$PROJECT_ROOT"/}"
248
+ log_success "Published: ${name} → ${target#"$PROJECT_ROOT"/}"
249
+ }
250
+
251
+ # Copy file for standard setup (skips if exists unless --force or CI)
252
+ setup_file() {
253
+ local source="$1"
254
+ local target="$2"
255
+ local name="$3"
256
+
257
+ if [[ "$CI_MODE" == "true" ]] || [[ "$FORCE_OVERWRITE" == "true" ]]; then
258
+ if [[ -f "$target" ]] && [[ ! -L "$target" ]] && [[ "$CI_MODE" != "true" ]]; then
259
+ if backup_file "$target" 2>/dev/null; then
260
+ track_backed_up "$name"
261
+ fi
262
+ fi
263
+ rm -f "$target" 2>/dev/null || true
264
+ mkdir -p "$(dirname "$target")"
265
+ if cp "$source" "$target" 2>/dev/null; then
266
+ track_created "$name"
267
+ log_success "Created: $name"
268
+ fi
269
+ else
270
+ if [[ -f "$target" ]] || [[ -L "$target" ]]; then
271
+ track_skipped "$name"
272
+ return 0
273
+ fi
274
+ mkdir -p "$(dirname "$target")"
275
+ if cp "$source" "$target" 2>/dev/null; then
276
+ track_created "$name"
277
+ log_success "Created: $name"
278
+ fi
279
+ fi
280
+ }
281
+
282
+ # ============================================================================
283
+ # Governance Files Publisher
284
+ # Publishes SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md to project root.
285
+ # Replaces PACKAGE_GITLAB_ISSUES with the actual GitLab issues URL.
286
+ # Hash-protected: skips files modified by the user (use --force to overwrite).
287
+ # Not included in "all" — requires explicit opt-in (--publish=governance).
288
+ # ============================================================================
289
+
290
+ publish_governance_file() {
291
+ local source="$1"
292
+ local target="$2"
293
+ local name="$3"
294
+ local issues_url="$4"
295
+
296
+ if [[ ! -f "$source" ]]; then
297
+ log_warning "Stub not found: stubs/governance/${name}"
298
+ return 0
299
+ fi
300
+
301
+ # Process placeholders into a temp file for hash comparison and copy
302
+ local processed
303
+ processed="$(mktemp)"
304
+ sed "s|PACKAGE_GITLAB_ISSUES|${issues_url}|g" "$source" > "$processed"
305
+
306
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" != "true" ]]; then
307
+ local src_hash tgt_hash
308
+ src_hash="$(file_hash "$processed")"
309
+ tgt_hash="$(file_hash "$target")"
310
+
311
+ if [[ -n "$src_hash" ]] && [[ "$src_hash" == "$tgt_hash" ]]; then
312
+ cp "$processed" "$target"
313
+ track_published "${name} (refreshed)"
314
+ else
315
+ track_skipped "$name"
316
+ log_info "Skipping modified: ${name} (use --force to overwrite)"
317
+ fi
318
+ rm -f "$processed"
319
+ return 0
320
+ fi
321
+
322
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" == "true" ]]; then
323
+ backup_file "$target" 2>/dev/null || true
324
+ track_backed_up "$name"
325
+ fi
326
+
327
+ cp "$processed" "$target"
328
+ rm -f "$processed"
329
+ track_published "$name"
330
+ log_success "Published: ${name}"
331
+ }
332
+
333
+ publish_governance_files() {
334
+ local pkg_name
335
+ pkg_name="$(node -e "try{const p=require('${PROJECT_ROOT}/package.json');console.log(p.name||'')}catch(e){}" 2>/dev/null || echo "")"
336
+
337
+ local pkg_short="${pkg_name##*/}" # strip scope: @zairakai/js-dev-tools → dev-tools
338
+ local issues_url="https://gitlab.com/zairakai/npm-packages/${pkg_short}/-/issues"
339
+
340
+ local -a docs=("SECURITY.md" "CONTRIBUTING.md" "CODE_OF_CONDUCT.md")
341
+
342
+ for filename in "${docs[@]}"; do
343
+ publish_governance_file \
344
+ "${DEV_TOOLS_ROOT}/stubs/governance/${filename}.stub" \
345
+ "${PROJECT_ROOT}/${filename}" \
346
+ "$filename" \
347
+ "$issues_url"
348
+ done
349
+ }
350
+
351
+ # ============================================================================
352
+ # GitLab CI — Placeholder Replacement Helper
353
+ # ============================================================================
354
+
355
+ # Extract package name and derive cache key from package.json
356
+ get_package_info() {
357
+ local pkg_name pkg_cache_key
358
+
359
+ pkg_name="$(node -e "try{const p=require('${PROJECT_ROOT}/package.json');console.log(p.name||'')}catch(e){}" 2>/dev/null || echo "")"
360
+ pkg_cache_key="${pkg_name##*/}" # strip scope: @org/my-pkg → my-pkg
361
+ pkg_cache_key="${pkg_cache_key}-v1"
362
+
363
+ echo "${pkg_name}|${pkg_cache_key}"
364
+ }
365
+
366
+ # Replace placeholders in a published GitLab CI file
367
+ replace_gitlab_ci_placeholders() {
368
+ local target="$1"
369
+ local pkg_info
370
+ pkg_info="$(get_package_info)"
371
+
372
+ local pkg_name="${pkg_info%%|*}"
373
+ local pkg_cache_key="${pkg_info#*|}"
374
+
375
+ # Get current version of @zairakai/js-dev-tools
376
+ local dev_tools_version
377
+ dev_tools_version="$(node -e "try{const p=require('${DEV_TOOLS_ROOT}/package.json');console.log(p.version||'0.0.0')}catch(e){}" 2>/dev/null || echo "0.0.0")"
378
+
379
+ if [[ -n "$pkg_name" ]]; then
380
+ sed -i "s|PACKAGE_NPM_NAME|${pkg_name}|g" "$target"
381
+ sed -i "s|PACKAGE_CACHE_KEY|${pkg_cache_key}|g" "$target"
382
+ fi
383
+
384
+ if [[ -n "$dev_tools_version" ]]; then
385
+ sed -i "s|v0.0.0|v${dev_tools_version}|g" "$target"
386
+ fi
387
+ }
388
+
389
+ # ============================================================================
390
+ # GitLab CI — NPM Package (.gitlab-ci.yml)
391
+ # ============================================================================
392
+
393
+ publish_gitlab_ci_npm() {
394
+ local source="${DEV_TOOLS_ROOT}/stubs/gitlab-ci/gitlab-ci.yml.stub"
395
+ local target="${PROJECT_ROOT}/.gitlab-ci.yml"
396
+ local name="gitlab-ci"
397
+
398
+ if [[ ! -f "$source" ]]; then
399
+ log_warning "Stub not found: stubs/gitlab-ci/gitlab-ci.yml.stub"
400
+ return 0
401
+ fi
402
+
403
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" != "true" ]]; then
404
+ track_skipped "$name"
405
+ log_info "Already exists: .gitlab-ci.yml (use --force to overwrite)"
406
+ return 0
407
+ fi
408
+
409
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" == "true" ]]; then
410
+ if backup_file "$target" 2>/dev/null; then
411
+ track_backed_up ".gitlab-ci.yml"
412
+ fi
413
+ fi
414
+
415
+ cp "$source" "$target"
416
+ replace_gitlab_ci_placeholders "$target"
417
+
418
+ track_published ".gitlab-ci.yml"
419
+ log_success "Published: .gitlab-ci.yml"
420
+ log_info "ref: v0.0.0 will be updated automatically on: ${PM} update @zairakai/js-dev-tools"
421
+ }
422
+
423
+ # ============================================================================
424
+ # GitLab CI — Laravel App (.gitlab/pipeline-js-app.yml + inject .gitlab-ci.yml)
425
+ # ============================================================================
426
+
427
+ # Publish .gitlab/pipeline-js-app.yml (the JS pipeline fragment)
428
+ publish_gitlab_pipeline_js() {
429
+ local source="${DEV_TOOLS_ROOT}/stubs/gitlab-ci/gitlab-pipeline-js-app.yml.stub"
430
+ local target="${PROJECT_ROOT}/.gitlab/pipeline-js-app.yml"
431
+ local name="gitlab/pipeline-js-app.yml"
432
+
433
+ if [[ ! -f "$source" ]]; then
434
+ log_warning "Stub not found: stubs/gitlab-ci/gitlab-pipeline-js-app.yml.stub"
435
+ return 0
436
+ fi
437
+
438
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" != "true" ]]; then
439
+ track_skipped "$name"
440
+ log_info "Already exists: .gitlab/pipeline-js-app.yml (use --force to overwrite)"
441
+ return 0
442
+ fi
443
+
444
+ if [[ -f "$target" ]] && [[ "$FORCE_OVERWRITE" == "true" ]]; then
445
+ if backup_file "$target" 2>/dev/null; then
446
+ track_backed_up ".gitlab/pipeline-js-app.yml"
447
+ fi
448
+ fi
449
+
450
+ mkdir -p "${PROJECT_ROOT}/.gitlab"
451
+ cp "$source" "$target"
452
+ replace_gitlab_ci_placeholders "$target"
453
+
454
+ track_published ".gitlab/pipeline-js-app.yml"
455
+ log_success "Published: .gitlab/pipeline-js-app.yml"
456
+ }
457
+
458
+ # Inject the local pipeline-js-app.yml include into .gitlab-ci.yml (or create it)
459
+ inject_or_create_gitlab_ci() {
460
+ local target="${PROJECT_ROOT}/.gitlab-ci.yml"
461
+ local marker="# @zairakai/js-dev-tools — JS"
462
+
463
+ if [[ ! -f "$target" ]]; then
464
+ # No .gitlab-ci.yml yet — create a minimal one
465
+ cat > "$target" << 'GITLAB_EOF'
466
+ # .gitlab-ci.yml
467
+ # Generated by @zairakai/js-dev-tools setup-project.sh
468
+ # To regenerate:
469
+ # bash node_modules/@zairakai/js-dev-tools/scripts/setup-project.sh --publish=gitlab-ci --force
470
+
471
+ include:
472
+ # JavaScript/frontend pipeline:
473
+ - local: '.gitlab/pipeline-js-app.yml'
474
+
475
+ # @zairakai/js-dev-tools — JS ────────────────────────────────────────────────────
476
+ GITLAB_EOF
477
+ track_created ".gitlab-ci.yml"
478
+ log_success "Created: .gitlab-ci.yml (includes .gitlab/pipeline-js-app.yml)"
479
+ return 0
480
+ fi
481
+
482
+ # .gitlab-ci.yml exists — check if already injected
483
+ if grep -qF "$marker" "$target" 2>/dev/null; then
484
+ log_info ".gitlab-ci.yml already includes pipeline-js-app.yml — skipping"
485
+ track_skipped ".gitlab-ci.yml (already injected)"
486
+ return 0
487
+ fi
488
+
489
+ log_info "Existing .gitlab-ci.yml detected — injecting pipeline-js-app.yml include"
490
+
491
+ if [[ "$FORCE_OVERWRITE" == "true" ]]; then
492
+ if backup_file "$target" 2>/dev/null; then
493
+ track_backed_up ".gitlab-ci.yml"
494
+ fi
495
+ fi
496
+
497
+ cat >> "$target" << 'INJECT_EOF'
498
+
499
+ # @zairakai/js-dev-tools — JS ────────────────────────────────────────────────────
500
+ # Injected by: bash node_modules/@zairakai/js-dev-tools/scripts/setup-project.sh --publish=gitlab-ci
501
+ # Adds JavaScript quality and test jobs.
502
+ include:
503
+ - local: '.gitlab/pipeline-js-app.yml'
504
+ INJECT_EOF
505
+
506
+ track_created ".gitlab-ci.yml (injected)"
507
+ log_success "Injected pipeline-js-app.yml include into .gitlab-ci.yml"
508
+ }
509
+
510
+ publish_gitlab_ci_laravel() {
511
+ publish_gitlab_pipeline_js
512
+ inject_or_create_gitlab_ci
513
+ }
514
+
515
+ # ============================================================================
516
+ # Git Hooks — Install into .git/hooks
517
+ # ============================================================================
518
+
519
+ publish_hooks() {
520
+ local installer="${DEV_TOOLS_ROOT}/scripts/install-hooks.sh"
521
+
522
+ if [[ ! -f "$installer" ]]; then
523
+ log_warning "Hooks installer not found: scripts/install-hooks.sh"
524
+ return 0
525
+ fi
526
+
527
+ log_info "Git hooks detected → installing hooks via scripts/install-hooks.sh"
528
+ bash "$installer" --install
529
+ }
530
+
531
+ # ============================================================================
532
+ # Publish Logic
533
+ # ============================================================================
534
+
535
+ resolve_publish_keys() {
536
+ local target="$1"
537
+
538
+ # Special handlers — not in PUBLISHABLE
539
+ if [[ "$target" == "gitlab-ci" || "$target" == "hooks" ]]; then
540
+ echo "$target"
541
+ return 0
542
+ fi
543
+
544
+ if [[ -n "${PUBLISH_GROUPS[$target]:-}" ]]; then
545
+ echo "${PUBLISH_GROUPS[$target]}"
546
+ return 0
547
+ fi
548
+
549
+ if [[ -n "${PUBLISHABLE[$target]:-}" ]]; then
550
+ echo "$target"
551
+ return 0
552
+ fi
553
+
554
+ log_error "Unknown publish target: '${target}'"
555
+ echo "" >&2
556
+ echo "Valid groups: ${!PUBLISH_GROUPS[*]}" >&2
557
+ echo "Valid configs: ${!PUBLISHABLE[*]} gitlab-ci hooks" >&2
558
+ echo "Run --help for details" >&2
559
+ exit 1
560
+ }
561
+
562
+ # Dispatch --publish=gitlab-ci based on detected project type.
563
+ # When laravel-dev-tools is present, delegates to it so the fullstack stub
564
+ # (single source of truth in laravel-dev-tools) is used automatically.
565
+ publish_gitlab_ci() {
566
+ if [[ "$PROJECT_TYPE" == "laravel-app" ]]; then
567
+ local laravel_setup="${PROJECT_ROOT}/vendor/zairakai/laravel-dev-tools/scripts/setup-package.sh"
568
+ if [[ -f "$laravel_setup" ]]; then
569
+ log_info "laravel-dev-tools detected → delegating gitlab-ci publish (fullstack)"
570
+ local extra_args=()
571
+ [[ "$FORCE_OVERWRITE" == "true" ]] && extra_args+=("--force")
572
+ [[ "$SILENT_MODE" == "true" ]] && extra_args+=("--silent")
573
+ bash "$laravel_setup" --publish=gitlab-ci "${extra_args[@]}"
574
+ return 0
575
+ fi
576
+ log_info "Project type: laravel-app → publishing .gitlab/pipeline-js-app.yml"
577
+ publish_gitlab_ci_laravel
578
+ else
579
+ log_info "Project type: npm-package → publishing .gitlab-ci.yml"
580
+ publish_gitlab_ci_npm
581
+ fi
582
+ }
583
+
584
+ do_publish() {
585
+ local -a keys
586
+ read -ra keys <<< "$(resolve_publish_keys "$PUBLISH_TARGET")"
587
+
588
+ log_header "Publishing Dev Tools Configs"
589
+
590
+ for key in "${keys[@]}"; do
591
+ # Dispatch special handler — project-type aware
592
+ if [[ "$key" == "gitlab-ci" ]]; then
593
+ publish_gitlab_ci
594
+ continue
595
+ fi
596
+
597
+ # Dispatch special handler — git hooks
598
+ if [[ "$key" == "hooks" ]]; then
599
+ publish_hooks
600
+ continue
601
+ fi
602
+
603
+ # Dispatch special handler — governance files
604
+ if [[ "$key" == "governance" ]]; then
605
+ publish_governance_files
606
+ continue
607
+ fi
608
+
609
+ local entry="${PUBLISHABLE[$key]:-}"
610
+ [[ -z "$entry" ]] && continue
611
+
612
+ local source_rel="${entry%%|*}"
613
+ local rest="${entry#*|}"
614
+ local target_rel="${rest%%|*}"
615
+
616
+ publish_file \
617
+ "${DEV_TOOLS_ROOT}/${source_rel}" \
618
+ "${PROJECT_ROOT}/${target_rel}" \
619
+ "$key"
620
+ done
621
+ }
622
+
623
+ # ============================================================================
624
+ # Makefile Setup
625
+ # ============================================================================
626
+
627
+ generate_makefile() {
628
+ local target="${PROJECT_ROOT}/Makefile"
629
+ local package_name
630
+
631
+ package_name="$(node -e "try{const p=require('${PROJECT_ROOT}/package.json');console.log(p.name||'')}catch(e){}" 2>/dev/null || echo "")"
632
+
633
+ cat > "$target" << 'MAKEFILE_EOF'
634
+ # Makefile
635
+ # Generated by @zairakai/js-dev-tools setup-project.sh
636
+ # Run `make help` to see available commands.
637
+ #
638
+ # To regenerate:
639
+ # bash node_modules/@zairakai/js-dev-tools/scripts/setup-project.sh --with-makefile --force
640
+
641
+ MAKEFILE_EOF
642
+
643
+ if [[ -n "$package_name" ]]; then
644
+ echo "NPM_DIRECTORY_TOOLS_PROJECT_NAME := \"${package_name}\"" >> "$target"
645
+ echo "" >> "$target"
646
+ fi
647
+
648
+ cat >> "$target" << 'MAKEFILE_EOF'
649
+ DEV_TOOLS_NPM := node_modules/@zairakai/js-dev-tools
650
+
651
+ include $(DEV_TOOLS_NPM)/tools/make/core.mk
652
+ MAKEFILE_EOF
653
+
654
+ track_created "Makefile"
655
+ log_success "Created: Makefile"
656
+ }
657
+
658
+ inject_makefile() {
659
+ local target="${PROJECT_ROOT}/Makefile"
660
+ local marker="# @zairakai/js-dev-tools"
661
+
662
+ if grep -qF "$marker" "$target" 2>/dev/null; then
663
+ log_info "Makefile already includes @zairakai/js-dev-tools — skipping"
664
+ track_skipped "Makefile (already injected)"
665
+ return 0
666
+ fi
667
+
668
+ log_info "Existing Makefile detected — injecting @zairakai/js-dev-tools includes"
669
+
670
+ cat >> "$target" << 'INJECT_EOF'
671
+
672
+ # ─── @zairakai/js-dev-tools ─────────────────────────────────────────────────────
673
+ # Injected by: bash node_modules/@zairakai/js-dev-tools/scripts/setup-project.sh --with-makefile
674
+ # Provides: eslint, prettier, stylelint, markdownlint, shellcheck, typecheck, build, knip, test, quality, bats targets
675
+ DEV_TOOLS_NPM := node_modules/@zairakai/js-dev-tools
676
+ -include $(DEV_TOOLS_NPM)/tools/make/variables.mk
677
+ -include $(DEV_TOOLS_NPM)/tools/make/help.mk
678
+ -include $(DEV_TOOLS_NPM)/tools/make/code-style.mk
679
+ -include $(DEV_TOOLS_NPM)/tools/make/stylelint.mk
680
+ -include $(DEV_TOOLS_NPM)/tools/make/markdownlint.mk
681
+ -include $(DEV_TOOLS_NPM)/tools/make/shellcheck.mk
682
+ -include $(DEV_TOOLS_NPM)/tools/make/typescript.mk
683
+ -include $(DEV_TOOLS_NPM)/tools/make/quality.mk
684
+ -include $(DEV_TOOLS_NPM)/tools/make/test.mk
685
+ -include $(DEV_TOOLS_NPM)/tools/make/bats.mk
686
+ INJECT_EOF
687
+
688
+ track_created "Makefile (injected)"
689
+ log_success "Injected @zairakai/js-dev-tools targets into existing Makefile"
690
+ log_info "Note: 'make help' will show JS targets alongside your existing targets"
691
+ }
692
+
693
+ setup_makefile() {
694
+ local target="${PROJECT_ROOT}/Makefile"
695
+
696
+ if [[ -f "$target" ]]; then
697
+ inject_makefile
698
+ else
699
+ generate_makefile
700
+ fi
701
+ }
702
+
703
+ # ============================================================================
704
+ # Main Setup
705
+ # ============================================================================
706
+
707
+ setup_project() {
708
+ [[ "$SILENT_MODE" != "true" ]] && {
709
+ echo ""
710
+ echo -e "${MAGENTA}📦 NPM Dev Tools Setup${NC}"
711
+ echo -e "${CYAN}Type:${NC} ${PROJECT_TYPE} | ${CYAN}Package manager:${NC} ${PM}"
712
+ echo ""
713
+ }
714
+
715
+ # ─── Publish mode ────────────────────────────────────────────────────────
716
+ if [[ -n "$PUBLISH_TARGET" ]]; then
717
+ do_publish
718
+
719
+ [[ "$WITH_MAKEFILE" == "true" ]] && setup_makefile
720
+
721
+ echo ""
722
+ echo -e "${MAGENTA}┌────────────────────────────────────────┐${NC}"
723
+ echo -e "${MAGENTA}│${NC} ${GREEN}Publish Complete${NC} ${MAGENTA}│${NC}"
724
+ echo -e "${MAGENTA}├────────────────────────────────────────┤${NC}"
725
+
726
+ for f in "${PUBLISHED_FILES[@]}"; do
727
+ echo -e "${MAGENTA}│${NC} ${GREEN}✓${NC} ${f}"
728
+ done
729
+
730
+ if [[ ${#CREATED_FILES[@]} -gt 0 ]]; then
731
+ for f in "${CREATED_FILES[@]}"; do
732
+ echo -e "${MAGENTA}│${NC} ${GREEN}✓${NC} ${f}"
733
+ done
734
+ fi
735
+
736
+ if [[ ${#SKIPPED_FILES[@]} -gt 0 ]]; then
737
+ echo -e "${MAGENTA}│${NC} ${YELLOW}○ Skipped (modified):${NC} ${SKIPPED_FILES[*]}"
738
+ echo -e "${MAGENTA}│${NC} ${YELLOW}→ Use --force to overwrite${NC}"
739
+ fi
740
+
741
+ if [[ ${#BACKED_UP_FILES[@]} -gt 0 ]]; then
742
+ echo -e "${MAGENTA}│${NC} ${CYAN}↩ Backed up:${NC} ${BACKED_UP_FILES[*]}"
743
+ echo -e "${MAGENTA}│${NC} ${CYAN}→ ${BACKUP_DIR}${NC}"
744
+ fi
745
+
746
+ echo -e "${MAGENTA}├────────────────────────────────────────┤${NC}"
747
+ echo -e "${MAGENTA}│${NC} ${CYAN}Edit configs in:${NC} config/dev-tools/"
748
+ echo -e "${MAGENTA}│${NC} ${CYAN}Help:${NC} bash setup-project.sh --help"
749
+ echo -e "${MAGENTA}└────────────────────────────────────────┘${NC}"
750
+ echo ""
751
+ exit 0
752
+ fi
753
+
754
+ # ─── Normal setup ────────────────────────────────────────────────────────
755
+
756
+ ensure_dir "${PROJECT_ROOT}/config/dev-tools"
757
+
758
+ if [[ "$PROJECT_TYPE" == "laravel-app" ]] \
759
+ && [[ -f "${PROJECT_ROOT}/vendor/zairakai/laravel-dev-tools/scripts/setup-package.sh" ]]; then
760
+ log_info "Laravel app detected — delegating setup to laravel-dev-tools (full-stack)"
761
+ extra_args=()
762
+ [[ "$FORCE_OVERWRITE" == "true" ]] && extra_args+=("--force")
763
+ [[ "$SILENT_MODE" == "true" ]] && extra_args+=("--silent")
764
+ bash "${PROJECT_ROOT}/vendor/zairakai/laravel-dev-tools/scripts/setup-package.sh" --fullstack "${extra_args[@]}"
765
+ else
766
+ if [[ "$WITH_MAKEFILE" == "true" ]] || [[ ! -f "${PROJECT_ROOT}/Makefile" ]]; then
767
+ setup_makefile
768
+ fi
769
+
770
+ # .editorconfig (always copied - needed by IDEs)
771
+ setup_file "${DEV_TOOLS_ROOT}/.editorconfig" "${PROJECT_ROOT}/.editorconfig" ".editorconfig"
772
+
773
+ # ESLint baseline (published config only)
774
+ setup_file "${DEV_TOOLS_ROOT}/stubs/quality/eslint.config.js.stub" \
775
+ "${PROJECT_ROOT}/config/dev-tools/eslint.config.js" \
776
+ "config/dev-tools/eslint.config.js"
777
+ fi
778
+
779
+ # ─── Summary ─────────────────────────────────────────────────────────────
780
+ if [[ "$SILENT_MODE" == "true" ]]; then
781
+ check_optional_deps
782
+ [[ ${#CREATED_FILES[@]} -eq 0 ]] && exit 0
783
+ fi
784
+
785
+ echo ""
786
+ echo -e "${MAGENTA}┌────────────────────────────────────────┐${NC}"
787
+ echo -e "${MAGENTA}│${NC} ${GREEN}Setup Complete${NC} ${MAGENTA}│${NC}"
788
+ echo -e "${MAGENTA}├────────────────────────────────────────┤${NC}"
789
+
790
+ if [[ ${#CREATED_FILES[@]} -gt 0 ]]; then
791
+ echo -e "${MAGENTA}│${NC} ${GREEN}✓ Created:${NC} ${CREATED_FILES[*]}"
792
+ fi
793
+
794
+ if [[ ${#SKIPPED_FILES[@]} -gt 0 ]]; then
795
+ echo -e "${MAGENTA}│${NC} ${YELLOW}○ Skipped:${NC} ${SKIPPED_FILES[*]}"
796
+ fi
797
+
798
+ if [[ ${#BACKED_UP_FILES[@]} -gt 0 ]]; then
799
+ echo -e "${MAGENTA}│${NC} ${CYAN}↩ Backed up:${NC} ${BACKED_UP_FILES[*]}"
800
+ echo -e "${MAGENTA}│${NC} ${CYAN}→ ${BACKUP_DIR}${NC}"
801
+ fi
802
+
803
+ echo -e "${MAGENTA}├────────────────────────────────────────┤${NC}"
804
+ echo -e "${MAGENTA}│${NC} ${CYAN}Type:${NC} ${PROJECT_TYPE} | ${CYAN}PM:${NC} ${PM}"
805
+ echo -e "${MAGENTA}│${NC} ${CYAN}Publish configs:${NC} bash setup-project.sh --publish"
806
+ echo -e "${MAGENTA}│${NC} ${CYAN}Run:${NC} make help"
807
+ echo -e "${MAGENTA}└────────────────────────────────────────┘${NC}"
808
+
809
+ check_optional_deps
810
+ }
811
+
812
+ # ============================================================================
813
+ # Execute
814
+ # ============================================================================
815
+
816
+ setup_project