dev / bin / ardz
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
#!/usr/bin/env bash
# ardz — the ardegazu ecosystem's dev management CLI.
#
# One manifest (apps.tsv), one command. Encodes the house rules so nobody has
# to re-learn them: anonymous per-repo git identities, the publish anonymity
# gate, node >=22 for the relay, sha-pinned kit dependencies (npm silently
# reuses stale lockfile resolutions for git deps — always the explicit
# `npm install pkg@git+…#<sha>` form), `site:<host>` pin naming, and NEVER
# `git add -A` in policy repos (that's how the assistant-config dir once leaked into a
# mirror build).
#
#   ardz status                  repo table: branch, dirty, version, last commit
#   ardz doctor                  environment + policy checks
#   ardz workspace               clone anything missing from git.ardegazu.ro,
#                                set anonymous identities, npm install clients
#   ardz dev <name>              dev server with the right env (bridge URL)
#   ardz relay                   the dev relay on :9090 (needs node >= 22)
#   ardz build <names…|all>      tsc + vite build
#   ardz release <names…|all>    bump (versioned apps) + build + deploy to IPFS/IPNS
#   ardz publish-src <names…|all>  anonymity-gated source mirrors -> git.ardegazu.ro
#   ardz kit-release <kit>       tag + publish a package mirror, wait for the
#                                gateway, then sha-bump every consumer
#   ardz new <name> <host>       scaffold a new p2p game from game1 (neon-grid)
#   ardz new-app <name> <host> <live|rooms>  scaffold a p2p app, ClojureScript
#                                (live = game stack, game1 is the canon; rooms =
#                                durable log — the shared core comes from
#                                ardegazu-rooms-kit over the deps.edn classpath,
#                                chat is the canon for the rest —
#                                dev/docs/CLJS.md)
#   ardz new-bot <name>          scaffold a resident bot (ClojureScript: four
#                                isolated shadow-cljs builds, two-stack law)
#   ardz home                    write the ecosystem memory file into workspace roots
#
# macOS bash 3.2 compatible. Needs: git, node, npm, jq, curl, rsync, ird (logged in).
set -euo pipefail

DEV_DIR="$(cd "$(dirname "$0")/.." && pwd)"
WORKSPACE="$(cd "$DEV_DIR/.." && pwd)"
MANIFEST="$DEV_DIR/apps.tsv"
GITHOST="git.ardegazu.ro"
ASSEMBLER="${ARDZ_ASSEMBLER:-$WORKSPACE/git}"
NODE22="${ARDZ_NODE22:-}"

say()  { printf '%s\n' "$*"; }
warn() { printf '⚠ %s\n' "$*" >&2; }
die()  { printf 'ABORT: %s\n' "$*" >&2; exit 1; }

# manifest dirs are workspace-relative; ~ and absolute paths work as overrides
expand() { case "$1" in "~"*) printf '%s%s' "$HOME" "${1#\~}";; /*) printf '%s' "$1";; *) printf '%s/%s' "$WORKSPACE" "$1";; esac; }

# each() FN [names…] — run FN name dir host kind ident versioned per manifest row
# Read the manifest on fd 3, NOT stdin. A row-fn spawns children (`ird`, npm)
# and `ird` DRAINS stdin — with the loop reading fd 0 it swallowed the rest of
# apps.tsv, so `ardz release game2 game3 …` deployed only the FIRST name and
# exited 0. A silent partial release, not an error. Keep the redirect on fd 3.
each() {
  local fn="$1"; shift
  local filter="$*"
  while IFS="$(printf '\t')" read -r name dir host kind ident versioned <&3; do
    case "$name" in ''|'#'*) continue;; esac
    if [ -n "$filter" ] && [ "$filter" != "all" ]; then
      case " $filter " in *" $name "*) ;; *) continue;; esac
    fi
    # </dev/null too: fd 3 keeps the manifest safe, and this keeps a child
    # that reads stdin (ird does) from eating the caller's terminal instead.
    "$fn" "$name" "$(expand "$dir")" "$host" "$kind" "$ident" "$versioned" </dev/null
  done 3< "$MANIFEST"
}

# _dir_of NAME — print the (expanded) manifest dir for one row
_dir_of() {
  local want="$1"
  _dir_of_row() { [ "$1" = "$want" ] && printf '%s' "$2"; return 0; }
  each _dir_of_row "$want"
}

find_node22() {
  [ -n "$NODE22" ] && { printf '%s' "$NODE22"; return; }
  local v
  for v in "$HOME"/.nvm/versions/node/v2[2-9]*/bin/node; do
    [ -x "$v" ] && { printf '%s' "$v"; return; }
  done
  node -e 'process.exit(parseInt(process.versions.node)>=22?0:1)' 2>/dev/null && { command -v node; return; }
  return 1
}

# ---- status -----------------------------------------------------------------

_status_row() {
  local name="$1" dir="$2" host="$3" kind="$4"
  if [ ! -d "$dir/.git" ]; then printf '%-11s %-8s MISSING (%s)\n' "$name" "$kind" "$dir"; return; fi
  local dirty ver last
  dirty=$(git -C "$dir" status --porcelain | wc -l | tr -d ' ')
  ver="-"
  [ -f "$dir/client/version.json" ] && ver="v$(jq -r .version "$dir/client/version.json")"
  [ -f "$dir/package.json" ] && [ ! -d "$dir/client" ] && ver="v$(jq -r .version "$dir/package.json")"
  last=$(git -C "$dir" log --format='%h %s' -1 2>/dev/null | cut -c1-56)
  [ -n "$last" ] || last="(no commits yet)"
  printf '%-11s %-8s %-5s dirty:%-3s %s\n' "$name" "$kind" "$ver" "$dirty" "$last"
}
cmd_status() { each _status_row; }

# ---- doctor -----------------------------------------------------------------

_doctor_row() {
  local name="$1" dir="$2" host="$3" kind="$4" ident="$5"
  [ -d "$dir/.git" ] || { warn "$name: repo missing at $dir (run: ardz workspace)"; return; }
  local u e
  u=$(git -C "$dir" config user.name || true); e=$(git -C "$dir" config user.email || true)
  [ "$u" = "$ident" ] && [ "$e" = "$ident@noreply.local" ] || warn "$name: git identity is '$u <$e>' — want '$ident <$ident@noreply.local>'"
  # the board lesson: any repo whose gate reads external patterns must never track assistant config
  #
  # Match the assistant dirs by NAME, not by the prefix '^\.cl'. That prefix was
  # written when nothing else in a repo began with it; today rooms-kit tracks
  # .cljfmt.edn and every CLJS repo tracks .clj-kondo/, so it fired on the
  # tooling and said "its gate will abort" about a file that is perfectly safe.
  # A false alarm on the anonymity check is worse than no check: this is the
  # one warning nobody can afford to learn to scroll past.
  if [ -f "$dir/deploy/publish-repo.sh" ] && grep -q IDPAT_FILE "$dir/deploy/publish-repo.sh"; then
    ! git -C "$dir" ls-files \
      | grep -qiE '^\.(claude|cursor|aider|codeium|continue|windsurf|github/copilot)(/|$)' \
      || warn "$name: assistant config dir is TRACKED — its gate will abort"
  fi
}
# scaffolding preconditions: template trees present, canon repos complete,
# and the tracked templates still placeholder-only (they ship with this repo)
_doctor_templates() {
  local t g1 ch bt
  for t in app-common app-cljs-live app-cljs-rooms bot; do
    [ -d "$DEV_DIR/templates/$t" ] || warn "template missing: dev/templates/$t (scaffold commands will fail)"
  done
  g1=$(_dir_of game1); ch=$(_dir_of chat); bt=$(_dir_of bot)
  # both app canons are CLJS since the migration — the scaffolder vendors from them
  [ -f "$g1/client/src/game1/net/peers.cljs" ] || warn "live canon incomplete: game1 client/src/game1/net/peers.cljs missing"
  [ -f "$g1/client/src/game1/i18n/runtime.cljs" ] || warn "i18n canon incomplete: game1 client/src/game1/i18n/runtime.cljs missing"
  # the rooms scaffold copies _rooms_vendor_list out of chat and NOTHING else —
  # the shared core (js.cljs, lib/{access,crypto,descriptor,encryption,log,net,
  # orbit-identity,protocol,turn}) is ardegazu-rooms-kit's, reached over the
  # classpath, so it is not copied and cannot be missing from chat
  _rooms_canon_missing "$ch" | while read -r p; do
    warn "rooms canon incomplete (ardz new-app <…> rooms cannot scaffold): chat $p"
  done
  [ -f "$bt/src/bot/rooms.cljs" ] || warn "bot canon incomplete: bot src/bot/rooms.cljs missing"
  for t in app-common app-cljs-live app-cljs-rooms bot; do
    grep -rq -- '__NAME__' "$DEV_DIR/templates/$t" 2>/dev/null \
      || warn "$t template lost its __NAME__ placeholders — substitution scheme broken?"
  done
  return 0
}

cmd_doctor() {
  say "== environment"
  command -v ird >/dev/null || warn "ird CLI missing"
  ird ipfs pins --json >/dev/null 2>&1 && say "✓ ird logged in" || warn "ird not logged in"
  command -v jq >/dev/null && say "✓ jq" || warn "jq missing"
  if N22=$(find_node22); then say "✓ node>=22 at $N22 (relay + engines)"; else warn "no node >= 22 found (nvm install 22) — the dev relay will crash on older node"; fi
  # cljs repos build with shadow-cljs on the JVM (dev machine only — the VPS never builds)
  JV=$(java -version 2>&1 | head -1 | sed 's/.*"\([0-9]*\)[.\"].*/\1/') || true
  if [ -n "$JV" ] && [ "$JV" -ge 17 ] 2>/dev/null; then say "✓ java $JV (shadow-cljs builds)"; else warn "no JDK >= 17 found — cljs repos will not build (see dev/docs/CLJS.md)"; fi
  command -v clojure >/dev/null && say "✓ clojure CLI" || warn "clojure CLI missing — cider/deps.edn workflow needs it (see dev/docs/CLJS.md)"
  [ -s "$HOME/.config/cod-sursa/idpat" ] && say "✓ identity patterns file present" || warn "~/.config/cod-sursa/idpat missing — newer publish gates refuse to run blind"
  [ -d "$ASSEMBLER" ] && say "✓ assembler at $ASSEMBLER" || warn "assembler dir missing ($ASSEMBLER)"
  say "== templates"
  _doctor_templates
  say "== repos"
  each _doctor_row
  say "doctor done — warnings above (if any) are the fixes to make"
}

# ---- workspace --------------------------------------------------------------

_ws_row() {
  local name="$1" dir="$2" host="$3" kind="$4" ident="$5"
  if [ ! -d "$dir/.git" ]; then
    say "== cloning $name -> $dir"
    mkdir -p "$(dirname "$dir")"
    git clone "https://$GITHOST/$name.git" "$dir"
  fi
  git -C "$dir" config user.name "$ident"
  git -C "$dir" config user.email "$ident@noreply.local"
  if [ -f "$dir/client/package.json" ] && [ ! -d "$dir/client/node_modules" ]; then
    say "== npm install $name/client"
    (cd "$dir/client" && npm install --no-fund --no-audit)
  fi
}
cmd_workspace() {
  each _ws_row
  say "workspace ready — run 'ardz doctor' to verify, 'ardz home' for the memory file"
}

# ---- dev / relay ------------------------------------------------------------

# _stage_id_bridge APPDIR — put the hub's /id/ bridge into APPDIR/client/dist-dev/id/.
#
# The default VITE_ID_BRIDGE_URL is http://localhost:4173/id/ — and whenever the
# app under development itself owned 4173, /id/ SPA-fell-back to the app's own
# index.html, the IdBridge boot timed out, and every dev tab silently ran on a
# per-origin FALLBACK identity (masking all identity-dependent behavior in dev).
# Staging the hub's real bridge into the app's own dev tree fixes that with no
# new port (the id-kit host allowlist covers localhost:4173/5173 only, so a
# third port was never an option) and no production traffic: the identity
# record lives in the localhost:4173 origin's localStorage — one REAL bridged
# identity shared by every app's dev session, since they all serve 4173 — and
# never reaches ardegazu.ro. (Pointing dev at the production bridge instead
# would be blocked in practice by third-party storage partitioning: localhost
# embedding ardegazu.ro is CROSS-site, so browsers give that iframe an
# isolated, partitioned localStorage — or none at all.)
# dist-dev/ is gitignored; the copy is refreshed on every run.
_stage_id_bridge() {
  local app="$1" hub
  hub=$(_dir_of home)
  [ -n "$hub" ] && [ -d "$hub/client" ] || die "home (the bridge canon) not found — run: ardz workspace"
  if [ ! -f "$hub/client/dist/id/assets/bridge.js" ]; then
    say "== hub bridge not built yet — one-time ':bridge' release in home/client (JVM >= 17)"
    (cd "$hub/client" && node scripts/patch-npm.mjs && node_modules/.bin/shadow-cljs release bridge) \
      || die "could not build the hub bridge (home/client needs npm install + JVM >= 17), or set VITE_ID_BRIDGE_URL yourself"
  fi
  rm -rf "$app/client/dist-dev/id"
  mkdir -p "$app/client/dist-dev/id"
  cp "$hub/client/public/id/index.html" "$app/client/dist-dev/id/index.html"
  cp -R "$hub/client/dist/id/assets" "$app/client/dist-dev/id/assets"
}

cmd_dev() {
  local target="${1:?usage: ardz dev <name>}"
  _dev_row() {
    [ "$1" = "$target" ] || return 0
    local dir="$2"
    [ -d "$dir/client" ] || die "$1 has no client/"
    local bridge_url="${VITE_ID_BRIDGE_URL:-}"
    if [ -n "$bridge_url" ]; then
      say "id bridge -> $bridge_url (VITE_ID_BRIDGE_URL override; nothing staged)"
    else
      bridge_url="http://localhost:4173/id/"
      _stage_id_bridge "$dir"
      say "id bridge -> $bridge_url (hub bridge staged at client/dist-dev/id/ — dev tabs share one real bridged identity on the localhost:4173 origin)"
    fi
    say "dev server for $1"
    cd "$dir/client" && VITE_ID_BRIDGE_URL="$bridge_url" exec npm run dev
  }
  each _dev_row "$target"
}

cmd_relay() {
  local n22
  n22=$(find_node22) || die "the dev relay needs node >= 22 (nvm install 22)"
  local chat_dir relay
  chat_dir=$(_dir_of chat)
  [ -n "$chat_dir" ] || die "no 'chat' row in the manifest"
  relay="$chat_dir/deploy/relay"
  [ -f "$relay/relay.mjs" ] || die "relay not found at $relay"
  say "dev relay on :9090 (node: $n22)"
  cd "$relay" && exec "$n22" relay.mjs --dev
}

# ---- build / release --------------------------------------------------------

_build_row() {
  local name="$1" dir="$2" host="$3" kind="$4"
  if [ "$kind" = "kit" ]; then
    [ -f "$dir/package.json" ] || return 0
    say "== check $name"
    (cd "$dir" && npm run check >/dev/null) && say "   ✓ tsc" || die "$name typecheck failed"
    return 0
  fi
  say "== build $name"
  (cd "$dir/client" && npm run build >/dev/null 2>&1) || die "$name build failed — run it manually for output"
  say "   ✓ $host"
}
cmd_build() { [ $# -ge 1 ] || die "usage: ardz build <names…|all>"; each _build_row "$@"; }

# deploy one dist: ipfs add -> re-pin as site:<host> -> ipns publish -> unpin
# previous site:<host> pin ONLY (never touches offline:/other-named pins)
_deploy() {
  local host="$1" dist="$2"
  local pin_name="site:$host" prev prev_id cid tmp_id
  prev=$(ird ipfs pins --json | jq -r --arg n "$pin_name" '[.pins[]? | select(.name==$n and .state=="pinned")][0].cid // empty')
  # Keep the pin ID, not just the CID: the old site pin is retired by ID below.
  # A CID can carry SEVERAL pins — taking a `rollback:<app>` pin on the live CID
  # before a release is exactly the right thing to do (the previous CID is
  # otherwise unpinned here and becomes GC-eligible, so a rollback would have
  # nothing to point at), and it makes `pin rm <cid>` ambiguous and fail.
  prev_id=$(ird ipfs pins --json | jq -r --arg n "$pin_name" '[.pins[]? | select(.name==$n and .state=="pinned")][0].id // empty')
  cid=$(ird ipfs add "$dist" --json --yes | sed -n '/^{/,$p' | jq -r '.root_cid // .pin.cid')
  [ -n "$cid" ] && [ "$cid" != "null" ] || die "$host: could not read CID from ird ipfs add"
  if [ "$cid" = "$prev" ]; then
    say "   = $host unchanged ($cid)"
    # drop the freshly-added duplicate folder-named pin, keep the site pin
    tmp_id=$(ird ipfs pins --json | jq -r --arg c "$cid" --arg n "$(basename "$dist")" '[.pins[]? | select(.cid==$c and .name==$n)][0].id // empty')
    [ -n "$tmp_id" ] && ird ipfs pin rm "$tmp_id" --yes >/dev/null || true
    return 0
  fi
  ird ipfs pin "$cid" --name "$pin_name" --yes >/dev/null
  tmp_id=$(ird ipfs pins --json | jq -r --arg c "$cid" --arg n "$(basename "$dist")" '[.pins[]? | select(.cid==$c and .name==$n)][0].id // empty')
  [ -n "$tmp_id" ] && ird ipfs pin rm "$tmp_id" --yes >/dev/null || true
  ird ipfs ipns publish "$host" "$cid" --yes >/dev/null
  if [ -n "$prev_id" ]; then
    ird ipfs pin rm "$prev_id" --yes >/dev/null \
      || warn "$host: could not retire the previous site pin ($prev) — remove it by ID"
  fi
  # the previous CID is the rollback target — print it so every release
  # leaves it in the log, whether or not a rollback pin was taken. The `||
  # true` is load-bearing: on a FIRST release there is no previous CID, and a
  # bare `[ -n ... ] && say` as the last statement made the whole deploy
  # exit 1 over nothing.
  say "   ✓ $host -> $cid (propagates within minutes)"
  [ -z "$prev" ] || say "     rollback target: $prev"
}

_release_row() {
  local name="$1" dir="$2" host="$3" kind="$4" ident="$5" versioned="$6"
  if [ "$kind" = "kit" ]; then
    # a kit without a host is source-only (use kit-release); a kit WITH a
    # host also serves a built dist/ (e.g. theme -> cdn.ardegazu.ro)
    [ "$host" = "-" ] && { say "== $name is a package — use 'ardz kit-release $name'"; return 0; }
    say "== release $name"
    (cd "$dir" && npm run build >/dev/null 2>&1) || die "$name build failed"
    _deploy "$host" "$dir/dist"
    return 0
  fi
  say "== release $name"
  if [ "$versioned" = "y" ]; then
    (cd "$dir/client" && npm run release >/dev/null 2>&1) || die "$name release build failed"
    say "   bumped to v$(jq -r .version "$dir/client/version.json") — remember to commit"
  else
    (cd "$dir/client" && npm run build >/dev/null 2>&1) || die "$name build failed"
  fi
  _deploy "$host" "$dir/client/dist"
}
cmd_release() { [ $# -ge 1 ] || die "usage: ardz release <names…|all>"; each _release_row "$@"; }

# ---- source mirrors ---------------------------------------------------------

cmd_publish_src() {
  [ $# -ge 1 ] || die "usage: ardz publish-src <names…|all>"
  [ -x "$ASSEMBLER/assemble.sh" ] || die "assembler not found at $ASSEMBLER"
  if [ "$1" = "all" ]; then (cd "$ASSEMBLER" && ./assemble.sh); else (cd "$ASSEMBLER" && ./assemble.sh "$@"); fi
}

# ---- kits -------------------------------------------------------------------

_consumers_bump() {
  local kit="$1" sha="$2" pkg="$3"
  _bump_row() {
    local name="$1" dir="$2" kind="$4"
    # a kit is never a consumer of itself
    [ "$name" = "$kit" ] && return 0
    # The package.json is under client/ for apps but at the REPO ROOT for the
    # kits and the bot. This used to `return 0` on kind=kit and require
    # client/package.json, so peer-kit (root, kind=kit) and bot (root) were
    # skipped silently — both sat on stale kit pins through two whole kit
    # rewrites, and peer-kit's was shipping social-kit 2.1.0 inside its dist.
    local pdir rel
    if   [ -f "$dir/client/package.json" ]; then pdir="$dir/client"; rel="client/"
    elif [ -f "$dir/package.json" ];        then pdir="$dir";        rel=""
    else return 0; fi
    grep -q "\"$pkg\"" "$pdir/package.json" || return 0
    say "== bump $name -> $pkg#$sha"
    # A repo that TRACKS dist/ ships those bytes, and a kit compiles its CLJS
    # dependencies from classpath source (peer-kit :paths social-kit's src), so
    # the bump does not exist until dist is rebuilt AND committed. Build it
    # COLD: a warm .shadow-cljs draws Closure property renames from a different
    # pool (dev/docs/CLJS.md), and one source gave three different dist hashes.
    local tracks_dist=0
    git -C "$dir" ls-files --error-unmatch "${rel}dist" >/dev/null 2>&1 && tracks_dist=1
    # WHICH SECTION declares it? `npm install <spec>` writes to `dependencies`
    # by default — but every KIT in this suite declares its sibling kits as
    # `devDependencies` (pinned) plus a `"*"` `peerDependencies` entry, so the
    # default form fights the existing declaration instead of updating it and
    # the bump dies here. That is why social-kit, wallet-kit, rooms-kit and
    # peer-kit all sat on a stale id-kit pin while every APP (which uses plain
    # `dependencies`) bumped cleanly. Match the section that already exists.
    local save_flag="--save-prod"
    node -e "const p=require('$pdir/package.json');process.exit(p.devDependencies&&p.devDependencies['$pkg']?0:1)" \
      >/dev/null 2>&1 && save_flag="--save-dev"
    # THE npm git-dep gotcha: a plain install reuses the stale lockfile
    # resolution even when the committish changes — the explicit spec form
    # is the only reliable bump. Retry once (gateway can be flappy mid-IPNS).
    local try ok=0
    for try in 1 2; do
      (cd "$pdir" \
        && npm install --no-fund --no-audit "$save_flag" "$pkg@git+https://$GITHOST/$kit.git#$sha" >/dev/null 2>&1 \
        && { [ "$tracks_dist" = 1 ] && rm -rf .shadow-cljs; true; } \
        && npm run build >/dev/null 2>&1 \
        && node -e "const l=require('./package-lock.json');process.exit(l.packages['node_modules/$pkg'].resolved.includes('$sha'.slice(0,7))?0:1)") && { ok=1; break; }
    done
    [ "$ok" = 1 ] || die "$name failed to bump/build"
    # never add -A: only the dependency files, plus dist where the repo ships it
    local paths="${rel}package.json ${rel}package-lock.json"
    [ "$tracks_dist" = 1 ] && paths="$paths ${rel}dist"
    (cd "$dir" && git add $paths \
      && git -c commit.gpgsign=false commit -q -m "deps: $pkg -> $sha" 2>/dev/null) \
      && say "   committed" || say "   (nothing to commit)"
    if [ "$kind" = "kit" ]; then
      warn "$name is a KIT and was bumped — it needs its own 'ardz kit-release $name'"
    fi
    return 0
  }
  each _bump_row
}

cmd_kit_release() {
  local kit="${1:?usage: ardz kit-release <kit>}"
  _kit_row() {
    [ "$1" = "$kit" ] || return 0
    local dir="$2"
    [ -f "$dir/package.json" ] || die "$kit has no package.json"
    (cd "$dir" && [ -z "$(git status --porcelain)" ]) || die "$kit tree is dirty — commit first"
    (cd "$dir" && npm run check >/dev/null) || die "$kit typecheck failed"
    # `check` COMPILES but never BUILDS, so kit-release used to publish whatever
    # dist happened to be committed. A kit whose dist embeds a bumped dependency
    # (peer-kit compiles social-kit from classpath source) would ship the old
    # one silently. check-dist.sh rebuilds cold and fails if dist is stale.
    if [ -x "$dir/deploy/check-dist.sh" ]; then
      (cd "$dir" && ./deploy/check-dist.sh >/dev/null) \
        || die "$kit: deploy/check-dist.sh failed — the committed dist is stale (rebuild cold and commit it) or it leaked a path"
      say "   ✓ committed dist matches a cold build"
    else
      warn "$kit has no deploy/check-dist.sh — its committed dist is unverified"
    fi
    local ver sha pkg
    ver=$(jq -r .version "$dir/package.json")
    pkg=$(jq -r .name "$dir/package.json")
    (cd "$dir" && git tag -f "v$ver" >/dev/null)
    sha=$(git -C "$dir" rev-parse HEAD)
    say "== publish $kit v$ver ($sha)"
    cmd_publish_src "$kit"
    say "== waiting for the gateway to serve $sha"
    until curl -s --max-time 10 "https://$GITHOST/$kit.git/info/refs" 2>/dev/null | grep -q "$sha"; do sleep 15; done
    say "   ✓ live"
    _consumers_bump "$kit" "$sha" "$pkg"
    say "kit released — now 'ardz release all' to deploy the rebuilt apps, then 'ardz publish-src all'"
  }
  each _kit_row "$kit"
}

# ---- scaffold helpers -------------------------------------------------------

# _rooms_vendor_list — the files `ardz new-app <name> <host> rooms` copies out
# of the chat canon at scaffold time, and EXACTLY what `ardz doctor` asserts is
# still there. One list, so the scaffolder and its own precondition check cannot
# drift apart (they did: doctor warned about paths the scaffolder no longer
# needed while the scaffolder died inside _vendor on paths doctor never checked,
# leaving a git-inited directory behind).
#
# What is deliberately NOT here is the point of the list: the shared rooms core
# — js.cljs and lib/{access,crypto,descriptor,encryption,log,net,orbit-identity,
# protocol,turn} — has since 2026-08-28 exactly one source in the suite,
# ardegazu-rooms-kit. An app reaches it over the CLASSPATH (the template's
# client/deps.edn puts node_modules/ardegazu-rooms-kit/src on :paths, and the
# npm sha pin in the vendored package.json decides which sha sits there), so
# there is nothing to copy and nothing to keep byte-identical. What is left is
# what the kit has no counterpart for, plus the build/test plumbing.
_rooms_vendor_list() {
  cat <<'EOF'
client/src/sueta/lib/mailbox.cljs
client/src/sueta/lib/selftest.cljs
client/src/sueta/stores.cljs
client/src/sueta/fx.cljs
client/src/sueta/wire.cljs
client/src/js
client/scripts
client/test/helpers/cljs-reader.mjs
client/test/helpers/facade.mjs
client/.cljfmt.edn
client/package.json
client/package-lock.json
client/public/icons
EOF
}

# _rooms_canon_missing CHAT_DIR — print every precondition of the rooms scaffold
# that is NOT satisfied, one per line. Empty output means `ardz new-app <name>
# <host> rooms` will run to completion. `ardz doctor` warns each line; new-app
# dies on the whole list BEFORE it creates a directory, because the failure this
# replaces was a repo half-scaffolded on disk.
_rooms_canon_missing() {
  local ch="$1" p
  for p in $(_rooms_vendor_list); do
    [ -e "$ch/$p" ] || printf '%s\n' "$p"
  done
  # not a file: the template's deps.edn classpath entry
  # (node_modules/ardegazu-rooms-kit/src) only resolves because the vendored
  # package.json sha-pins the kit. No pin, no shared core, and the scaffold's
  # first `npm run build` dies on an unresolved ardegazu.rooms.* require.
  grep -q '"ardegazu-rooms-kit"' "$ch/client/package.json" 2>/dev/null \
    || printf '%s\n' 'client/package.json has no ardegazu-rooms-kit sha pin (the deps.edn classpath entry would point at nothing)'
  return 0
}

# _scaffold_tree DEST TPL… — layer template trees, then activate dot-less files
# (tracked templates never carry live dotfiles — the publish-gate .cl* rule)
_scaffold_tree() {
  local dest="$1"; shift
  local tpl
  for tpl in "$@"; do
    [ -d "$DEV_DIR/templates/$tpl" ] || die "template missing: $DEV_DIR/templates/$tpl"
    rsync -a "$DEV_DIR/templates/$tpl/" "$dest/"
  done
  # both names are assembled, never written literally: the publish gate's
  # private patterns forbid the assistant's name anywhere in tracked bytes
  local tplcl livecl
  tplcl="_agentcfg"; livecl="$(printf '.cl%sude' a)"
  [ -d "$dest/$tplcl" ] && mv "$dest/$tplcl" "$dest/$livecl"
  [ -f "$dest/gitignore" ] && mv "$dest/gitignore" "$dest/.gitignore"
  return 0
}

# _scaffold_subst DEST NAME HOST SUB — fill __NAME__/__HOST__/__SUB__ in file
# contents, then in dir/file names (systemd units and the cljs templates'
# src/__NAME__/ namespace root carry the name token)
_scaffold_subst() {
  local dest="$1" name="$2" host="$3" sub="$4" f
  { grep -rlE -- '__NAME__|__HOST__|__SUB__' "$dest" 2>/dev/null || true; } | while read -r f; do
    perl -pi -e "s/__NAME__/$name/g; s/__HOST__/$host/g; s/__SUB__/$sub/g" "$f"
  done
  # directories first (topmost first, one per pass — renames shift child paths)
  while f=$({ find "$dest" -type d -name '*__NAME__*' 2>/dev/null || true; } | head -1) && [ -n "$f" ]; do
    mv "$f" "$(printf '%s' "$f" | sed "s/__NAME__/$name/g")"
  done
  { find "$dest" -type f -name '*__NAME__*' 2>/dev/null || true; } | while read -r f; do
    mv "$f" "$(printf '%s' "$f" | sed "s/__NAME__/$name/g")"
  done
  return 0
}

# _scaffold_git DEST IDENT — anonymous identity BEFORE any commit (house rule 1)
_scaffold_git() { (cd "$1" && git init -q && git config user.name "$2" && git config user.email "$2@noreply.local"); }

# _manifest_add NAME DIR HOST KIND IDENT VERSIONED — dup-checked apps.tsv append
_manifest_add() {
  if grep -q "^$1$(printf '\t')" "$MANIFEST"; then die "apps.tsv already has a row named '$1'"; fi
  printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" "$6" >> "$MANIFEST"
}

# _vendor SRC DEST PATH… — copy canon-governed files at scaffold time (the
# byte-identical rule: templates never carry their own copies to drift)
_vendor() {
  local src="$1" dest="$2"; shift 2
  local p
  for p in "$@"; do
    [ -e "$src/$p" ] || die "canon file missing: $src/$p (run: ardz workspace)"
    mkdir -p "$dest/$(dirname "$p")"
    cp -R "$src/$p" "$dest/$p"
  done
}

# _ns_rename DEST NAME — re-home the vendored CLJS sources under the app's own
# namespace root. The CLJS vendoring rule (house rule 6, dev/docs/CLJS.md): the
# vendored files stay byte-identical to their canon modulo this rename
# (`game1.`/`sueta.` -> `<name>.`, `src/game1`/`src/sueta` -> `src/<name>`)
# plus, for the live stack only, the one protocol-prefix line in net/peers.cljs.
# It runs over client/test too, because the vendored test helpers and the
# template's own source lint name the tree they scan by path.
# `ardegazu.rooms.*` is untouched by construction — the shared
# rooms core is not vendored at all, it is compiled off the classpath.
# The canon's own namespace prefixes never appear in a bare-word form that would
# be caught here: the protocol ids (`/sueta/2/msg/1.0`), the OrbitDB provider
# type (`"sueta"`) and the domain tags (`sueta-id|v2`) all survive unchanged,
# which is exactly what wire compatibility with the canon needs.
_ns_rename() {
  local dest="$1" name="$2" canon f
  for canon in game1 sueta; do
    [ -d "$dest/client/src/$canon" ] || continue
    rsync -a "$dest/client/src/$canon/" "$dest/client/src/$name/"
    rm -rf "$dest/client/src/$canon"
  done
  { grep -rlE -- '(game1|sueta)' "$dest/client/src/$name" "$dest/client/scripts" \
      "$dest/client/test" 2>/dev/null || true; } \
    | while read -r f; do
    perl -pi -e "s/game1\\./$name./g; s{src/game1\\b}{src/$name}g; s/sueta\\./$name./g; s{src/sueta\\b}{src/$name}g" "$f"
  done
  return 0
}

# _pkg_rename CLIENT_DIR NAME — the vendored package files carry the canon's name
_pkg_rename() {
  local old
  old=$(jq -r .name "$1/package.json")
  perl -pi -e "s/\"\Q$old\E\"/\"$2\"/g" "$1/package.json" "$1/package-lock.json"
}

# _rooms_pkg_scripts CLIENT_DIR — the vendored `test` script is chat's, and it
# expects three things a day-zero scaffold does not have: a :testlib shadow
# build, test/vectors/*.json, and (via the `pretest` hook) the libdatachannel
# native addon that only chat's orbit-address test loads. Inherited as-is it is
# a gate that fails on the author's first `npm test`, before they have written a
# line — which is worse than shipping no gate, because it teaches them to ignore
# the one gate they have.
#
# So rewrite it down to the three checks a fresh scaffold can run HONESTLY and
# pass: cljfmt over src, a compile of the :app build, and
# test/source-hygiene.test.mjs (the template's own app-agnostic copy of the
# CLJS lint whose canon is chat's — chat's file also carries chat-only façade
# checks a fresh app cannot pass, so it is no longer vendored verbatim). All
# three are real. None pretends to test an app that does not exist yet, and client/test/README.md says exactly what to add, in what order, to get
# the rest back.
#
# check:app earns its place the hard way. The vendored lib/mailbox.cljs requires
# promesa.core, <name>.fx and <name>.wire; for a long while the template shipped
# none of them, so every rooms scaffold's :app build — the one that actually
# reaches a browser — did not compile, and every scaffold's `npm test` was green
# anyway, because nothing in the suite ever built :app. A gate that never
# compiles what ships is a gate that passes by not looking. It compiles into a
# throwaway target/check-app, so it costs a compile and writes nothing to dist/.
#
# The glob is UNQUOTED on purpose: `node --test 'test/*.test.mjs'` is not
# expanded by the shell and node does not expand it either on every version, so
# the quoted form can run ZERO tests and still exit 0 — the same failure mode as
# the paragraph above, one line further down.
_rooms_pkg_scripts() {
  local pj="$1/package.json" tmp="$1/package.json.scaffold"
  jq --indent 2 \
     --arg c "shadow-cljs compile app --config-merge '{:output-dir \"target/check-app\"}'" \
     --arg t 'clojure -M:cljfmt check src && npm run check:app && TZ=UTC node --test test/*.test.mjs' \
     '.scripts["check:app"] = $c | .scripts.test = $t | del(.scripts.pretest)' "$pj" > "$tmp"
  mv "$tmp" "$pj"
}

# ---- scaffold ---------------------------------------------------------------

cmd_new() {
  local name="${1:?usage: ardz new <name> <host>}" host="${2:?usage: ardz new <name> <host>}"
  local ref dir="$WORKSPACE/$name"
  ref=$(_dir_of game1)
  [ -n "$ref" ] && [ -d "$ref" ] || die "game1 (the scaffold reference) not found — run: ardz workspace"
  [ -d "$dir" ] && die "$dir already exists"
  say "== scaffolding $name from game1 (neon-grid)"
  mkdir -p "$dir"
  rsync -a --exclude .git --exclude node_modules --exclude dist --exclude test-dist \
    --exclude deploy/.site --exclude "$(printf '.cl%sude' a)" --exclude .shadow-cljs --exclude .cpcache "$ref/" "$dir/"
  # identity FIRST — the repo will be published (NEW-GAME-PROMPT §4)
  (cd "$dir" && git init -q && git config user.name "$name" && git config user.email "$name@noreply.local")
  # namespace: salt first (game1.ardegazu.ro), then the ns rename, then the
  # protocol prefix — game1 is CLJS, so the whole clone re-homes to <name>.*
  local sub="${host%%.*}"
  perl -pi -e "s{game1\\.ardegazu\\.ro/v2}{$host/v2}g" "$dir/client/src/game1/config.cljs"
  rsync -a "$dir/client/src/game1/" "$dir/client/src/$name/"
  rm -rf "$dir/client/src/game1"
  { grep -rlE -- 'game1[./]' "$dir/client/src/$name" "$dir/client/scripts" "$dir/client/shadow-cljs.edn" "$dir/client/deps.edn" "$dir/client/test" 2>/dev/null || true; } | while read -r f; do
    perl -pi -e "s/game1\\./$name./g; s{src/game1/}{src/$name/}g" "$f"
  done
  perl -pi -e "s{/neon-grid/2/}{/$name/2/}g" "$dir/client/src/$name/net/peers.cljs"
  perl -pi -e "s{^APP=game1}{APP=$sub}; s{^IDENT=neon-grid}{IDENT=$name}" "$dir/deploy/publish-repo.sh" 2>/dev/null || true
  printf '%s\t%s\t%s\tgame\t%s\tn\n' "$sub" "$name" "$host" "$name" >> "$MANIFEST"
  say "scaffolded at $dir and added to apps.tsv (as '$sub')."
  say "next, by hand (docs/NEW-GAME-PROMPT.md is the canon):"
  say "  1. gut client/src/$name/game/* into your game (keep protocol/net conventions; JVM >= 17 to build)"
  say "  2. rewrite README.md + site/index.html for the new game"
  say "  3. add an entry to home client/catalog.json (+ client/src/glyphs.ts if bespoke art) + a card in $ASSEMBLER/index.html"
  say "  4. first commit, then: ardz build $sub && ardz release $sub && ardz publish-src $sub"
  say "  5. once live: ipns_create for $host if it doesn't exist yet (see the prompt doc §5)"
}

cmd_new_app() {
  local name="${1:-}" host="${2:-}" variant="${3:-}"
  local usage="usage: ardz new-app <name> <host> <live|rooms>
  live  = ephemeral state only (game stack: vendored net/, libp2p v3; game1 is the canon)
  rooms = durable encrypted log (OrbitDB, mailbox, versioned PWA). The shared core
          comes from ardegazu-rooms-kit over the deps.edn classpath, not vendored;
          chat is the canon for the app-local remainder)
  Both stacks are ClojureScript (shadow-cljs) — dev/docs/CLJS.md is the build canon.
  The TypeScript arms are retired: no TS canon survives anywhere in the suite.
  'live-cljs'/'rooms-cljs' are accepted as aliases of live/rooms."
  [ -n "$name" ] && [ -n "$host" ] || die "$usage"
  # the -cljs suffix is now redundant (both stacks are CLJS) — keep the old
  # spellings working for anything that still passes them
  case "$variant" in live-cljs) variant=live;; rooms-cljs) variant=rooms;; esac
  case "$variant" in live|rooms) ;; *) die "$usage";; esac
  case "$name" in *[!a-z0-9-]*|"") die "name must be lowercase [a-z0-9-]";; esac
  # a CLJS namespace root dir can't carry '-' (ns segments munge - to _ on disk)
  case "$name" in *-*) die "cljs apps: name must be [a-z0-9] only";; esac
  local dir="$WORKSPACE/$name" sub="${host%%.*}" g1 ch versioned=n
  [ -d "$dir" ] && die "$dir already exists"
  g1=$(_dir_of game1)
  [ -n "$g1" ] && [ -d "$g1" ] || die "game1 (live + i18n canon) not found — run: ardz workspace"
  ch=$(_dir_of chat)
  local tpl=app-cljs-live missing=
  if [ "$variant" = rooms ]; then
    [ -n "$ch" ] && [ -d "$ch" ] || die "chat (rooms canon) not found — run: ardz workspace"
    # every precondition BEFORE the first mkdir: the previous shape died inside
    # _vendor, leaving a directory (and a git repo) behind for the operator to
    # find and clean up by hand
    missing=$(_rooms_canon_missing "$ch") || true
    [ -z "$missing" ] || die "rooms canon incomplete in chat — nothing scaffolded, nothing to clean up. Missing:
$(printf '%s' "$missing" | sed 's/^/  chat\//')
run: ardz doctor"
    tpl=app-cljs-rooms
    versioned=y
  fi
  say "== scaffolding $name ($variant) at $dir"
  mkdir -p "$dir"
  _scaffold_tree "$dir" app-common "$tpl"
  _scaffold_subst "$dir" "$name" "$host" "$sub"
  # identity FIRST — the repo will be published (NEW-APP-PROMPT §4)
  _scaffold_git "$dir" "$name"
  if [ "$variant" = live ]; then
    # the live canon (game1): vendored sources + the build scripts + package files
    _vendor "$g1" "$dir" client/src/game1/net client/src/game1/id/boot.cljs \
      client/src/game1/i18n/runtime.cljs client/scripts \
      client/package.json client/package-lock.json client/public/icons
    # the ONE sanctioned net/ delta (house rule 6)
    _ns_rename "$dir" "$name"
    perl -pi -e "s{/neon-grid/2/}{/$name/2/}g" "$dir/client/src/$name/net/peers.cljs"
  else
    # The rooms canon (chat), minus the shared core. Since 2026-08-28 js.cljs
    # and lib/{access,crypto,descriptor,encryption,log,net,orbit-identity,
    # protocol,turn} exist ONLY in ardegazu-rooms-kit: the template's
    # client/deps.edn puts node_modules/ardegazu-rooms-kit/src on :paths and the
    # app requires them under their real ardegazu.rooms.* names, so there is no
    # copy, no rename and nothing to keep byte-identical — a kit fix arrives on
    # the next sha bump. The npm sha pin (in the vendored package.json) stays
    # the ONE cross-repo dependency mechanism; the classpath entry only points
    # at what the pin already installed.
    #
    # What IS copied is _rooms_vendor_list: what the kit has no counterpart for
    # (lib/mailbox, lib/selftest, stores.cljs — the browser IndexedDB opener
    # lib/log deliberately does not name), plus the build/test plumbing (the
    # scripts carrying the rooms/versioned workbox semantics and the chunk-split
    # gates, the npm shim, the cljfmt config, the lint's parser helpers, the
    # package files). No protocol-prefix delta on this line: the wire ids stay chat's
    # and APP-SALT is what separates your rooms from chat's.
    #
    # NOT copied on purpose: lib/media.cljs. It is chat's CALL feature (per-pair
    # media-only RTCPeerConnections, perfect negotiation, sealed SDP), not part
    # of the rooms core — board has never had it, nothing in the scaffold
    # requires it, and it is tracked by no canon.tsv row, so shipping it would
    # hand every new app 445 ungoverned lines to drift. Copy it from chat the
    # day you actually add calls.
    _vendor "$ch" "$dir" $(_rooms_vendor_list)
    _vendor "$g1" "$dir" client/src/game1/i18n/runtime.cljs  # game1 is the i18n canon (rule 6)
    _ns_rename "$dir" "$name"
  fi
  _pkg_rename "$dir/client" "$name"
  [ "$variant" = rooms ] && _rooms_pkg_scripts "$dir/client"
  _manifest_add "$sub" "$name" "$host" app "$name" "$versioned"
  say "scaffolded at $dir and added to apps.tsv (as '$sub', versioned=$versioned)."
  say "next, by hand (dev/docs/NEW-APP-PROMPT.md is the canon):"
  case "$variant" in
    live)  say "  1. build your app in client/src/$name/game/ (the scaffold's demo compiles and runs as-is; JVM >= 17 needed to build)";;
    rooms) say "  1. build your app in client/src/$name/app/ (the scaffold's demo compiles and runs as-is; JVM >= 17 needed to build)";;
  esac
  [ "$variant" = rooms ] && say "  2. define your LogOp union in client/src/$name/lib/protocol.cljs (the one wire surface you own; the shared core is ardegazu-rooms-kit's, off the deps.edn classpath — bump its npm sha, never edit it here)"
  say "  3. replace client/public/icons (placeholder art from the canon repo) + rewrite README.md and site/index.html"
  say "  4. add a home client/catalog.json entry (+ glyphs.ts), a card in $ASSEMBLER/index.html, social-kit SUITE_APPS"
  say "  5. first commit, then: ardz build $sub && ardz release $sub && ardz publish-src $sub"
  say "  6. once live: ipns_create for $host if it doesn't exist yet"
}

cmd_new_bot() {
  local name="${1:-}" variant="${2:-cljs}"
  local usage="usage: ardz new-bot <name>
  ClojureScript: four isolated shadow-cljs builds (two-stack law) — dev/docs/CLJS.md.
  The TypeScript arm is retired: no TS scaffold survives anywhere in the suite."
  [ -n "$name" ] || die "$usage"
  # the trailing 'cljs' is now redundant (the only stack) — keep the old
  # spelling working for anything that still passes it
  case "$variant" in cljs) ;; *) die "$usage";; esac
  # a CLJS namespace root dir can't carry '-' (ns segments munge - to _ on disk)
  case "$name" in *[!a-z0-9]*|"") die "name must be lowercase [a-z0-9] (a CLJS namespace root)";; esac
  local behaviors="src/$name/behaviors.cljs" rldirs="src/$name/rl + deploy/*trainer*"
  local dir="$WORKSPACE/$name"
  [ -d "$dir" ] && die "$dir already exists"
  say "== scaffolding bot $name at $dir"
  mkdir -p "$dir"
  _scaffold_tree "$dir" bot
  _scaffold_subst "$dir" "$name" - "$name"
  # identity FIRST — the repo will be published (NEW-BOT-PROMPT §2)
  _scaffold_git "$dir" "$name"
  _manifest_add "$name" "$name" - kit "$name" n
  say "scaffolded at $dir and added to apps.tsv (kind=kit, source-only)."
  say "next, by hand (docs/NEW-BOT-PROMPT.md in the scaffold is the canon):"
  say "  1. wire behaviors in $behaviors, then: npm install && npm run check && npm run build"
  say "  2. RL games: run train-kit/docs/NEW-BRAIN-PROMPT.md per game — or drop RL (delete $rldirs)"
  say "  3. smoke test with a throwaway seed (prompt doc §6 — bots talk to the production relay; there is no dev relay)"
  say "  4. deploy: rsync to the box, sh deploy/install-fleet.sh <names…> (one name = a fleet of one)"
  say "  5. commit dist/ (check-dist gate), first commit, then: ardz publish-src $name"
  say "  6. hub card: home client/catalog.json bots[] entry, then release home (commit the version bump)"
}

# ---- ecosystem memory file --------------------------------------------------------------

cmd_home() {
  # the assistant memory file's name is assembled from pieces so this script
  # never trips a publish gate that hunts for it (same trick the gates use)
  local fname t root
  fname="$(printf 'CLA%sDE.md' U)"
  t="$DEV_DIR/templates/HOME.md"
  [ -f "$t" ] || die "template missing: $t"
  for root in "$WORKSPACE"; do
    [ -d "$root" ] && cp "$t" "$root/$fname" && say "✓ $root/$fname"
  done
}

# ---- main -------------------------------------------------------------------

case "${1:-help}" in
  status)       shift; cmd_status "$@";;
  doctor)       shift; cmd_doctor "$@";;
  workspace)    shift; cmd_workspace "$@";;
  dev)          shift; cmd_dev "$@";;
  relay)        shift; cmd_relay "$@";;
  build)        shift; cmd_build "$@";;
  release)      shift; cmd_release "$@";;
  publish-src)  shift; cmd_publish_src "$@";;
  kit-release)  shift; cmd_kit_release "$@";;
  new)          shift; cmd_new "$@";;
  new-app)      shift; cmd_new_app "$@";;
  new-bot)      shift; cmd_new_bot "$@";;
  home)         shift; cmd_home "$@";;
  help|--help|-h) sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//';;
  *) die "unknown command '$1' — try: ardz help";;
esac

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/dev.git