@zerwiz/ymir 0.1.5 → 0.1.7
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/.agents/agents/huginn-researcher.md +1 -1
- package/.agents/agents/kvasir-scout.md +1 -1
- package/.agents/agents/sindri-developer.md +1 -1
- package/.agents/backend/README.md +74 -0
- package/.agents/backend/model-bridge.py +74 -4
- package/.agents/config/cron.yaml +2 -0
- package/.agents/config/eindri-dispatch.json +6 -6
- package/.agents/migrations/0003-private-data-separation.sh +11 -1
- package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
- package/.agents/skills/README.md +1 -1
- package/.agents/skills/galdr-ymirsystem/SKILL.md +2 -1
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +73 -10
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +44 -1
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +215 -23
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +3 -1
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +39 -1
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +4 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +7 -2
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +1 -1
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +2 -2
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +16 -11
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +147 -4
- package/.agents/skills/groa-update/SKILL.md +20 -0
- package/.agents/skills/hamr-adapters/SKILL.md +2 -2
- package/.agents/skills/lifecycle/smoke_test.sh +7 -2
- package/.agents/skills/ymir-host/SKILL.md +2 -1
- package/.agents/skills/ymir-host/assets/install.md +69 -0
- package/AGENTS.md +78 -34
- package/CHANGELOG.md +1033 -1
- package/README.md +1 -1
- package/RULES/02-agents.md +2 -2
- package/RULES/04-hoard.md +32 -0
- package/RULES/06-append-only.md +50 -0
- package/RULES/07-config.md +56 -0
- package/RULES/09-electron.md +58 -0
- package/RULES/README.md +2 -0
- package/bin/a2a-mcp.sh +42 -7
- package/bin/agents-config.sh +30 -7
- package/bin/apodex-smoke-test.sh +14 -1
- package/bin/bifrost-bridge.sh +25 -6
- package/bin/bootstrap-macos.sh +6 -1
- package/bin/brokk-send.sh +14 -1
- package/bin/changelog-assemble.sh +154 -0
- package/bin/changelog-guard.sh +39 -7
- package/bin/crash-sense.sh +14 -1
- package/bin/design-check.sh +80 -0
- package/bin/design-icon.sh +153 -0
- package/bin/docs-guard.sh +2 -2
- package/bin/editor-place.sh +33 -15
- package/bin/eindri-acclaim.sh +14 -1
- package/bin/eindri-seat.sh +83 -0
- package/bin/eindri-seen.sh +14 -1
- package/bin/eindri-start.sh +14 -1
- package/bin/eir-doctor.sh +47 -1
- package/bin/feature-inventory.sh +73 -0
- package/bin/fleet-apply.sh +15 -2
- package/bin/github-deploy.sh +14 -1
- package/bin/gjallarhorn-expose.sh +43 -9
- package/bin/gjallarhorn-notify.sh +14 -1
- package/bin/gjallarhorn-purge.sh +14 -1
- package/bin/gjallarhorn-tunnel.sh +16 -3
- package/bin/groa-update.sh +9 -1
- package/bin/hall-snapshot.sh +21 -2
- package/bin/herdr-run.sh +23 -2
- package/bin/hlidskjalf-agents.sh +172 -0
- package/bin/hlidskjalf-usage.sh +196 -0
- package/bin/hoard-guard.sh +135 -0
- package/bin/hoard-lib.sh +91 -11
- package/bin/hodd.sh +28 -4
- package/bin/huginn-research-worker.sh +14 -1
- package/bin/local-model-lock.sh +22 -3
- package/bin/mimir-bridge.py +33 -0
- package/bin/mimir-bridge.sh +14 -1
- package/bin/mimir-reflect.sh +27 -0
- package/bin/mjollnir-webhook.sh +14 -1
- package/bin/mjollnir.sh +22 -3
- package/bin/model-hardware.sh +17 -3
- package/bin/model-resolve.sh +14 -1
- package/bin/nornir-job-hall-snapshot.sh +28 -0
- package/bin/nornir-job-nsr-compliance.sh +54 -0
- package/bin/npm-publish.sh +136 -0
- package/bin/omarchy-install.sh +18 -5
- package/bin/omarchy-plugins.sh +15 -2
- package/bin/omarchy-sense.sh +14 -1
- package/bin/pi-model-agnostic.sh +71 -0
- package/bin/private-guard.sh +20 -0
- package/bin/project-git.sh +18 -4
- package/bin/public-guard.sh +3 -2
- package/bin/sessrumnir-ensure.sh +1 -1
- package/bin/sessrumnir.sh +16 -3
- package/bin/smidja-bootstrap.sh +5 -1
- package/bin/syn-asset-pretool-check.sh +14 -1
- package/bin/syn-spawn-pretool-check.sh +58 -0
- package/bin/tailscale-sync.sh +15 -2
- package/bin/telegram-bot.sh +14 -1
- package/bin/ui-truth-check.sh +75 -0
- package/bin/valknut-load.sh +42 -4
- package/bin/wedge-notify.sh +14 -1
- package/bin/workspace-rag.sh +14 -1
- package/bin/ymir-install.sh +219 -54
- package/bin/ymir-plan.sh +356 -0
- package/bin/ymir-say.sh +14 -1
- package/bin/ymir-setup-auth.sh +12 -1
- package/bin/ymir-validate.sh +28 -8
- package/docs/lore.md +5 -1
- package/docs/runbooks/agents.md +1 -1
- package/package.json +13 -3
- package/scripts/electron.sh +74 -15
- package/scripts/lower.sh +34 -0
- package/scripts/raise.sh +42 -0
- package/scripts/start.sh +52 -2
- package/.agents/config/agents.yaml +0 -50
- package/.agents/config/tailscale-sync.yaml +0 -27
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/well/episodes.jsonl +0 -367
- package/.agents/memory/well/workspace.jsonl +0 -27
package/bin/ymir-plan.sh
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ymir-plan.sh — THE INSTALL PLAN. Probe this machine, print what would change,
|
|
3
|
+
# change nothing.
|
|
4
|
+
#
|
|
5
|
+
# The consent a real install asks for used to be a hardcoded paragraph, and a
|
|
6
|
+
# paragraph cannot know the host: it named an Omarchy version on a Mac, promised
|
|
7
|
+
# a workspace tree that already stood, and never mentioned that no application
|
|
8
|
+
# had been installed at all. This is the replacement: a plan COMPUTED from the
|
|
9
|
+
# same host the install will act on, one row per step, each row carrying its
|
|
10
|
+
# state and the reason for it.
|
|
11
|
+
#
|
|
12
|
+
# DO a change will be made
|
|
13
|
+
# SKIP already satisfied — nothing to do
|
|
14
|
+
# DO · (with a note) the step is available but conditional
|
|
15
|
+
# BLOCKED cannot run; the reason names what is missing
|
|
16
|
+
# CONSENT needs the operator's word before it may run
|
|
17
|
+
# INFO a fact about this host, discovered — no change implied
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# bin/ymir-plan.sh # the plan (TOON)
|
|
21
|
+
# bin/ymir-plan.sh --json # the same, as JSON, for automation
|
|
22
|
+
# bin/ymir-plan.sh --phase 5 # one phase
|
|
23
|
+
# bin/ymir-plan.sh --blocked # only what cannot proceed, and why
|
|
24
|
+
#
|
|
25
|
+
# Exit: 0 the plan was printed (a BLOCKED row is a fact, not a failure), 1 error,
|
|
26
|
+
# 2 usage.
|
|
27
|
+
set -u
|
|
28
|
+
|
|
29
|
+
# --- portability shim: bin/ymir-platform.sh --------------------------------
|
|
30
|
+
if [ -z "${YMIR_PLATFORM_LOADED:-}" ]; then
|
|
31
|
+
_ymir_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
32
|
+
for _ymir_c in "$_ymir_dir/ymir-platform.sh" "$(dirname "$_ymir_dir")/bin/ymir-platform.sh"; do
|
|
33
|
+
[ -r "$_ymir_c" ] && { . "$_ymir_c"; YMIR_PLATFORM_LOADED=1; break; }
|
|
34
|
+
done
|
|
35
|
+
unset _ymir_dir _ymir_c
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
VERSION="1.0.0"
|
|
39
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
40
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
41
|
+
# Whether the home was named explicitly matters: a home that was chosen is not
|
|
42
|
+
# the same as one we defaulted to, and the plan says which.
|
|
43
|
+
YMIR_HOME_WAS_SET=0; [ -n "${YMIR_HOME:-}" ] && YMIR_HOME_WAS_SET=1
|
|
44
|
+
# shellcheck source=bin/hoard-lib.sh
|
|
45
|
+
. "$SCRIPT_DIR/hoard-lib.sh"
|
|
46
|
+
ymir_home_root YMIR_HOME
|
|
47
|
+
hoard_root HOARD
|
|
48
|
+
|
|
49
|
+
JSON=0; ONLY_PHASE=""; ONLY_BLOCKED=0
|
|
50
|
+
case "${1-}" in
|
|
51
|
+
-v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
|
|
52
|
+
-h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
53
|
+
esac
|
|
54
|
+
while [ $# -gt 0 ]; do
|
|
55
|
+
case "$1" in
|
|
56
|
+
--json) JSON=1; shift ;;
|
|
57
|
+
--phase) ONLY_PHASE="${2-}"; shift 2 ;;
|
|
58
|
+
--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
|
+
esac
|
|
61
|
+
done
|
|
62
|
+
|
|
63
|
+
declare -a P_N P_NAME P_STEP P_STATE P_WHY
|
|
64
|
+
emit() { P_N+=("$1"); P_NAME+=("$2"); P_STEP+=("$3"); P_STATE+=("$4"); P_WHY+=("$5"); }
|
|
65
|
+
|
|
66
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
67
|
+
port_up() { # <port>
|
|
68
|
+
if have curl; then curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$1/" 2>/dev/null && return 0; fi
|
|
69
|
+
if have ss; then ss -ltn 2>/dev/null | grep -q ":$1 " && return 0; fi
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
# An app is ours by one of two shapes: a source checkout under apps/, or a
|
|
73
|
+
# 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
|
|
77
|
+
[ -d "$c" ] && { printf '%s' "$c"; return 0; }
|
|
78
|
+
done
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# ── phase 0 · resolve — where this runs, and against what ────────────────────
|
|
83
|
+
resolve_phase() {
|
|
84
|
+
local os="" desk="terminal"
|
|
85
|
+
os="$(ymir_os 2>/dev/null || printf unknown)"
|
|
86
|
+
[ -n "${WAYLAND_DISPLAY:-}" ] && desk="wayland"
|
|
87
|
+
[ -n "${DISPLAY:-}" ] && desk="x11"
|
|
88
|
+
[ -d /usr/share/omarchy ] && desk="$desk · omarchy first-class"
|
|
89
|
+
case "$os" in
|
|
90
|
+
wsl) desk="$desk · wsl2" ;;
|
|
91
|
+
msys) desk="$desk · msys (best-effort)" ;;
|
|
92
|
+
esac
|
|
93
|
+
emit 0 resolve os INFO "$os · $desk"
|
|
94
|
+
|
|
95
|
+
local eng=""
|
|
96
|
+
if command -v ymir_container_engine >/dev/null 2>&1; then eng="$(ymir_container_engine_name 2>/dev/null || true)"; fi
|
|
97
|
+
[ -z "$eng" ] && have docker && eng=docker
|
|
98
|
+
[ -z "$eng" ] && have podman && eng=podman
|
|
99
|
+
if [ -n "$eng" ]; then emit 0 resolve engine INFO "$eng"
|
|
100
|
+
else emit 0 resolve engine BLOCKED "no docker or podman on PATH — the Utgard image cannot be built"; fi
|
|
101
|
+
|
|
102
|
+
case "$ROOT" in
|
|
103
|
+
*/node_modules/*) emit 0 resolve code INFO "$ROOT — a package install (read-only at runtime)" ;;
|
|
104
|
+
*) emit 0 resolve code INFO "$ROOT — a source checkout" ;;
|
|
105
|
+
esac
|
|
106
|
+
|
|
107
|
+
case "$HOARD" in
|
|
108
|
+
"$ROOT"/*) emit 0 resolve home BLOCKED "the home ($HOARD) sits inside the code tree — private data would live in the package" ;;
|
|
109
|
+
*) emit 0 resolve home INFO "$HOARD (the home = $YMIR_HOME)" ;;
|
|
110
|
+
esac
|
|
111
|
+
|
|
112
|
+
# The home is the OPERATOR's to choose. Until they have, it is only a default,
|
|
113
|
+
# and the install will ask — so the plan says so rather than pretending.
|
|
114
|
+
local rec=""
|
|
115
|
+
ymir_home_record rec
|
|
116
|
+
if [ -n "$rec" ]; then
|
|
117
|
+
emit 0 resolve chosen SKIP "the home was chosen at installation: $rec"
|
|
118
|
+
elif [ "$YMIR_HOME_WAS_SET" = 1 ]; then
|
|
119
|
+
emit 0 resolve chosen SKIP "the home is set by \$YMIR_HOME: $YMIR_HOME"
|
|
120
|
+
else
|
|
121
|
+
emit 0 resolve chosen CONSENT "no home chosen yet — the install asks, then records it (default $YMIR_HOME_DEFAULT)"
|
|
122
|
+
fi
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ── phase 1 · code — the tree itself, and its purity ────────────────────────
|
|
126
|
+
code_phase() {
|
|
127
|
+
local n=0
|
|
128
|
+
[ -x "$ROOT/bin/ymir-install.sh" ] && n=$(find "$ROOT/bin" -maxdepth 1 -name '*.sh' 2>/dev/null | wc -l)
|
|
129
|
+
if [ "$n" -gt 0 ] && [ -d "$ROOT/.agents/skills" ] && [ -d "$ROOT/RULES" ]; then
|
|
130
|
+
emit 1 code integrity SKIP "tree intact — $n scripts, .agents/skills, RULES/"
|
|
131
|
+
else
|
|
132
|
+
emit 1 code integrity BLOCKED "bin/, .agents/skills or RULES/ is missing — reinstall the package or re-clone"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# The law this row enforces: the package is the code that RUNS the programs.
|
|
136
|
+
# Everything the operator owns — their records, their state, their settings,
|
|
137
|
+
# their credentials — lives in the home they chose. A packaged install replaces
|
|
138
|
+
# its tree on upgrade, so anything of theirs kept here is kept at its peril.
|
|
139
|
+
# A settings file GIT TRACKS is not theirs: that is the distro's shipped
|
|
140
|
+
# default, and it stays where the code ships it.
|
|
141
|
+
local leaked="" f n
|
|
142
|
+
n="$(ls -A "$ROOT/data" 2>/dev/null | grep -vc '^\.gitkeep$' || true)"
|
|
143
|
+
[ -d "$ROOT/data" ] && [ "${n:-0}" -gt 0 ] && leaked="$leaked data/"
|
|
144
|
+
n="$(ls -A "$ROOT/state" 2>/dev/null | grep -vc '^\.gitkeep$' || true)"
|
|
145
|
+
[ -d "$ROOT/state" ] && [ "${n:-0}" -gt 0 ] && leaked="$leaked state/"
|
|
146
|
+
for f in config/agents.yaml config/cron.yaml config/tailscale-sync.yaml config/wedge-alarm .env.local; do
|
|
147
|
+
[ -e "$ROOT/$f" ] || continue
|
|
148
|
+
# `config` is a symlink into .agents/config, and git tracks the REAL path —
|
|
149
|
+
# ask it about the name the index holds, not the link the tree shows.
|
|
150
|
+
local ask="$f"
|
|
151
|
+
case "$f" in config/*) [ -L "$ROOT/config" ] && ask=".agents/config/${f#config/}" ;; esac
|
|
152
|
+
if command -v git >/dev/null 2>&1 && git -C "$ROOT" ls-files --error-unmatch "$ask" >/dev/null 2>&1; then
|
|
153
|
+
continue # the distro ships this one — code, not the operator's own
|
|
154
|
+
fi
|
|
155
|
+
leaked="$leaked $f"
|
|
156
|
+
done
|
|
157
|
+
if [ -n "$leaked" ]; then
|
|
158
|
+
emit 1 code purity DO "the operator's own things sit in the code tree ($leaked) — they belong in $YMIR_HOME, or the next upgrade will erase them"
|
|
159
|
+
else
|
|
160
|
+
emit 1 code purity SKIP "nothing of the operator's is written into the code tree"
|
|
161
|
+
fi
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# ── phase 2 · home — the operator's world, outside the tree ─────────────────
|
|
165
|
+
home_phase() {
|
|
166
|
+
if [ -d "$HOARD/identity" ] && [ -d "$HOARD/secrets" ]; then
|
|
167
|
+
emit 2 home hoard SKIP "hodd/{identity,secrets,memory} present"
|
|
168
|
+
else
|
|
169
|
+
emit 2 home hoard DO "create hodd/{identity,secrets,docs,tenants,memory} — all private data lives here"
|
|
170
|
+
fi
|
|
171
|
+
if [ -d "$YMIR_HOME/workspaces" ]; then
|
|
172
|
+
emit 2 home workspaces SKIP "$YMIR_HOME/workspaces present"
|
|
173
|
+
else
|
|
174
|
+
emit 2 home workspaces DO "create workspaces/{work,personal} and the registries"
|
|
175
|
+
fi
|
|
176
|
+
# Settings are the operator's: they live in the home, never in the package.
|
|
177
|
+
local settings; hoard_settings_dir settings
|
|
178
|
+
if [ -e "$settings/agents.yaml" ]; then
|
|
179
|
+
emit 2 home agents-config SKIP "the agent set is seeded ($settings/agents.yaml)"
|
|
180
|
+
else
|
|
181
|
+
emit 2 home agents-config DO "seed the private agent set into $settings/agents.yaml (harness + model per agent)"
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# ── phase 3 · runtimes — every one proven by a probe, never assumed ─────────
|
|
186
|
+
runtimes_phase() {
|
|
187
|
+
local missing=""
|
|
188
|
+
for c in git python3; do have "$c" || missing="$missing $c"; done
|
|
189
|
+
if [ -n "$missing" ]; then
|
|
190
|
+
emit 3 runtimes system BLOCKED "missing$missing — a system package (apt/dnf/brew), not user space"
|
|
191
|
+
else
|
|
192
|
+
emit 3 runtimes system SKIP "git and python3 present"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
local user=""
|
|
196
|
+
for c in bun uv; do have "$c" || user="$user $c"; done
|
|
197
|
+
if [ -n "$user" ]; then emit 3 runtimes userspace DO "install in user space (no sudo):$user"
|
|
198
|
+
else emit 3 runtimes userspace SKIP "bun and uv present"; fi
|
|
199
|
+
|
|
200
|
+
if have mcp; then emit 3 runtimes mcp SKIP "mcp<2> present"; else emit 3 runtimes mcp DO "install mcp<2> via pip"; fi
|
|
201
|
+
if have pi; then emit 3 runtimes pi SKIP "the Pi harness present"; else emit 3 runtimes pi DO "install the Pi harness"; fi
|
|
202
|
+
if have hermes; then emit 3 runtimes hermes SKIP "the Hermes worker runtime present"; else emit 3 runtimes hermes DO "install the Hermes worker runtime"; fi
|
|
203
|
+
|
|
204
|
+
if have herdr; then emit 3 runtimes backend SKIP "herdr — Þjazi (the preferred terminal backend)"
|
|
205
|
+
elif have tmux; then emit 3 runtimes backend SKIP "tmux — the accepted reference backend"
|
|
206
|
+
else emit 3 runtimes backend DO "install herdr (Þjazi), else tmux — without a backend no Eindri can be seated"; fi
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# ── phase 4 · engines — the OSS anvils Ymir wraps ───────────────────────────
|
|
210
|
+
engine_row() { # <bin> <label>
|
|
211
|
+
if have "$1"; then emit 4 engines "$1" SKIP "$2 present"
|
|
212
|
+
else emit 4 engines "$1" DO "install $2 ($1)"; fi
|
|
213
|
+
}
|
|
214
|
+
engines_phase() {
|
|
215
|
+
engine_row treehouse "Yggdrasil (the worktree engine)"
|
|
216
|
+
engine_row no-mistakes "the clean-PR gate (Mjollnir · Glitnir)"
|
|
217
|
+
if have sandcastle; then emit 4 engines sandcastle SKIP "Utgard present"
|
|
218
|
+
else emit 4 engines sandcastle DO "install sandcastle (Utgard) if available"; fi
|
|
219
|
+
|
|
220
|
+
if have docker || have podman; then
|
|
221
|
+
local img
|
|
222
|
+
if have docker && docker image inspect utgard-runner:latest >/dev/null 2>&1; then
|
|
223
|
+
emit 4 engines utgard-image SKIP "utgard-runner:latest present"
|
|
224
|
+
elif have podman && podman image exists utgard-runner:latest >/dev/null 2>&1; then
|
|
225
|
+
emit 4 engines utgard-image SKIP "utgard-runner:latest present (podman)"
|
|
226
|
+
else
|
|
227
|
+
emit 4 engines utgard-image DO "build the sandbox image utgard-runner:latest"
|
|
228
|
+
fi
|
|
229
|
+
else
|
|
230
|
+
emit 4 engines utgard-image BLOCKED "no container engine — the sandbox cannot be built"
|
|
231
|
+
fi
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# ── phase 5 · apps — the surfaces the operator actually sees ────────────────
|
|
235
|
+
# Four surfaces, one shape each: web build required, Electron shell gated.
|
|
236
|
+
# 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
|
|
240
|
+
if [ -d "$dir/dist" ] || [ -d "$dir/out" ]; then
|
|
241
|
+
emit 5 apps "$name" SKIP "$about — installed, and its build shipped with it"
|
|
242
|
+
else
|
|
243
|
+
emit 5 apps "$name" DO "$about — installed from source; the web build is still to make"
|
|
244
|
+
fi
|
|
245
|
+
elif grep -q "\"@zerwiz/$name\"" "$ROOT/package.json" 2>/dev/null; then
|
|
246
|
+
# Declared as a dependency of the distro but not present: the package exists,
|
|
247
|
+
# the tree simply has not fetched it. That is a DO, not a dead end.
|
|
248
|
+
emit 5 apps "$name" DO "$about — declared as a dependency but not fetched (npm i -g @zerwiz/ymir fetches it)"
|
|
249
|
+
else
|
|
250
|
+
emit 5 apps "$name" BLOCKED "$about — no apps/$name, no @zerwiz/$name package, and the distro does not depend on it"
|
|
251
|
+
fi
|
|
252
|
+
}
|
|
253
|
+
apps_phase() {
|
|
254
|
+
app_row hlidskjalf "the control plane (the high seat), served on :3888"
|
|
255
|
+
app_row odrerir "the live hall (chat + council)"
|
|
256
|
+
app_row sessrumnir "the seat-hall desktop"
|
|
257
|
+
app_row smidja "the smithy and its visualizer (:8437)"
|
|
258
|
+
|
|
259
|
+
local shells=0 d
|
|
260
|
+
for d in hlidskjalf odrerir sessrumnir; do
|
|
261
|
+
dir="$(app_dir "$d" 2>/dev/null || true)"
|
|
262
|
+
if [ -n "$dir" ] && [ -d "$dir/node_modules/electron/dist" ]; then shells=$((shells+1)); fi
|
|
263
|
+
done
|
|
264
|
+
if [ "$shells" -ge 3 ]; then
|
|
265
|
+
emit 5 apps electron SKIP "the three desktop shells are built (electron runtime present)"
|
|
266
|
+
elif [ "${YMIR_NO_DESKTOP:-0}" = 1 ]; then
|
|
267
|
+
emit 5 apps electron SKIP "declined (--no-desktop) — the web surfaces stand without the shells"
|
|
268
|
+
else
|
|
269
|
+
emit 5 apps electron CONSENT "build the desktop shells? each pulls a ~100 MB Electron runtime — the web surfaces need none of it"
|
|
270
|
+
fi
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# ── phase 6 · wire — the way in, and the agent set ──────────────────────────
|
|
274
|
+
wire_phase() {
|
|
275
|
+
local door="none"
|
|
276
|
+
if [ -x "$ROOT/bin/ymir-setup-auth.sh" ]; then
|
|
277
|
+
door="$(bash "$ROOT/bin/ymir-setup-auth.sh" status 2>/dev/null | sed -n '2p' | cut -d, -f1 | tr -d ' "')"
|
|
278
|
+
fi
|
|
279
|
+
if [ -n "$door" ] && [ "$door" != none ]; then
|
|
280
|
+
emit 6 wire auth SKIP "an operator credential is set ($door)"
|
|
281
|
+
else
|
|
282
|
+
emit 6 wire auth CONSENT "no credential — a password or GitHub sign-in; without it the gate has no door"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
local codes=""
|
|
286
|
+
[ -x "$ROOT/bin/ymir-invite.sh" ] && codes="$(bash "$ROOT/bin/ymir-invite.sh" list 2>/dev/null | grep -c '^ "' || true)"
|
|
287
|
+
if [ "${codes:-0}" -gt 0 ]; then emit 6 wire invite SKIP "a live invite code exists"
|
|
288
|
+
else emit 6 wire invite CONSENT "mint an invite code — how anyone else is let in (registration stays closed without one)"; fi
|
|
289
|
+
|
|
290
|
+
if [ -d /usr/share/omarchy ]; then
|
|
291
|
+
emit 6 wire launchers DO "place each app on its own numbered desktop + write the launcher entries"
|
|
292
|
+
else
|
|
293
|
+
emit 6 wire launchers SKIP "not an Omarchy host — the Omarchy layer skips cleanly (Rule 05)"
|
|
294
|
+
fi
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# ── phase 7 · raise — the services, then what the operator sees ─────────────
|
|
298
|
+
raise_phase() {
|
|
299
|
+
local down="" p
|
|
300
|
+
for p in 3888:Hlidskjalf 3889:gate-API 4603:Bifrost 8437:Smidja-visualizer; do
|
|
301
|
+
port_up "${p%%:*}" || down="$down ${p#*:}"
|
|
302
|
+
done
|
|
303
|
+
if [ -n "$down" ]; then emit 7 raise services DO "raise:$down"
|
|
304
|
+
else emit 7 raise services SKIP "every service is listening (SPA · gate API · Bifrost · visualizer)"; fi
|
|
305
|
+
if have hermes; then emit 7 raise well INFO "the memory well (engram) is provisioned by its own step"; fi
|
|
306
|
+
emit 7 raise desktop CONSENT "open the desktop apps so they are seen, not merely installed"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# ── phase 8 · verify — what stands, honestly ────────────────────────────────
|
|
310
|
+
verify_phase() {
|
|
311
|
+
if [ -x "$SCRIPT_DIR/ymir-validate.sh" ]; then
|
|
312
|
+
emit 8 verify validate DO "observe the result: ports, stores, processes, the sandbox image"
|
|
313
|
+
else
|
|
314
|
+
emit 8 verify validate BLOCKED "bin/ymir-validate.sh is absent — the install could not be proven"
|
|
315
|
+
fi
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
resolve_phase; code_phase; home_phase; runtimes_phase; engines_phase; apps_phase; wire_phase; raise_phase; verify_phase
|
|
319
|
+
|
|
320
|
+
# ── output ──────────────────────────────────────────────────────────────────
|
|
321
|
+
if [ "$JSON" = 1 ]; then
|
|
322
|
+
printf '['
|
|
323
|
+
local_i=0
|
|
324
|
+
for i in "${!P_STEP[@]}"; do
|
|
325
|
+
[ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
|
|
326
|
+
[ "$ONLY_BLOCKED" = 1 ] && [ "${P_STATE[$i]}" != BLOCKED ] && continue
|
|
327
|
+
[ "$local_i" -gt 0 ] && printf ','
|
|
328
|
+
printf '\n {"phase":%s,"name":"%s","step":"%s","state":"%s","why":"%s"}' \
|
|
329
|
+
"${P_N[$i]}" "${P_NAME[$i]}" "${P_STEP[$i]}" "${P_STATE[$i]}" "${P_WHY[$i]}"
|
|
330
|
+
local_i=$((local_i+1))
|
|
331
|
+
done
|
|
332
|
+
[ "$local_i" -gt 0 ] && printf '\n'
|
|
333
|
+
printf ']\n'
|
|
334
|
+
exit 0
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
shown=0
|
|
338
|
+
for i in "${!P_STEP[@]}"; do
|
|
339
|
+
[ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
|
|
340
|
+
[ "$ONLY_BLOCKED" = 1 ] && [ "${P_STATE[$i]}" != BLOCKED ] && continue
|
|
341
|
+
shown=$((shown+1))
|
|
342
|
+
done
|
|
343
|
+
|
|
344
|
+
if [ "$shown" -eq 0 ]; then
|
|
345
|
+
if [ "$ONLY_BLOCKED" = 1 ]; then printf 'plan: 0 blocked steps — nothing is holding the install back\n'
|
|
346
|
+
else printf 'plan: 0 rows for phase %s\n' "$ONLY_PHASE"; fi
|
|
347
|
+
exit 0
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
printf 'plan[%d]{phase,name,step,state,why}:\n' "$shown"
|
|
351
|
+
for i in "${!P_STEP[@]}"; do
|
|
352
|
+
[ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
|
|
353
|
+
[ "$ONLY_BLOCKED" = 1 ] && [ "${P_STATE[$i]}" != BLOCKED ] && continue
|
|
354
|
+
printf ' "%s","%s","%s","%s","%s"\n' "${P_N[$i]}" "${P_NAME[$i]}" "${P_STEP[$i]}" "${P_STATE[$i]}" "${P_WHY[$i]}"
|
|
355
|
+
done
|
|
356
|
+
exit 0
|
package/bin/ymir-say.sh
CHANGED
|
@@ -24,7 +24,20 @@ set -u
|
|
|
24
24
|
VERSION="1.0.0"
|
|
25
25
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
26
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
# The roots that live OUTSIDE the code tree: this machine's records and the
|
|
29
|
+
# runtime state belong to the home the operator chose at installation, never in
|
|
30
|
+
# the tree — a packaged install replaces its tree on upgrade (Rule 04).
|
|
31
|
+
if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
|
|
32
|
+
_yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
33
|
+
for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
|
|
34
|
+
[ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
|
|
35
|
+
done
|
|
36
|
+
unset _yr _yc
|
|
37
|
+
fi
|
|
38
|
+
hoard_state_dir YMIR_STATE_DIR
|
|
39
|
+
hoard_data_dir YMIR_DATA_DIR
|
|
40
|
+
STATE_DIR="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
|
|
28
41
|
LOG="$STATE_DIR/ymir-said.log"
|
|
29
42
|
|
|
30
43
|
case "${1-}" in
|
package/bin/ymir-setup-auth.sh
CHANGED
|
@@ -18,7 +18,18 @@ set -u
|
|
|
18
18
|
VERSION="1.0.0"
|
|
19
19
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
20
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
# The operator's credentials are theirs and live in the HOME they chose — never
|
|
23
|
+
# in the code tree, which a packaged install replaces on upgrade (Rule 04).
|
|
24
|
+
if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
|
|
25
|
+
_yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
|
|
27
|
+
[ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
|
|
28
|
+
done
|
|
29
|
+
unset _yr _yc
|
|
30
|
+
fi
|
|
31
|
+
hoard_local_env _YMIR_ENV_DEFAULT
|
|
32
|
+
ENV_FILE="${YMIR_ENV_FILE:-$_YMIR_ENV_DEFAULT}"
|
|
22
33
|
|
|
23
34
|
usage() { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; }
|
|
24
35
|
die() { printf 'error: %s\n' "$1" >&2; exit 1; }
|
package/bin/ymir-validate.sh
CHANGED
|
@@ -99,21 +99,35 @@ else
|
|
|
99
99
|
fi
|
|
100
100
|
|
|
101
101
|
# ── 7. smidja db (visualizer readiness) ─────────────────────────────────────
|
|
102
|
+
# The same pair scripts/start.sh and bin/smidja-bootstrap.sh resolve: an existing
|
|
103
|
+
# $YMIR_HOME/smidja/smidja.db first, then an in-repo copy.
|
|
102
104
|
YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
|
|
103
|
-
SMIDJA_DB_PATH="${
|
|
104
|
-
|
|
105
|
+
SMIDJA_DB_PATH="${SMIDJA_DB:-}"
|
|
106
|
+
if [ -z "$SMIDJA_DB_PATH" ]; then
|
|
107
|
+
for c in "$YMIR_HOME/smidja/smidja.db" "$ROOT/apps/smidja/smidja_data/smidja.db"; do
|
|
108
|
+
[ -f "$c" ] && { SMIDJA_DB_PATH="$c"; break; }
|
|
109
|
+
done
|
|
110
|
+
SMIDJA_DB_PATH="${SMIDJA_DB_PATH:-$YMIR_HOME/smidja/smidja.db}"
|
|
111
|
+
fi
|
|
105
112
|
if [ -f "$SMIDJA_DB_PATH" ]; then
|
|
106
113
|
add smidja-db PASS "smidja.db present"
|
|
107
114
|
else
|
|
108
115
|
add smidja-db FAIL "smidja.db missing — run bin/smidja-bootstrap.sh"
|
|
109
116
|
fi
|
|
110
117
|
|
|
111
|
-
# ── 7b. visualizer
|
|
118
|
+
# ── 7b. visualizer (built ./dist AND its API actually listening) ────────────
|
|
119
|
+
# A PASS must mean the thing is UP. Probe the port, not just the build: a
|
|
120
|
+
# built-but-dead visualizer (a bad CMD_DB, a crashed API) is a FAIL, not green.
|
|
112
121
|
VIZ="$ROOT/.agents/skills/smidja-factory/apps/visualizer"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
VIZ_PORT="${SMIDJA_VIZ_API_PORT:-8437}"
|
|
123
|
+
if [ ! -d "$VIZ" ]; then
|
|
124
|
+
add visualizer SKIP "no visualizer tree at $VIZ"
|
|
125
|
+
elif [ ! -d "$VIZ/dist" ]; then
|
|
116
126
|
add visualizer FAIL "UI not built (./dist missing) — (cd $VIZ && bun run build)"
|
|
127
|
+
elif port_open "$VIZ_PORT"; then
|
|
128
|
+
add visualizer PASS "UI built and served on :$VIZ_PORT"
|
|
129
|
+
else
|
|
130
|
+
add visualizer FAIL "UI built but no API listening on :$VIZ_PORT — run scripts/start.sh"
|
|
117
131
|
fi
|
|
118
132
|
|
|
119
133
|
# ── 8. desktop apps (both Electron windows) ─────────────────────────────────
|
|
@@ -133,8 +147,14 @@ if port_open 4602; then add memory PASS "Mimir bridge up on :4602"
|
|
|
133
147
|
else add memory WARN "well off (:4602) — needs the engram engine; platform runs without it"; fi
|
|
134
148
|
|
|
135
149
|
# ── 10. audit ledger intact ─────────────────────────────────────────────────
|
|
136
|
-
RUNES_LEDGER="
|
|
137
|
-
|
|
150
|
+
RUNES_LEDGER=""
|
|
151
|
+
# The ledger follows the hoard (Rule 06): 0004-hoard-and-realms moved it under
|
|
152
|
+
# hodd/memory/. Try the hoard first, then the pre-move locations.
|
|
153
|
+
for c in "${YMIR_HOME:+${YMIR_HOARD:-$YMIR_HOME/hodd}/memory/runes_audit.md}" \
|
|
154
|
+
"${YMIR_HOME:+$YMIR_HOME/memory/runes_audit.md}" \
|
|
155
|
+
"$ROOT/workspace/memory/runes_audit.md"; do
|
|
156
|
+
[ -n "$c" ] && [ -r "$c" ] && { RUNES_LEDGER="$c"; break; }
|
|
157
|
+
done
|
|
138
158
|
if [ -r "$RUNES_LEDGER" ]; then
|
|
139
159
|
add runes PASS "audit ledger readable ($(wc -l <"$RUNES_LEDGER" | tr -d ' ') lines)"
|
|
140
160
|
else
|
package/docs/lore.md
CHANGED
|
@@ -818,9 +818,13 @@ what belongs to all houses, nothing to one.
|
|
|
818
818
|
| Path | What it holds |
|
|
819
819
|
|------|---------------|
|
|
820
820
|
| `midgard/design-system/` | Theme tokens, icons, rune glyphs, component rules |
|
|
821
|
-
| `midgard/company_wiki/` | Cross-tenant company knowledge |
|
|
822
821
|
| `midgard/github_org_repos/` | GitHub organization repo inventory |
|
|
823
822
|
| `midgard/shared-packages/` | Shared npm/Python packages |
|
|
823
|
+
|
|
824
|
+
Midgard holds **public-by-design** cross-tenant assets only. Company knowledge —
|
|
825
|
+
policies, vision, specs, client names — is **tenant data** and lives at
|
|
826
|
+
`$YMIR_HOME/hodd/identity/companies/`. A company wiki here would publish that
|
|
827
|
+
company's internals in a public repo.
|
|
824
828
|
| `midgard/infrastructure/` | DB schemas, Caddyfile, cloudflared, oauth2-proxy |
|
|
825
829
|
|
|
826
830
|
**Rule:** Midgard is shared. No realm may mutate another realm's Midgard assets.
|
package/docs/runbooks/agents.md
CHANGED
|
@@ -6,7 +6,7 @@ symlinked into each harness.
|
|
|
6
6
|
## Where agents live
|
|
7
7
|
|
|
8
8
|
- **Canonical:** `.agents/agents/<name>-<craft>.md` (edit these only).
|
|
9
|
-
- **Bindings:** `.opencode/
|
|
9
|
+
- **Bindings:** `.opencode/agents/*.md` and `.pi/agents/*.md` are symlinks.
|
|
10
10
|
|
|
11
11
|
Rebind after any change:
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerwiz/ymir",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"bin/",
|
|
30
30
|
".agents/",
|
|
31
31
|
"scripts/",
|
|
32
|
-
"config/",
|
|
33
32
|
"RULES/",
|
|
34
33
|
"docs/",
|
|
35
34
|
"packaging/",
|
|
@@ -38,7 +37,12 @@
|
|
|
38
37
|
"CHANGELOG.md",
|
|
39
38
|
"LICENSE",
|
|
40
39
|
"NOTICE",
|
|
41
|
-
"!**/node_modules/**"
|
|
40
|
+
"!**/node_modules/**",
|
|
41
|
+
"!.agents/memory/kaia.engram*",
|
|
42
|
+
"!.agents/memory/well/*.jsonl",
|
|
43
|
+
"!.agents/memory/*.db",
|
|
44
|
+
"!**/__pycache__/",
|
|
45
|
+
"!**/*.pyc"
|
|
42
46
|
],
|
|
43
47
|
"engines": {
|
|
44
48
|
"node": ">=20"
|
|
@@ -53,5 +57,11 @@
|
|
|
53
57
|
"scripts": {
|
|
54
58
|
"test": "bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh",
|
|
55
59
|
"compliance": "bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh"
|
|
60
|
+
},
|
|
61
|
+
"optionalDependencies": {
|
|
62
|
+
"@zerwiz/hlidskjalf": "^0.1.0",
|
|
63
|
+
"@zerwiz/odrerir": "^0.1.0",
|
|
64
|
+
"@zerwiz/sessrumnir": "^0.1.8-alpha",
|
|
65
|
+
"@zerwiz/smidja-factory": "^0.1.0"
|
|
56
66
|
}
|
|
57
67
|
}
|
package/scripts/electron.sh
CHANGED
|
@@ -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
|
+
|
|
30
|
+
# The roots that live OUTSIDE the code tree: this machine's records and the
|
|
31
|
+
# runtime state belong to the home the operator chose at installation, never in
|
|
32
|
+
# the tree — a packaged install replaces its tree on upgrade (Rule 04).
|
|
33
|
+
if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
|
|
34
|
+
_yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
|
|
36
|
+
[ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
|
|
37
|
+
done
|
|
38
|
+
unset _yr _yc
|
|
39
|
+
fi
|
|
40
|
+
hoard_state_dir YMIR_STATE_DIR
|
|
41
|
+
hoard_data_dir YMIR_DATA_DIR
|
|
29
42
|
APP="$ROOT/apps/hlidskjalf"
|
|
30
43
|
Odrerir_app="$ROOT/apps/odrerir"
|
|
31
44
|
NO_INSTALL=0
|
|
@@ -85,6 +98,23 @@ pid_of() {
|
|
|
85
98
|
printf '%s' "$(printf '%s\n' "$pids" | head -1)"
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
stop_view() { # stop ONE view's pids - and only that view's
|
|
102
|
+
local v="$1" pids
|
|
103
|
+
pids="$(view_pids "$v")"
|
|
104
|
+
if [ -z "$pids" ]; then
|
|
105
|
+
printf 'electron: %s already stopped\n' "$v"
|
|
106
|
+
rm -f "$(pid_file "$v")"
|
|
107
|
+
return 0
|
|
108
|
+
fi
|
|
109
|
+
local list; list="$(printf '%s' "$pids" | tr '\n' ',')"
|
|
110
|
+
printf '%s\n' "$pids" | xargs -r kill 2>/dev/null || true
|
|
111
|
+
sleep 0.5
|
|
112
|
+
local left; left="$(view_pids "$v")"
|
|
113
|
+
[ -z "$left" ] || printf '%s\n' "$left" | xargs -r kill -KILL 2>/dev/null || true
|
|
114
|
+
rm -f "$(pid_file "$v")"
|
|
115
|
+
printf 'electron: stopped %s pid(s)=%s\n' "$v" "${list%,}"
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
case "$ACTION" in
|
|
89
119
|
status)
|
|
90
120
|
printf 'electron[%s]{view,state,pid}:\n' "${#VIEWS[@]}"
|
|
@@ -93,15 +123,13 @@ case "$ACTION" in
|
|
|
93
123
|
done
|
|
94
124
|
exit 0 ;;
|
|
95
125
|
stop)
|
|
96
|
-
for
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
rm -f "$(pid_file "$v")"
|
|
104
|
-
done
|
|
126
|
+
# ONLY the view asked for. Walking every view here is why `stop --view odrerir`
|
|
127
|
+
# used to take the smithy's window down with it.
|
|
128
|
+
if [ "$VIEW" = both ]; then
|
|
129
|
+
for v in "${VIEWS[@]}"; do stop_view "$v"; done
|
|
130
|
+
else
|
|
131
|
+
stop_view "$VIEW"
|
|
132
|
+
fi
|
|
105
133
|
printf 'electron: stopped\n'
|
|
106
134
|
exit 0 ;;
|
|
107
135
|
start|hlidskjalf|smidja|odrerir) [ "$ACTION" = smidja ] && VIEW=smidja; [ "$ACTION" = odrerir ] && VIEW=odrerir ;;
|
|
@@ -109,7 +137,7 @@ case "$ACTION" in
|
|
|
109
137
|
esac
|
|
110
138
|
|
|
111
139
|
command -v npm >/dev/null 2>&1 || { printf 'error: npm not found\nhelp: install node/npm to run the desktop shell\n' >&2; exit 1; }
|
|
112
|
-
mkdir -p "$
|
|
140
|
+
mkdir -p "$YMIR_STATE_DIR"
|
|
113
141
|
|
|
114
142
|
if [ ! -x "$APP/node_modules/.bin/electron" ]; then
|
|
115
143
|
if [ "$NO_INSTALL" = 1 ]; then
|
|
@@ -151,14 +179,39 @@ fi
|
|
|
151
179
|
app_dir() { case "$1" in odrerir) printf '%s' "$Odrerir_app" ;; *) printf '%s' "$APP" ;; esac; }
|
|
152
180
|
view_host() { case "$1" in smidja) printf '%s' 'http://127.0.0.1:8437/' ;; odrerir) printf '%s' 'http://127.0.0.1:4322/' ;; *) printf '%s' 'http://127.0.0.1:3888/' ;; esac; }
|
|
153
181
|
|
|
182
|
+
service_up() { curl -s -o /dev/null --max-time 3 "$1" 2>/dev/null; }
|
|
183
|
+
wait_for_service() {
|
|
184
|
+
local u="$1" i
|
|
185
|
+
for i in $(seq 1 120); do service_up "$u" && return 0; sleep 0.5; done
|
|
186
|
+
return 1
|
|
187
|
+
}
|
|
188
|
+
# Does the compositor hold a window of this class at all? A process with no window
|
|
189
|
+
# cannot be focused, and focusing nothing is exactly the silence a click must not get.
|
|
190
|
+
window_present() {
|
|
191
|
+
command -v hyprctl >/dev/null 2>&1 || return 0 # no compositor to ask: assume yes
|
|
192
|
+
hyprctl clients 2>/dev/null | grep -qi "$1"
|
|
193
|
+
}
|
|
194
|
+
|
|
154
195
|
start_one() {
|
|
155
196
|
local v="$1" f l
|
|
156
197
|
f="$(pid_file "$v")"; l="$(log_file "$v")"
|
|
198
|
+
# THE SYSTEM FIRST. A shell whose backing service is down draws an empty window,
|
|
199
|
+
# and a window that never mapped cannot be focused. Clicking an icon must raise
|
|
200
|
+
# THE SYSTEM, so the service is converged before the window is judged.
|
|
201
|
+
local host; host="$(view_host "$v")"
|
|
202
|
+
if ! service_up "$host"; then
|
|
203
|
+
printf 'electron: %s has no service at %s — raising the system\n' "$v" "$host" >&2
|
|
204
|
+
( nohup bash "$ROOT/scripts/start.sh" >/dev/null 2>&1 </dev/null & )
|
|
205
|
+
if ! wait_for_service "$host"; then
|
|
206
|
+
printf 'electron: %s still does not answer at %s — see $YMIR_STATE_DIR/\n' "$v" "$host" >&2
|
|
207
|
+
fi
|
|
208
|
+
# A process that lived through the outage is windowless; it must be reborn.
|
|
209
|
+
if is_running "$v"; then stop_view "$v"; fi
|
|
210
|
+
fi
|
|
157
211
|
if is_running "$v"; then
|
|
158
|
-
# A
|
|
212
|
+
# A bound key must RAISE the app, never report that it is already up. The
|
|
159
213
|
# window lives on its own numbered desktop by design, so "nothing happens"
|
|
160
|
-
# is exactly what a
|
|
161
|
-
# desktop when the window is elsewhere.
|
|
214
|
+
# is exactly what a click must not do. focuswindow also switches desktops.
|
|
162
215
|
local cls state=already-up
|
|
163
216
|
cls="$(view_mark "$v")" # the window class, from the one place that defines it
|
|
164
217
|
if command -v hyprctl >/dev/null 2>&1; then
|
|
@@ -167,8 +220,14 @@ start_one() {
|
|
|
167
220
|
# string form is a syntax error there. This is the working idiom.
|
|
168
221
|
hyprctl dispatch "hl.dsp.focus({window=\"class:^${cls}\$\"})" >/dev/null 2>&1 && state=raised
|
|
169
222
|
fi
|
|
170
|
-
|
|
171
|
-
|
|
223
|
+
# A process whose window is gone cannot be focused — reborn, not silent.
|
|
224
|
+
if [ "$state" = already-up ] && ! window_present "$cls"; then
|
|
225
|
+
printf 'electron: %s is running without a window — restarting it\n' "$v" >&2
|
|
226
|
+
stop_view "$v"
|
|
227
|
+
else
|
|
228
|
+
printf 'electron[1]{view,state,pid}:\n "%s","%s",%s\n' "$v" "$state" "$(pid_of "$v")"
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
172
231
|
fi
|
|
173
232
|
# Launch the REAL Electron binary, not the .bin node shim, so the recorded pid
|
|
174
233
|
# is the app itself (the shim spawns and would leave a stale/incorrect pid).
|