@zerwiz/ymir 0.1.7 → 0.1.8

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.
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bash
2
+ # smidja-board.sh — Smíðja's board, by one name.
3
+ #
4
+ # The visualizer can live in two very different trees: a clone's
5
+ # `apps/smidja-factory/apps/visualizer`, or the `@zerwiz/smidja-factory` package
6
+ # a user got from npm. Its database lives in the HOME (`$YMIR_HOME/smidja/smidja.db`),
7
+ # not in either tree. This door resolves all three so the operator never has to.
8
+ #
9
+ # smidja-board.sh build # install deps and build the UI (./dist)
10
+ # smidja-board.sh start # raise the API + UI, detached, ready to answer
11
+ # smidja-board.sh stop
12
+ # smidja-board.sh status
13
+ # smidja-board.sh --version | --help
14
+ #
15
+ # The operator's door is `ymir smidja [build|start|stop|status]` — this is what
16
+ # that door runs.
17
+ #
18
+ # Exit: 0 ok, 1 error, 2 usage.
19
+ set -u
20
+
21
+ VERSION="1.0.0"
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
24
+ YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
25
+
26
+ # shellcheck source=bin/hoard-lib.sh
27
+ . "$SCRIPT_DIR/hoard-lib.sh"
28
+ ymir_home_root YMIR_HOME
29
+ hoard_state_dir STATE
30
+ # shellcheck source=bin/smidja-lib.sh
31
+ . "$SCRIPT_DIR/smidja-lib.sh"
32
+
33
+ PORT="${SMIDJA_VIZ_API_PORT:-8437}"
34
+ PID_FILE="$STATE/smidja-viz-api.pid"
35
+ LOG_FILE="$STATE/smidja-viz-api.log"
36
+
37
+ # The database follows the HOME; SMIDJA_DB overrides. A tree-local copy is the
38
+ # last resort, for a checkout that keeps its own.
39
+ SMIDJA_DB_PATH="${SMIDJA_DB:-}"
40
+ if [ -z "$SMIDJA_DB_PATH" ]; then
41
+ for _db in "$YMIR_HOME/smidja/smidja.db" "$ROOT/apps/smidja/smidja_data/smidja.db"; do
42
+ [ -f "$_db" ] && { SMIDJA_DB_PATH="$_db"; break; }
43
+ done
44
+ fi
45
+
46
+ viz_dir() { # <result-var>
47
+ local result_var=${1-} v
48
+ [ -n "$result_var" ] || return 2
49
+ if smidja_visualizer_dir v; then printf -v "$result_var" '%s' "$v"; return 0; fi
50
+ printf -v "$result_var" '%s' ""; return 1
51
+ }
52
+
53
+ alive() { [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; }
54
+
55
+ case "${1-}" in
56
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
57
+ -h|--help|"") sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
58
+ esac
59
+ ACTION="$1"
60
+
61
+ VIZ=""
62
+ if ! viz_dir VIZ; then
63
+ printf 'error: the visualizer is not installed in this tree\n'
64
+ printf 'help: npm i -g @zerwiz/ymir (the smithy arrives as @zerwiz/smidja-factory)\n'
65
+ exit 1
66
+ fi
67
+
68
+ case "$ACTION" in
69
+ build)
70
+ if ! command -v bun >/dev/null 2>&1; then
71
+ printf 'error: bun is needed to build the UI\nhelp: bin/prereq-ensure.sh bun\n' >&2; exit 1
72
+ fi
73
+ printf 'visualizer_build[1]{step,state}:\n'
74
+ if [ ! -d "$VIZ/node_modules" ]; then
75
+ if (cd "$VIZ" && bun install >/dev/null 2>&1); then printf ' "deps","installed"\n'
76
+ else printf ' "deps","FAILED — (cd %s && bun install)"\n' "$VIZ" >&2; exit 1; fi
77
+ else printf ' "deps","present"\n'; fi
78
+ if (cd "$VIZ" && bun run build >/dev/null 2>&1) || (cd "$VIZ" && bunx vite build >/dev/null 2>&1); then
79
+ printf ' "ui","%s/dist"\n' "$VIZ"
80
+ else
81
+ printf 'error: the UI build failed\nhelp: (cd %s && bun run build)\n' "$VIZ" >&2; exit 1
82
+ fi
83
+ ;;
84
+ start)
85
+ command -v bun >/dev/null 2>&1 || { printf 'error: bun is needed to run the visualizer\nhelp: bin/prereq-ensure.sh bun\n' >&2; exit 1; }
86
+ [ -n "$SMIDJA_DB_PATH" ] && [ -f "$SMIDJA_DB_PATH" ] || {
87
+ printf 'error: no smidja.db found\nhelp: bin/smidja-bootstrap.sh (creates $YMIR_HOME/smidja/smidja.db)\n' >&2; exit 1; }
88
+ [ -d "$VIZ/dist" ] || printf 'note: the UI is unbuilt — the API will answer and show no interface\nnote: mend it with: bin/ymir-visualizer.sh build\n' >&2
89
+ if alive; then
90
+ printf 'visualizer[1]{state,pid,url}:\n "already running","%s","http://127.0.0.1:%s/"\n' "$(cat "$PID_FILE")" "$PORT"
91
+ exit 0
92
+ fi
93
+ mkdir -p "$STATE"
94
+ # ymir_detach PRINTS the pid it spawned (setsid/nohup is its business, not
95
+ # ours); reading $! here would read a variable the helper already consumed.
96
+ pid=""
97
+ if command -v ymir_detach >/dev/null 2>&1; then
98
+ pid="$(ymir_detach env CMD_DB="$SMIDJA_DB_PATH" PORT="$PORT" bun run "$VIZ/server/index.ts" 2>>"$LOG_FILE")"
99
+ fi
100
+ if [ -z "$pid" ]; then
101
+ ( cd "$VIZ" && exec env CMD_DB="$SMIDJA_DB_PATH" PORT="$PORT" bun run server/index.ts ) >>"$LOG_FILE" 2>&1 &
102
+ pid="$!"
103
+ fi
104
+ [ -n "$pid" ] && printf '%s\n' "$pid" >"$PID_FILE"
105
+ for _i in $(seq 1 20); do
106
+ curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$PORT/api/health" 2>/dev/null && break
107
+ sleep 0.5
108
+ done
109
+ if curl -s --max-time 2 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then
110
+ printf 'visualizer[1]{state,db,url}:\n "up","%s","http://127.0.0.1:%s/"\n' "$SMIDJA_DB_PATH" "$PORT"
111
+ else
112
+ printf 'error: the visualizer did not answer on :%s\nhelp: see %s\n' "$PORT" "$LOG_FILE" >&2; exit 1
113
+ fi
114
+ ;;
115
+ stop)
116
+ if alive; then
117
+ kill "$(cat "$PID_FILE")" 2>/dev/null; rm -f "$PID_FILE"
118
+ printf 'visualizer[1]{state}:\n "stopped"\n'
119
+ else
120
+ rm -f "$PID_FILE"
121
+ printf 'visualizer[1]{state}:\n "not running"\n'
122
+ fi
123
+ ;;
124
+ status)
125
+ local_db="${SMIDJA_DB_PATH:-none}"
126
+ if alive && curl -s --max-time 2 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then
127
+ printf 'visualizer[3]{state,pid,db,url}:\n "up","%s","%s","http://127.0.0.1:%s/"\n' "$(cat "$PID_FILE")" "$local_db" "$PORT"
128
+ elif alive; then
129
+ printf 'visualizer[2]{state,pid,note}:\n "starting or wedged","%s","answers nothing on :%s — see %s"\n' "$(cat "$PID_FILE")" "$PORT" "$LOG_FILE"
130
+ exit 1
131
+ else
132
+ printf 'visualizer[3]{state,tree,db,ui}:\n "down","%s","%s","%s"\n' "$VIZ" "$local_db" "$([ -d "$VIZ/dist" ] && echo built || echo unbuilt)"
133
+ fi
134
+ ;;
135
+ *) printf 'error: unknown action %s\nhelp: bin/smidja-board.sh [build|start|stop|status]\n' "$ACTION" >&2; exit 2 ;;
136
+ esac
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # smidja-lib.sh — where the smithy's parts actually live.
3
+ #
4
+ # Two installs, one name. In a clone the smithy sits at `apps/smidja-factory`
5
+ # (the registry's `repo: apps/<path>` block clones it there), and
6
+ # `.agents/skills/smidja-factory` is a symlink to it. In a packaged install the
7
+ # tree has no `apps/` at all: the smithy arrives as a dependency, at
8
+ # `node_modules/@zerwiz/smidja-factory`, and that symlink dangles.
9
+ #
10
+ # Every script that needs the visualizer resolves it HERE, so the two shapes can
11
+ # never disagree (Rule 07). Source-safe; defines functions only.
12
+ #
13
+ # smidja_factory_dir <result-var> the smithy: skill, skills/, templates/, apps/
14
+ # smidja_visualizer_dir <result-var> the board: its server, its UI source, its dist
15
+ set -u
16
+
17
+ # A surface has a name the operator knows and a package has a name npm serves;
18
+ # they are not always the same, and the smithy is the pair that proves it.
19
+ SMIDJA_SURFACE="smidja"
20
+ SMIDJA_PACKAGE="smidja-factory"
21
+
22
+ smidja_factory_dir() { # <result-var> — the smithy's directory, or empty
23
+ local result_var=${1-} root="${YMIR_ROOT_DIR:-}" c
24
+ [ -n "$result_var" ] || return 2
25
+ if [ -z "$root" ]; then
26
+ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
27
+ fi
28
+ for c in \
29
+ "$root/apps/$SMIDJA_PACKAGE" \
30
+ "$root/node_modules/@zerwiz/$SMIDJA_PACKAGE" \
31
+ "$root/.agents/skills/$SMIDJA_PACKAGE"
32
+ do
33
+ # A dangling symlink is not a home: the visualizer must be readable there.
34
+ [ -d "$c/apps/visualizer" ] && { printf -v "$result_var" '%s' "$c"; return 0; }
35
+ done
36
+ printf -v "$result_var" '%s' ""
37
+ return 1
38
+ }
39
+
40
+ smidja_visualizer_dir() { # <result-var> — the visualizer (server · UI source · dist)
41
+ local result_var=${1-} factory
42
+ [ -n "$result_var" ] || return 2
43
+ if smidja_factory_dir factory; then
44
+ printf -v "$result_var" '%s' "$factory/apps/visualizer"
45
+ return 0
46
+ fi
47
+ printf -v "$result_var" '%s' ""
48
+ return 1
49
+ }
@@ -7,6 +7,7 @@
7
7
  # Usage:
8
8
  # bin/ymir-install.sh [--check] [--skip-engines] [--skip-services] [--no-desktop] [--yes]
9
9
  # bin/ymir-install.sh --plan [--json] # the plan, computed — changes nothing
10
+ # bin/ymir-install.sh --yes | --non-interactive | --accept-all-defaults
10
11
  # bin/ymir-install.sh --status
11
12
  # bin/ymir-install.sh --version
12
13
  #
@@ -53,6 +54,18 @@ hoard_local_env YMIR_ENV_FILE
53
54
  # shellcheck source=bin/hoard-lib.sh
54
55
  . "$SCRIPT_DIR/hoard-lib.sh"
55
56
  ymir_home_root YMIR_HOME
57
+ # Where the smithy's parts live: apps/smidja-factory in a clone, or the
58
+ # @zerwiz/smidja-factory package in an npm install (bin/smidja-lib.sh).
59
+ if [ -z "${YMIR_SMIDJA_LIB_LOADED:-}" ]; then
60
+ _ys="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
61
+ for _yc in "$_ys/smidja-lib.sh" "$(dirname "$_ys")/bin/smidja-lib.sh"; do
62
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_SMIDJA_LIB_LOADED=1; break; }
63
+ done
64
+ unset _ys _yc
65
+ fi
66
+ smidja_visualizer_dir SMIDJA_VIZ
67
+ smidja_factory_dir SMIDJA_FACTORY
68
+
56
69
  WORKSPACE="${YMIR_WORKSPACE:-$YMIR_HOME/workspaces}"
57
70
  hoard_root HOARD
58
71
  DOMAINS="company marketing development life me"
@@ -63,11 +76,12 @@ while [ $# -gt 0 ]; do
63
76
  case "$1" in
64
77
  --check) CHECK=1; shift ;;
65
78
  --plan|--dry-run) PLAN_ONLY=1; shift ;;
66
- --json) PLAN_ARGS+=(--json); shift ;;
79
+ --json|--blocked) PLAN_ARGS+=("$1"); shift ;;
80
+ --phase) PLAN_ARGS+=("$1" "${2-}"); shift 2 ;; # --phase carries its number
67
81
  --skip-engines) SKIP_ENGINES=1; shift ;;
68
82
  --skip-services) SKIP_SERVICES=1; shift ;;
69
83
  --no-desktop) NO_DESKTOP=1; shift ;;
70
- --yes|-y) ASSUME_YES=1; shift ;;
84
+ --yes|-y|--non-interactive|--accept-all-defaults) ASSUME_YES=1; shift ;;
71
85
  --status) exec "$SCRIPT_DIR/ymir-install.sh" --check ;;
72
86
  *) printf 'error: unknown flag %s\nhelp: bin/ymir-install.sh [--check|--plan|--skip-engines|--skip-services|--no-desktop|--yes]\n' "$1" >&2; exit 2 ;;
73
87
  esac
@@ -78,6 +92,11 @@ if [ "$PLAN_ONLY" = 1 ]; then
78
92
  exec "$SCRIPT_DIR/ymir-plan.sh" ${PLAN_ARGS[@]+"${PLAN_ARGS[@]}"}
79
93
  fi
80
94
 
95
+ # The cloth (bin/ymir-style.sh): colour, marks and spacing for the human, while
96
+ # every row of data stays TOON on stdout.
97
+ . "$SCRIPT_DIR/ymir-style.sh"
98
+ style_init
99
+
81
100
  declare -a IDS STATUS DETAIL
82
101
  # The code minted for this install, reported at the end so it is not lost in the
83
102
  # step table. Empty on --check, and when no code could be minted.
@@ -97,8 +116,8 @@ confirm_install() {
97
116
  printf 'error: refusing a non-interactive install without --yes\nhelp: re-run with --yes to accept non-interactively, or --plan / --check to preview\n' >&2
98
117
  exit 3
99
118
  fi
119
+ style_title "Ymir first setup" "the plan below is probed on this machine, not recited"
100
120
  cat <<'PLAN'
101
- Ymir first setup — the plan below is probed on THIS machine, not recited.
102
121
  Each row is a phase, a step, its state, and why.
103
122
 
104
123
  DO a change will be made
@@ -110,7 +129,7 @@ Each row is a phase, a step, its state, and why.
110
129
  Nothing is deleted. Every step is idempotent.
111
130
  PLAN
112
131
  printf '\n'
113
- bash "$SCRIPT_DIR/ymir-plan.sh" 2>&1 || true
132
+ bash "$SCRIPT_DIR/ymir-plan.sh" --colour >/dev/null 2>&1 || true
114
133
  printf '\nProceed with the install? [y/N] '
115
134
  read -r reply || reply=""
116
135
  case "$reply" in
@@ -527,7 +546,7 @@ step_smidja() {
527
546
  if [ "$CHECK" = 1 ]; then
528
547
  local dbok vizok
529
548
  [ -f "$YMIR_HOME/smidja/smidja.db" ] && dbok=present || dbok=missing
530
- [ -d "$ROOT/.agents/skills/smidja-factory/apps/visualizer/dist" ] && vizok=built || vizok=unbuilt
549
+ [ -n "${SMIDJA_VIZ:-}" ] && [ -d "$SMIDJA_VIZ/dist" ] && vizok=built || vizok=unbuilt
531
550
  if [ "$dbok" = missing ]; then
532
551
  add smidja WARN "smidja.db missing — a full run (without --check) creates it and seeds one bootstrap session"
533
552
  else
@@ -543,7 +562,7 @@ step_smidja() {
543
562
  else add smidja SKIP "no smidja-bootstrap.sh"; fi
544
563
  # The visualizer API serves its UI from ./dist — without a build it answers
545
564
  # the API but shows "No ./dist build found". Build it once when absent.
546
- local viz="$ROOT/.agents/skills/smidja-factory/apps/visualizer"
565
+ local viz="${SMIDJA_VIZ:-}"; [ -n "$viz" ] || return 0
547
566
  [ -d "$viz" ] || return 0
548
567
  if [ -d "$viz/dist" ]; then
549
568
  add visualizer OK "UI built (served on :8437)"
@@ -682,6 +701,22 @@ step_desktop() {
682
701
  fi
683
702
  # Placement is the Omarchy layer's job and has already run (step_omarchy runs
684
703
  # before this step), so here we only launch.
704
+ # Verify the runtime before claiming anything: a skipped Electron postinstall
705
+ # leaves a partial runtime that fails to launch while every build still passes.
706
+ if [ -z "${YMIR_ELECTRON_LIB_LOADED:-}" ] && [ -r "$SCRIPT_DIR/electron-lib.sh" ]; then
707
+ . "$SCRIPT_DIR/electron-lib.sh"; YMIR_ELECTRON_LIB_LOADED=1
708
+ fi
709
+ local shell partial=""
710
+ for shell in hlidskjalf odrerir sessrumnir; do
711
+ local dir="$ROOT/apps/$shell" d
712
+ [ -d "$dir" ] || dir="$(cd "$ROOT" && npm root 2>/dev/null)/@zerwiz/$shell"
713
+ d="$(electron_runtime_state "$dir" 2>/dev/null || true)"
714
+ [ "$d" = partial ] && partial="$partial $shell"
715
+ done
716
+ if [ -n "$partial" ]; then
717
+ add desktop WARN "the Electron runtime is PARTIAL for:$partial — the web surfaces stand; approve and rebuild to launch the shells"
718
+ return 0
719
+ fi
685
720
  if "$ROOT/scripts/electron.sh" start --both >/dev/null 2>&1; then
686
721
  add desktop OK "raised Hlidskjalf + Smíðja"
687
722
  else
package/bin/ymir-plan.sh CHANGED
@@ -46,7 +46,7 @@ YMIR_HOME_WAS_SET=0; [ -n "${YMIR_HOME:-}" ] && YMIR_HOME_WAS_SET=1
46
46
  ymir_home_root YMIR_HOME
47
47
  hoard_root HOARD
48
48
 
49
- JSON=0; ONLY_PHASE=""; ONLY_BLOCKED=0
49
+ JSON=0; COLOUR=0; ONLY_PHASE=""; ONLY_BLOCKED=0
50
50
  case "${1-}" in
51
51
  -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
52
52
  -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
@@ -54,9 +54,10 @@ esac
54
54
  while [ $# -gt 0 ]; do
55
55
  case "$1" in
56
56
  --json) JSON=1; shift ;;
57
+ --colour|--color) COLOUR=1; shift ;;
57
58
  --phase) ONLY_PHASE="${2-}"; shift 2 ;;
58
59
  --blocked) ONLY_BLOCKED=1; shift ;;
59
- *) printf 'error: unknown flag %s\nhelp: bin/ymir-plan.sh [--json] [--phase N] [--blocked]\n' "$1" >&2; exit 2 ;;
60
+ *) printf 'error: unknown flag %s\nhelp: bin/ymir-plan.sh [--json|--colour] [--phase N] [--blocked]\n' "$1" >&2; exit 2 ;;
60
61
  esac
61
62
  done
62
63
 
@@ -71,9 +72,12 @@ port_up() { # <port>
71
72
  }
72
73
  # An app is ours by one of two shapes: a source checkout under apps/, or a
73
74
  # published package under node_modules/@zerwiz/. Neither is guessed.
74
- app_dir() { # <name> -> path, or empty
75
- local n="$1" c
76
- for c in "$ROOT/apps/$n" "$ROOT/node_modules/@zerwiz/$n"; do
75
+ # A surface has a name the operator knows and a PACKAGE has a name npm serves;
76
+ # they are not always the same (the smithy is `smidja` to us and
77
+ # `@zerwiz/smidja-factory` on the registry). <surface> [<package>]
78
+ app_dir() {
79
+ local n="$1" pkg="${2:-$1}" c
80
+ for c in "$ROOT/apps/$n" "$ROOT/node_modules/@zerwiz/$pkg"; do
77
81
  [ -d "$c" ] && { printf '%s' "$c"; return 0; }
78
82
  done
79
83
  return 1
@@ -234,35 +238,48 @@ engines_phase() {
234
238
  # ── phase 5 · apps — the surfaces the operator actually sees ────────────────
235
239
  # Four surfaces, one shape each: web build required, Electron shell gated.
236
240
  # The apps are their own packages (@zerwiz/<app>) — the distro depends on them.
237
- app_row() { # <name> <about>
238
- local name="$1" about="$2" dir
239
- if dir="$(app_dir "$name")"; then
241
+ app_row() { # <surface> <about> [<package>]
242
+ local name="$1" about="$2" pkg="${3:-$1}" dir
243
+ if dir="$(app_dir "$name" "$pkg")"; then
240
244
  if [ -d "$dir/dist" ] || [ -d "$dir/out" ]; then
241
245
  emit 5 apps "$name" SKIP "$about — installed, and its build shipped with it"
242
246
  else
243
247
  emit 5 apps "$name" DO "$about — installed from source; the web build is still to make"
244
248
  fi
245
- elif grep -q "\"@zerwiz/$name\"" "$ROOT/package.json" 2>/dev/null; then
249
+ elif grep -q "\"@zerwiz/$pkg\"" "$ROOT/package.json" 2>/dev/null; then
246
250
  # Declared as a dependency of the distro but not present: the package exists,
247
251
  # the tree simply has not fetched it. That is a DO, not a dead end.
248
252
  emit 5 apps "$name" DO "$about — declared as a dependency but not fetched (npm i -g @zerwiz/ymir fetches it)"
249
253
  else
250
- emit 5 apps "$name" BLOCKED "$about — no apps/$name, no @zerwiz/$name package, and the distro does not depend on it"
254
+ emit 5 apps "$name" BLOCKED "$about — no apps/$name, no @zerwiz/$pkg package, and the distro does not depend on it"
251
255
  fi
252
256
  }
253
257
  apps_phase() {
254
258
  app_row hlidskjalf "the control plane (the high seat), served on :3888"
255
259
  app_row odrerir "the live hall (chat + council)"
256
260
  app_row sessrumnir "the seat-hall desktop"
257
- app_row smidja "the smithy and its visualizer (:8437)"
261
+ app_row smidja "the smithy and its visualizer (:8437)" smidja-factory
258
262
 
259
- local shells=0 d
263
+ # A shell is ready only when its runtime VERIFIES: npm gates install scripts,
264
+ # and a skipped Electron postinstall leaves a partial runtime that still builds
265
+ # the web app and still reports success (bin/electron-lib.sh).
266
+ if [ -z "${YMIR_ELECTRON_LIB_LOADED:-}" ]; then
267
+ for c in "$SCRIPT_DIR/electron-lib.sh"; do [ -r "$c" ] && { . "$c"; YMIR_ELECTRON_LIB_LOADED=1; }; done
268
+ fi
269
+ local shells=0 partial="" missing=0 d
260
270
  for d in hlidskjalf odrerir sessrumnir; do
261
271
  dir="$(app_dir "$d" 2>/dev/null || true)"
262
- if [ -n "$dir" ] && [ -d "$dir/node_modules/electron/dist" ]; then shells=$((shells+1)); fi
272
+ if [ -z "$dir" ]; then missing=$((missing+1)); continue; fi
273
+ case "$(electron_runtime_state "$dir" 2>/dev/null || true)" in
274
+ ok) shells=$((shells+1)) ;;
275
+ partial) partial="$partial $d" ;;
276
+ *) missing=$((missing+1)) ;;
277
+ esac
263
278
  done
264
279
  if [ "$shells" -ge 3 ]; then
265
- emit 5 apps electron SKIP "the three desktop shells are built (electron runtime present)"
280
+ emit 5 apps electron SKIP "the three desktop shells' runtimes verify"
281
+ elif [ -n "$partial" ]; then
282
+ emit 5 apps electron DO "the runtime is PARTIAL for:$partial — npm skipped the Electron postinstall; the web surfaces stand, the shells will not launch"
266
283
  elif [ "${YMIR_NO_DESKTOP:-0}" = 1 ]; then
267
284
  emit 5 apps electron SKIP "declined (--no-desktop) — the web surfaces stand without the shells"
268
285
  else
@@ -347,6 +364,23 @@ if [ "$shown" -eq 0 ]; then
347
364
  exit 0
348
365
  fi
349
366
 
367
+ # A human sees the same rows, rendered in the cloth, on stderr — the TOON below
368
+ # stays the data on stdout, so a pipeline never parses a decoration.
369
+ if [ "$COLOUR" = 1 ]; then
370
+ . "$SCRIPT_DIR/ymir-style.sh"
371
+ style_init
372
+ last_phase=""
373
+ for i in "${!P_STEP[@]}"; do
374
+ [ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
375
+ [ "$ONLY_BLOCKED" = 1 ] && [ "${P_STATE[$i]}" != BLOCKED ] && continue
376
+ if [ "${P_N[$i]}" != "$last_phase" ]; then
377
+ style_heading "${P_NAME[$i]}"
378
+ last_phase="${P_N[$i]}"
379
+ fi
380
+ style_line "${P_STATE[$i]}" "${P_STEP[$i]}" "${P_WHY[$i]}"
381
+ done
382
+ fi
383
+
350
384
  printf 'plan[%d]{phase,name,step,state,why}:\n' "$shown"
351
385
  for i in "${!P_STEP[@]}"; do
352
386
  [ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # ymir-style.sh — the cloth of the terminal halls.
3
+ #
4
+ # Ymir had no design for its own command line: correct output, no experience.
5
+ # This is the cloth — cut from the same stone as the halls
6
+ # (midgard/design-system/tokens.css): bone for words, bronze for what acts,
7
+ # steel for what stands, blood for what is wrong, and the forge's marks for the
8
+ # states a plan can be in.
9
+ #
10
+ # The rules it obeys (cli-guidelines, clig.dev, Monospace TUI):
11
+ # · colour, marks and motion only when a human is present — never in a pipe,
12
+ # never when NO_COLOR is set, never on a dumb terminal, never for --no-color
13
+ # · data goes to stdout, the human's rendering goes to stderr, so a pipeline
14
+ # never has to parse a decoration
15
+ # · a reaction for every action, and a next step for every ending
16
+ # · density first: no banner bigger than four lines, no rule longer than the text
17
+ #
18
+ # Source-safe; functions only. Nothing here writes a byte of data.
19
+ set -u
20
+
21
+ # --- the palette, from the tokens (truecolor → 256 → none) ------------------
22
+ style_init() {
23
+ STYLE_ON=0
24
+ # A human must be present: stderr is a terminal and nobody said no.
25
+ if [ -t 2 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != dumb ] && [ "${YMIR_NO_COLOR:-0}" != 1 ]; then
26
+ STYLE_ON=1
27
+ fi
28
+ if [ "$STYLE_ON" = 1 ] && { [ "${COLORTERM:-}" = truecolor ] || [ "${COLORTERM:-}" = 24bit ]; }; then
29
+ C_BONE=$'\033[38;2;207;195;169m'; C_BRONZE=$'\033[38;2;201;151;79m'
30
+ C_STEEL=$'\033[38;2;150;160;168m'; C_BLOOD=$'\033[38;2;194;88;74m'
31
+ C_FAINT=$'\033[38;2;107;98;80m'; C_BOLD=$'\033[1m'
32
+ elif [ "$STYLE_ON" = 1 ]; then
33
+ C_BONE=$'\033[38;5;187m'; C_BRONZE=$'\033[38;5;179m'
34
+ C_STEEL=$'\033[38;5;109m'; C_BLOOD=$'\033[38;5;167m'
35
+ C_FAINT=$'\033[38;5;101m'; C_BOLD=$'\033[1m'
36
+ else
37
+ C_BONE=""; C_BRONZE=""; C_STEEL=""; C_BLOOD=""; C_FAINT=""; C_BOLD=""
38
+ fi
39
+ C_OFF=""
40
+ [ "$STYLE_ON" = 1 ] && C_OFF=$'\033[0m'
41
+ }
42
+
43
+ # The forge's marks, one per state a thing can be in.
44
+ style_mark() { # <state> → a glyph
45
+ case "${1:-}" in
46
+ DO|do) printf '%s' '◆' ;;
47
+ SKIP|skip|already) printf '%s' '·' ;;
48
+ INFO|info) printf '%s' '—' ;;
49
+ BLOCKED|blocked) printf '%s' '✕' ;;
50
+ CONSENT|consent) printf '%s' '?' ;;
51
+ OK|ok) printf '%s' '✓' ;;
52
+ WARN|warn) printf '%s' '·' ;;
53
+ FAIL|fail) printf '%s' '✕' ;;
54
+ *) printf '%s' '·' ;;
55
+ esac
56
+ }
57
+
58
+ style_colour() { # <state> → the colour for it
59
+ case "${1:-}" in
60
+ DO|CONSENT|WARN|warn|do|consent) printf '%s' "$C_BRONZE" ;;
61
+ OK|SKIP|INFO|ok|skip|info) printf '%s' "$C_STEEL" ;;
62
+ FAIL|BLOCKED|fail|blocked) printf '%s' "$C_BLOOD" ;;
63
+ *) printf '%s' "$C_FAINT" ;;
64
+ esac
65
+ }
66
+
67
+ # --- the pieces -------------------------------------------------------------
68
+
69
+ # The mark: four lines at most. The WORDS show even in a pipe — only the colour
70
+ # is conditional, because colour is decoration and the name is information.
71
+ style_title() { # <name> <claim>
72
+ printf '%s\n' "${C_BRONZE} ᛉ ${C_BOLD}${1}${C_OFF}" >&2
73
+ printf '%s\n' "${C_FAINT} ${2}${C_OFF}" >&2
74
+ printf '\n' >&2
75
+ }
76
+
77
+ style_rule() { # a hairline, no wider than the words it separates
78
+ local width="${1:-52}"
79
+ [ "$STYLE_ON" = 1 ] || return 0
80
+ local line=""; local i=0
81
+ while [ "$i" -lt "$width" ]; do line="$line─"; i=$((i+1)); done
82
+ printf '%s\n' "${C_FAINT}${line}${C_OFF}" >&2
83
+ }
84
+
85
+ style_say() { printf '%s\n' "${C_BONE}$*${C_OFF}" >&2; }
86
+ style_hint() { printf '%s\n' "${C_FAINT}$*${C_OFF}" >&2; }
87
+
88
+ style_line() { # <state> <name> <detail> — the one line per thing
89
+ local state="$1" name="$2" detail="${3:-}"
90
+ printf ' %s%s%s %-12s %s%s\n' \
91
+ "$(style_colour "$state")" "$(style_mark "$state")" "$C_OFF" \
92
+ "$name" "$(style_colour "$state")" "$detail" >&2
93
+ printf '%s' "$C_OFF" >/dev/null
94
+ }
95
+
96
+ style_heading() { printf '\n%s%s%s\n' "$C_BRONZE" "$1" "$C_OFF" >&2; }
97
+
98
+ # The ending: what stands, then exactly what to type next. Every CLI deserves
99
+ # to leave the operator with the next step and nothing else to guess.
100
+ style_next() { # one command per line, as "verb — what it does"
101
+ printf '\n%s\n' "${C_BOLD}Where to go from here${C_OFF}" >&2
102
+ while [ $# -gt 0 ]; do
103
+ local cmd="${1%% — *}" what="${1#* — }"
104
+ printf ' %s%-22s%s %s\n' "$C_BRONZE" "$cmd" "$C_OFF" "$(printf '%s' "$what" | sed "s/^/$C_FAINT/;s/$/$C_OFF/")" >&2
105
+ shift
106
+ done
107
+ printf '\n' >&2
108
+ }
@@ -16,6 +16,24 @@ set -u
16
16
  VERSION="1.0.0"
17
17
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
18
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
19
+ # The cloth: colour and marks for the human reading this report; the TOON rows on
20
+ # stdout stay the data (bin/ymir-style.sh).
21
+ if [ -z "${YMIR_STYLE_LOADED:-}" ]; then
22
+ . "$SCRIPT_DIR/ymir-style.sh"; YMIR_STYLE_LOADED=1
23
+ fi
24
+ style_init
25
+ # Where the smithy's parts live: apps/smidja-factory in a clone, or the
26
+ # @zerwiz/smidja-factory package in an npm install (bin/smidja-lib.sh).
27
+ if [ -z "${YMIR_SMIDJA_LIB_LOADED:-}" ]; then
28
+ _ys="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ for _yc in "$_ys/smidja-lib.sh" "$(dirname "$_ys")/bin/smidja-lib.sh"; do
30
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_SMIDJA_LIB_LOADED=1; break; }
31
+ done
32
+ unset _ys _yc
33
+ fi
34
+ smidja_visualizer_dir SMIDJA_VIZ
35
+ smidja_factory_dir SMIDJA_FACTORY
36
+
19
37
  # shellcheck source=bin/ymir-platform.sh
20
38
  . "$SCRIPT_DIR/ymir-platform.sh"
21
39
  # Docker or rootless Podman (Fedora), whichever is present.
@@ -118,7 +136,7 @@ fi
118
136
  # ── 7b. visualizer (built ./dist AND its API actually listening) ────────────
119
137
  # A PASS must mean the thing is UP. Probe the port, not just the build: a
120
138
  # built-but-dead visualizer (a bad CMD_DB, a crashed API) is a FAIL, not green.
121
- VIZ="$ROOT/.agents/skills/smidja-factory/apps/visualizer"
139
+ VIZ="${SMIDJA_VIZ:-}"
122
140
  VIZ_PORT="${SMIDJA_VIZ_API_PORT:-8437}"
123
141
  if [ ! -d "$VIZ" ]; then
124
142
  add visualizer SKIP "no visualizer tree at $VIZ"
@@ -182,12 +200,27 @@ else
182
200
  for i in "${!IDS[@]}"; do
183
201
  say " \"${IDS[$i]}\",\"${STATES[$i]}\",\"${DETAILS[$i]}\""
184
202
  done
203
+ # The same rows, rendered for the eye, on stderr: a human reads marks and
204
+ # colour, a pipeline reads the TOON above, and neither parses the other.
205
+ if [ "$QUIET" != 1 ]; then
206
+ printf '\n' >&2
207
+ for i in "${!IDS[@]}"; do
208
+ style_line "${STATES[$i]}" "${IDS[$i]}" "${DETAILS[$i]}"
209
+ done
210
+ fi
185
211
  fi
186
212
 
187
213
  if [ "$fails" -gt 0 ]; then
188
- [ "$QUIET" = 1 ] || printf '\n%s required check(s) failed — the install is not fully usable\n' "$fails"
214
+ [ "$QUIET" = 1 ] || {
215
+ style_rule 52
216
+ style_line FAIL "not usable" "$fails required check(s) failed, $warns warning(s)"
217
+ style_hint "mend it: ymir eir (diagnose every surface, then mend what is broken)"
218
+ }
189
219
  printf 'help: %s required check(s) failed, %s warning(s)\n' "$fails" "$warns" >&2
190
220
  exit 1
191
221
  fi
192
- [ "$QUIET" = 1 ] || printf '\nall required checks pass (%s warning(s))\n' "$warns"
222
+ [ "$QUIET" = 1 ] || {
223
+ style_rule 52
224
+ style_line OK "stands" "every required check passes ($warns warning(s))"
225
+ }
193
226
  exit 0