@zairakai/dev-tools 1.0.11

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