@zerwiz/ymir 0.1.7 → 0.1.9

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.
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,121 @@
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
+ # For a long hour: say what is happening and why it takes a while. A user who
99
+ # knows the halls are being set right waits; a user watching a silent cursor
100
+ # wonders whether it has broken.
101
+ style_patience() { # [what is being set right]
102
+ local what="${1:-the halls are being set right}"
103
+ printf '\n' >&2
104
+ style_line DO "much moves" "$what"
105
+ style_hint " this hour is long, and nothing of yours is lost in it —"
106
+ style_hint " roots come home, shapes are re-cut, names are set true again."
107
+ style_hint " Your patience is noted, and it is earned."
108
+ printf '\n' >&2
109
+ }
110
+
111
+ # The ending: what stands, then exactly what to type next. Every CLI deserves
112
+ # to leave the operator with the next step and nothing else to guess.
113
+ style_next() { # one command per line, as "verb — what it does"
114
+ printf '\n%s\n' "${C_BOLD}Where to go from here${C_OFF}" >&2
115
+ while [ $# -gt 0 ]; do
116
+ local cmd="${1%% — *}" what="${1#* — }"
117
+ printf ' %s%-22s%s %s\n' "$C_BRONZE" "$cmd" "$C_OFF" "$(printf '%s' "$what" | sed "s/^/$C_FAINT/;s/$/$C_OFF/")" >&2
118
+ shift
119
+ done
120
+ printf '\n' >&2
121
+ }
@@ -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
package/bin/ymir.js CHANGED
@@ -1,19 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * ymir — the CLI the npm package publishes.
3
+ * ymir — the CLI the npm package publishes, and the operator's front door.
4
4
  *
5
5
  * It is a thin, honest wrapper: the real work lives in the shell scripts under
6
- * bin/, which are the same scripts a git clone runs. This file exists because
7
- * npm needs a Node entry point (`bin: ymir`) and because a package user should
8
- * not have to know where the checkout landed.
6
+ * bin/ and scripts/ the same scripts a git clone runs. This file exists
7
+ * because npm needs a Node entry point (`bin: ymir`) and because an operator
8
+ * should never have to know where the package landed.
9
9
  *
10
- * ymir # the same as `ymir install`
11
- * ymir install [--yes] # first setup (idempotent, self-healing)
12
- * ymir sense # what THIS machine is
13
- * ymir validate # prove what stands
14
- * ymir migrate # heal an older home forward
10
+ * The doors are named for the figure whose work they do (the naming law):
11
+ * Eir heals, Gróa renews, Heimdall keeps the way in, Smíðja is the smithy and
12
+ * its board, Hlidskjalf the high seat, Sessrúmnir the seat-hall, Mímir the well.
13
+ *
14
+ * ymir first setup (the same as `ymir install`)
15
+ * ymir install [...] first setup, idempotent and self-healing
16
+ * ymir raise / lower lift the hall, or lay it down
17
+ * ymir eir what stands, and mend what does not
18
+ * ymir groa take the latest, and mend this home forward
19
+ * ymir heimdall the way in: your credential, and invites
20
+ * ymir smidja the smithy's board (:8437) — build · start · stop · status
21
+ * ymir hlidskjalf the high seat's window
22
+ * ymir sessrumnir the seat-hall's window
23
+ * ymir mimir the memory well
24
+ * ymir sense what THIS machine is
15
25
  * ymir --version
16
26
  *
27
+ * Colour appears only where a human is watching (a TTY, no NO_COLOR); the data
28
+ * on stdout is always plain TOON.
29
+ *
17
30
  * Exit codes follow the scripts: 0 ok, 1 error, 2 usage.
18
31
  */
19
32
  'use strict';
@@ -27,32 +40,65 @@ const pkg = (() => {
27
40
  try { return require(path.join(ROOT, 'package.json')); } catch { return { version: '0.0.0' }; }
28
41
  })();
29
42
 
30
- const TASKS = {
31
- install: 'ymir-install.sh',
32
- validate: 'ymir-validate.sh',
33
- migrate: 'ymir-migrate.sh',
34
- sense: 'host-sense.sh',
43
+ // ── the cloth (the same palette as bin/ymir-style.sh, cut from the tokens) ───
44
+ const colour = process.stderr.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
45
+ const wrap = (code) => (s) => (colour ? `\u001b[${code}m${s}\u001b[0m` : `${s}`);
46
+ const bone = wrap('38;2;207;195;169');
47
+ const bronze = wrap('38;2;201;151;79');
48
+ const faint = wrap('38;2;107;98;80');
49
+ const blood = wrap('38;2;194;88;74');
50
+ const bold = wrap('1');
51
+
52
+ // ── the doors ────────────────────────────────────────────────────────────────
53
+ // verb → { script, args } — args are prepended to whatever the operator passes,
54
+ // so a verb can be a doorway to a sub-verb of a script that has several.
55
+ const DOORS = {
56
+ install: { script: 'bin/ymir-install.sh', about: 'first setup (idempotent, self-healing)' },
57
+ raise: { script: 'scripts/start.sh', about: 'lift the hall — SPA, gate API, Nornir, bridges, the board' },
58
+ lower: { script: 'scripts/stop.sh', about: 'lay the hall down' },
59
+ eir: { script: 'bin/eir-doctor.sh', about: 'diagnose every surface; mend what is broken' },
60
+ groa: { script: 'bin/groa-update.sh', about: 'take the latest, then mend this home forward' },
61
+ heimdall: { script: 'bin/ymir-setup-auth.sh', about: 'the way in — your credential (status · set · github)' },
62
+ invite: { script: 'bin/ymir-invite.sh', about: 'let someone else in (mint · list · revoke)' },
63
+ smidja: { script: 'bin/smidja-board.sh', about: "the smithy's board on :8437 (build · start · stop · status)" },
64
+ hlidskjalf: { script: 'scripts/electron.sh', about: "the high seat's window", args: ['start', '--view', 'hlidskjalf'] },
65
+ sessrumnir: { script: 'scripts/electron.sh', about: "the seat-hall's window", args: ['start', '--view', 'sessrumnir'] },
66
+ mimir: { script: 'bin/mimir.sh', about: 'the memory well' },
67
+ sense: { script: 'bin/host-sense.sh', about: 'what THIS machine is' },
68
+ plan: { script: 'bin/ymir-plan.sh', about: 'what an install would do here — writes nothing' },
69
+ migrate: { script: 'bin/ymir-migrate.sh', about: "heal this home's structure forward (Gr\u00f3a's mend)" },
70
+ validate: { script: 'bin/ymir-validate.sh', about: 'alias of `ymir eir`' },
35
71
  };
36
72
 
73
+ // Names the law has not given a home are kept for a while, so a muscle memory
74
+ // built yesterday still works — and says what to type instead.
75
+ const RENAMED = { validate: 'eir', auth: 'heimdall', desktop: 'hlidskjalf', doctor: 'eir', update: 'groa' };
76
+
37
77
  function usage() {
38
- process.stdout.write(
39
- `ymir ${pkg.version} — the single-tenant agent operating system\n\n` +
40
- 'Usage:\n' +
41
- ' ymir first setup (same as `ymir install`)\n' +
42
- ' ymir install [--yes] first setup, idempotent and self-healing\n' +
43
- ' ymir sense report THIS machine (distro, session, desktop, capabilities)\n' +
44
- ' ymir validate verify the running system\n' +
45
- ' ymir migrate heal an older home forward\n' +
46
- ' ymir --version\n\n' +
47
- 'Output is TOON. The scripts are the source of truth; this CLI only points at them.\n'
48
- );
78
+ const lines = [];
79
+ lines.push('');
80
+ lines.push(` ${bronze('\u16c9')} ${bold('Ymir')} ${faint(pkg.version)}`);
81
+ lines.push(` ${faint('the single-tenant agent operating system')}`);
82
+ lines.push('');
83
+ lines.push(bold(' The doors'));
84
+ for (const [verb, d] of Object.entries(DOORS)) {
85
+ lines.push(` ${bronze(verb.padEnd(11))} ${faint(d.about)}`);
86
+ }
87
+ lines.push('');
88
+ lines.push(bold(' Composed by hand'));
89
+ lines.push(` ${bronze('ymir groa migrate'.padEnd(11))} ${faint("heal this home's structure (run by groa / install)")}`);
90
+ lines.push(` ${bronze('ymir heimdall set'.padEnd(11))} ${faint('set the operator password (or `github`)')}`);
91
+ lines.push('');
92
+ lines.push(faint(' A bare `ymir` runs the first setup. Output is TOON; colour only on a TTY.'));
93
+ lines.push('');
94
+ process.stdout.write(lines.join('\n') + '\n');
49
95
  }
50
96
 
51
97
  function run(script, args) {
52
- const file = path.join(ROOT, 'bin', script);
98
+ const file = path.join(ROOT, script);
53
99
  if (!fs.existsSync(file)) {
54
- process.stderr.write(`error: ${path.relative(ROOT, file)} is missing from this install\n`);
55
- process.stderr.write('help: reinstall — npm i -g @zerwiz/ymir, or git clone the distro\n');
100
+ process.stderr.write(`${blood('error:')} ${script} is missing from this install\n`);
101
+ process.stderr.write(`${faint('help: reinstall — npm i -g @zerwiz/ymir, or git clone the distro')}\n`);
56
102
  process.exit(1);
57
103
  }
58
104
  const r = spawnSync('bash', [file, ...args], { stdio: 'inherit' });
@@ -62,17 +108,19 @@ function run(script, args) {
62
108
  const argv = process.argv.slice(2);
63
109
  const first = argv[0];
64
110
 
65
- if (!first) run(TASKS.install, []);
111
+ if (!first) run(DOORS.install.script, []);
66
112
  if (first === '--version' || first === '-v' || first === '-V') {
67
113
  process.stdout.write(`${pkg.version}\n`);
68
114
  process.exit(0);
69
115
  }
70
116
  if (first === '--help' || first === '-h' || first === 'help') { usage(); process.exit(0); }
71
117
 
72
- const script = TASKS[first];
73
- if (!script) {
74
- process.stderr.write(`error: unknown command ${first}\n`);
75
- process.stderr.write('help: ymir [install|sense|validate|migrate|--version]\n');
118
+ let door = DOORS[first];
119
+ if (!door) {
120
+ const replacement = RENAMED[first];
121
+ process.stderr.write(`${blood('error:')} unknown door ${bold(first)}\n`);
122
+ if (replacement) process.stderr.write(`${faint(`help: the door is named \`ymir ${replacement}\` now (the naming law — a figure does the work)`)}\n`);
123
+ process.stderr.write(`${faint('help: `ymir --help` lists every door')}\n`);
76
124
  process.exit(2);
77
125
  }
78
- run(script, argv.slice(1));
126
+ run(door.script, [...(door.args || []), ...argv.slice(1)]);
package/docs/design.md CHANGED
@@ -226,7 +226,7 @@ everywhere; never put bronze text on bronze.
226
226
  - **Runecoded, not emoji.** A minimal, hand-drawn-style rune set replaces emoji
227
227
  in all UI: 16×16 and 24×24 grid, 2px stroke, chisel bevel on primary.
228
228
  - System subsystems each carry one rune-glyph (reuse Algiz-family geometry:
229
- Bifrost gate, Ratatoskr squirrel, the Well). Map is in `midgard/design-system/icons.md` (TBD) created from this doc.
229
+ Bifrost gate, Ratatoskr squirrel, the Well). Map is in `midgard/design-system/icons.md` and the library in `midgard/design-system/runes.md`.
230
230
  - Status dot: filled rune ring (ok) / half-ring (warn) / broken ring (danger), never color-only.
231
231
 
232
232
  ### 4.5 Surface & texture
@@ -345,7 +345,7 @@ All components read tokens from `tokens.css`; color-only states are forbidden
345
345
 
346
346
  - `docs/design.md` — this document (master).
347
347
  - `midgard/design-system/tokens.css` — the consumable design tokens.
348
- - `midgard/design-system/icons.md` — rune-glyph map (to create).
348
+ - `midgard/design-system/icons.md` — rune-glyph map (21 glyphs, drawn in `icons/*.svg`).
349
349
  - `docs/ymir-rut.md` Part 3 — the original Rut design-system specification this
350
350
  document implements.
351
351
  - `docs/lore.md` §VI — house lineage behind the house accents.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerwiz/ymir",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Ymir \u2014 the single-tenant agent operating system. Omarchy-first, host-aware, self-healing.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/zerwiz/ymir",
@@ -26,6 +26,19 @@ fi
26
26
  VERSION="1.1.0"
27
27
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
28
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
29
+ # Where an app lives: apps/<surface> in a clone, node_modules/@zerwiz/<pkg> in an
30
+ # npm install — both shapes, one resolver (bin/app-lib.sh).
31
+ if [ -z "${YMIR_APP_LIB_LOADED:-}" ]; then
32
+ _ya="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ for _yac in "$_ya/app-lib.sh" "$(dirname "$_ya")/bin/app-lib.sh"; do
34
+ [ -r "$_yac" ] && { . "$_yac"; YMIR_APP_LIB_LOADED=1; break; }
35
+ done
36
+ unset _ya _yac
37
+ fi
38
+ app_dir hlidskjalf APP_HLIDSKJALF || APP_HLIDSKJALF=""
39
+ app_dir odrerir APP_ODRERIR || APP_ODRERIR=""
40
+ app_dir sessrumnir APP_SESSRUMNIR || APP_SESSRUMNIR=""
41
+
29
42
 
30
43
  # The roots that live OUTSIDE the code tree: this machine's records and the
31
44
  # runtime state belong to the home the operator chose at installation, never in
@@ -39,8 +52,8 @@ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
39
52
  fi
40
53
  hoard_state_dir YMIR_STATE_DIR
41
54
  hoard_data_dir YMIR_DATA_DIR
42
- APP="$ROOT/apps/hlidskjalf"
43
- Odrerir_app="$ROOT/apps/odrerir"
55
+ APP="$APP_HLIDSKJALF"
56
+ Odrerir_app="$APP_ODRERIR"
44
57
  NO_INSTALL=0
45
58
  VIEW="${YMIR_DESKTOP_VIEW:-hlidskjalf}"
46
59
  VIEWS=(hlidskjalf smidja odrerir)
package/scripts/start.sh CHANGED
@@ -17,6 +17,33 @@ if [ -z "${YMIR_PLATFORM_LOADED:-}" ]; then
17
17
  fi
18
18
 
19
19
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ # The cloth (bin/ymir-style.sh) — colour and words for the human watching.
21
+ if [ -z "${YMIR_STYLE_LOADED:-}" ] && [ -r "$ROOT/bin/ymir-style.sh" ]; then
22
+ . "$ROOT/bin/ymir-style.sh"; YMIR_STYLE_LOADED=1; style_init
23
+ fi
24
+ # Where an app lives: apps/<surface> in a clone, node_modules/@zerwiz/<pkg> in an
25
+ # npm install — both shapes, one resolver (bin/app-lib.sh).
26
+ if [ -z "${YMIR_APP_LIB_LOADED:-}" ]; then
27
+ _ya="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ for _yac in "$_ya/app-lib.sh" "$(dirname "$_ya")/bin/app-lib.sh"; do
29
+ [ -r "$_yac" ] && { . "$_yac"; YMIR_APP_LIB_LOADED=1; break; }
30
+ done
31
+ unset _ya _yac
32
+ fi
33
+ app_dir hlidskjalf APP_HLIDSKJALF || APP_HLIDSKJALF=""
34
+
35
+ # Where the smithy's parts live: apps/smidja-factory in a clone, or the
36
+ # @zerwiz/smidja-factory package in an npm install (bin/smidja-lib.sh).
37
+ if [ -z "${YMIR_SMIDJA_LIB_LOADED:-}" ]; then
38
+ _ys="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ for _yc in "$_ys/smidja-lib.sh" "$(dirname "$_ys")/bin/smidja-lib.sh"; do
40
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_SMIDJA_LIB_LOADED=1; break; }
41
+ done
42
+ unset _ys _yc
43
+ fi
44
+ smidja_visualizer_dir SMIDJA_VIZ
45
+ smidja_factory_dir SMIDJA_FACTORY
46
+
20
47
 
21
48
  # The operator's settings and secrets live in the home they chose, never in the
22
49
  # code tree — a packaged install replaces its tree on upgrade, and a credential
@@ -36,7 +63,7 @@ hoard_data_dir YMIR_DATA_DIR
36
63
  # The roots that live OUTSIDE the code tree: this machine's records and the
37
64
  # runtime state belong to the home the operator chose at installation, never in
38
65
  # the tree — a packaged install replaces its tree on upgrade (Rule 04).
39
- APP="$ROOT/apps/hlidskjalf"
66
+ APP="$APP_HLIDSKJALF"
40
67
  RUN="$ROOT/.run"
41
68
  PID_FILE="$RUN/hlidskjalf.pid"
42
69
  LOG="$RUN/hlidskjalf.log"
@@ -68,7 +95,8 @@ else
68
95
  fi
69
96
 
70
97
  if [[ ! -d "$APP/node_modules" ]]; then
71
- echo "Installing dependencies"
98
+ style_patience "the seat's own dependencies are being fetched, then the hall is raised"
99
+ echo "Installing dependencies…"
72
100
  (cd "$APP" && npm install --no-audit --no-fund)
73
101
  fi
74
102
 
@@ -116,7 +144,7 @@ fi
116
144
  # Smíðja's eye — the Vue trace visualizer (API :8437, UI :8438). Reads the repo's
117
145
  # own smidja.db and exposes the Sessions/Trace/Decisions/Stats views behind the
118
146
  # Hlidskjalf Sessions gate's "Open visualizer" button.
119
- VIZ_DIR="$ROOT/.agents/skills/smidja-factory/apps/visualizer"
147
+ VIZ_DIR="${SMIDJA_VIZ:-}"
120
148
  VIZ_API_PORT="${SMIDJA_VIZ_API_PORT:-8437}"
121
149
  VIZ_UI_PORT="${SMIDJA_VIZ_UI_PORT:-8438}"
122
150
  VIZ_API_PID_FILE="$RUN/smidja-viz-api.pid"