@techgoblin/gobstack 0.4.4-beta.4 → 0.4.4-beta.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/README.md +29 -26
- package/bin/goblin +19 -18
- package/bin/goblin-doctor +3 -3
- package/bin/goblin-emit +3 -3
- package/bin/goblin-init +139 -95
- package/bin/goblin-install +1 -1
- package/bin/goblin-upgrade +5 -5
- package/bin/goblin-verify +1 -1
- package/bin/goblin.js +22 -22
- package/docs/GUIDE.md +26 -25
- package/docs/w6-tty-redraw-pty-transcript.txt +334 -0
- package/package.json +1 -1
package/bin/goblin-init
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# goblin-init — the W6 first-run wizard (`gob init`).
|
|
3
3
|
#
|
|
4
|
-
#
|
|
4
|
+
# gob init [--target <dir>] [--class <app|service|game|research|agent|desktop|A..F>]
|
|
5
5
|
# [--branch <name>] [--email <addr>] [--gate <cmd>]
|
|
6
6
|
# [--emit <p[,p..]>] [--scope project|global] [--yes] [--dry-run]
|
|
7
7
|
#
|
|
8
|
-
# One screen per question, answered steps collapsing into the ✔/◆/○ rail above.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# One screen per question, answered steps collapsing into the ✔/◆/○ rail above. On a tty
|
|
9
|
+
# the whole wizard frame (logotype, header, answered rows, rail) is redrawn in place from
|
|
10
|
+
# a saved buffer of the wizard's own lines — never by counting printed output, which the
|
|
11
|
+
# installer/emit engines would break. Every question has a flag: with the flags set (or
|
|
12
|
+
# --yes, or a non-tty stdin) the wizard asks nothing and runs from the flags and the
|
|
13
|
+
# defaults — non-interactive parity is structural, not a mode. Over a pipe the redraw
|
|
14
|
+
# degrades to a numbered cascade (rustup shape) with zero cursor motion.
|
|
12
15
|
#
|
|
13
16
|
# The wizard is a FRONT END over the existing engines, never a second installer:
|
|
14
17
|
# .goblin/ + the payload -> bin/goblin-install (its refusal contract — an existing
|
|
@@ -47,9 +50,11 @@ VERSION=$(cat "$SRC/VERSION" 2>/dev/null || printf 'unknown')
|
|
|
47
50
|
# nearest 16-colour slot. NO_COLOR or a piped stdout turns colour off entirely.
|
|
48
51
|
TTY_OUT=0; [ -t 1 ] && TTY_OUT=1
|
|
49
52
|
TTY_IN=0; [ -t 0 ] && TTY_IN=1
|
|
50
|
-
C_ACCENT="" C_GREEN="" C_TEXT="" C_MUTED="" C_DIM="" C_RED="" C_YELLOW="" C_RESET=""
|
|
53
|
+
C_ACCENT="" C_GREEN="" C_TEXT="" C_MUTED="" C_DIM="" C_RED="" C_YELLOW="" C_RESET=""
|
|
54
|
+
# Single-accent discipline: one accent (#F54E00) + green + text + muted + dim. No bold
|
|
55
|
+
# is ever emitted — bold was a second emphasis voice and is gone from every screen.
|
|
51
56
|
if [ "$TTY_OUT" -eq 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
52
|
-
C_RESET=$'\033[0m'
|
|
57
|
+
C_RESET=$'\033[0m'
|
|
53
58
|
case "${COLORTERM:-}" in
|
|
54
59
|
truecolor|24bit)
|
|
55
60
|
C_ACCENT=$'\033[38;2;245;78;0m'; C_GREEN=$'\033[38;2;74;222;128m'
|
|
@@ -64,41 +69,64 @@ if [ "$TTY_OUT" -eq 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
|
64
69
|
fi
|
|
65
70
|
|
|
66
71
|
# ---------------------------------------------------------------- the rail ----
|
|
67
|
-
# 6 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty
|
|
68
|
-
#
|
|
72
|
+
# 6 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty from a SAVED
|
|
73
|
+
# BUFFER of the wizard's own lines (RAIL_BUF) — never by counting the output between
|
|
74
|
+
# draws, because the installer/emit engines print freely and break naive accounting.
|
|
75
|
+
# Over a pipe nothing is saved and nothing rewinds: each screen is one numbered line.
|
|
69
76
|
STEP_LABELS=("platforms" "class" "branch/email" "first gate" "emit" "done")
|
|
70
77
|
STEP_VAL=("" "" "" "" "" "")
|
|
71
|
-
RAIL_LINES=0
|
|
72
|
-
Q_LINES=0
|
|
73
78
|
CURRENT=1
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
# rail_say: one line of wizard output on a tty — stored in the redraw buffer AND
|
|
81
|
+
# printed with a VT100 clear-line prefix (the redraw may overwrite a longer old line).
|
|
82
|
+
# Over a pipe this is exactly printf '%s\n' and nothing is stored.
|
|
83
|
+
rail_say() {
|
|
84
|
+
if [ "$TTY_OUT" -eq 1 ]; then
|
|
85
|
+
RAIL_BUF+=("$*")
|
|
86
|
+
printf '\033[2K%s\n' "$*"
|
|
87
|
+
else
|
|
88
|
+
printf '%s\n' "$*"
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
# rail_seed: the pre-draw frame (logo + header). On a tty it only FILLS the buffer —
|
|
92
|
+
# rail_draw prints it; over a pipe nothing is buffered and it prints directly.
|
|
93
|
+
rail_seed() {
|
|
94
|
+
if [ "$TTY_OUT" -eq 1 ]; then
|
|
95
|
+
RAIL_BUF+=("$*")
|
|
96
|
+
else
|
|
97
|
+
printf '%s\n' "$*"
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
# rail_note: a line that prints now but never joins the redraw buffer (engine output,
|
|
101
|
+
# transcripts, the verify echo): the next redraw wipes it with its clear-below.
|
|
102
|
+
rail_note() {
|
|
76
103
|
printf '%s\n' "$*"
|
|
77
|
-
Q_LINES=$((Q_LINES + 1))
|
|
78
104
|
}
|
|
79
105
|
rail_rewind() {
|
|
80
|
-
if [ "$TTY_OUT" -eq 1 ]
|
|
81
|
-
printf '\033[%dA\033[J' "$
|
|
106
|
+
if [ "$TTY_OUT" -eq 1 ]; then
|
|
107
|
+
printf '\033[%dA\033[J' "${#RAIL_BUF[@]}"
|
|
82
108
|
fi
|
|
83
|
-
RAIL_LINES=0; Q_LINES=0
|
|
84
109
|
}
|
|
85
|
-
|
|
86
|
-
local i glyph col label
|
|
110
|
+
rail_draw() { # reprint the saved buffer, then the live rail row for $CURRENT
|
|
111
|
+
local i line glyph col label
|
|
112
|
+
if [ "$TTY_OUT" -eq 0 ]; then return 0; fi
|
|
113
|
+
for line in "${RAIL_BUF[@]}"; do
|
|
114
|
+
printf '\033[2K%s\n' "$line"
|
|
115
|
+
done
|
|
87
116
|
for i in 1 2 3 4 5 6; do
|
|
88
117
|
label="${STEP_LABELS[$((i - 1))]}"
|
|
89
118
|
if [ "$i" -lt "$CURRENT" ]; then
|
|
90
|
-
|
|
119
|
+
glyph="✔"; col="$C_GREEN"
|
|
91
120
|
elif [ "$i" -eq "$CURRENT" ]; then
|
|
92
|
-
|
|
93
|
-
RAIL_LINES=$((RAIL_LINES + 1))
|
|
94
|
-
return 0 # the asking row is printed by the screen itself, after its question
|
|
121
|
+
glyph="◆"; col="$C_ACCENT"
|
|
95
122
|
else
|
|
96
|
-
|
|
123
|
+
glyph="○"; col="$C_DIM"
|
|
97
124
|
fi
|
|
98
|
-
|
|
125
|
+
printf '\033[2K %s%s %d. %-13s%s%s%s\n' "${col}${glyph}" "$C_RESET" "$i" "$label" "$C_TEXT" "${STEP_VAL[$((i - 1))]}" "$C_RESET"
|
|
99
126
|
done
|
|
100
127
|
}
|
|
101
|
-
|
|
128
|
+
rail_reset() { RAIL_BUF=(); }
|
|
129
|
+
cascade() { # the pipe-degraded shape: one numbered line per screen (zero cursor motion)
|
|
102
130
|
printf 'gob init [%d/6] %s\n' "$CURRENT" "$1"
|
|
103
131
|
}
|
|
104
132
|
|
|
@@ -108,7 +136,7 @@ ask_value() {
|
|
|
108
136
|
if [ "$TTY_IN" -eq 0 ]; then REPLY="$dflt"; return 0; fi
|
|
109
137
|
printf ' %s%s%s %s─ %s' "$C_TEXT" "$label" "$C_RESET" "$C_DIM" "$dflt"
|
|
110
138
|
read -r REPLY
|
|
111
|
-
|
|
139
|
+
rail_say ""
|
|
112
140
|
[ -n "$REPLY" ] || REPLY="$dflt"
|
|
113
141
|
return 0
|
|
114
142
|
}
|
|
@@ -137,7 +165,7 @@ done
|
|
|
137
165
|
for dep in git awk sed grep; do
|
|
138
166
|
command -v "$dep" >/dev/null 2>&1 || { g_err "missing dependency: $dep"; exit 2; }
|
|
139
167
|
done
|
|
140
|
-
[ -f "$SRC/VERSION" ] || { g_err "no VERSION at $SRC — is this a
|
|
168
|
+
[ -f "$SRC/VERSION" ] || { g_err "no VERSION at $SRC — is this a gobstack checkout?"; exit 2; }
|
|
141
169
|
[ -n "$SCOPE" ] || SCOPE="project"
|
|
142
170
|
case "$SCOPE" in project|global) ;; *) g_err "--scope must be project or global, got '$SCOPE'"; exit 2 ;; esac
|
|
143
171
|
TARGET="${TARGET:-$PWD}"
|
|
@@ -146,17 +174,25 @@ TARGET=$(g_expand_tilde "$TARGET")
|
|
|
146
174
|
TARGET=$(cd "$TARGET" && pwd)
|
|
147
175
|
|
|
148
176
|
# ------------------------------------------------------------ screen 1: welcome
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
printf
|
|
152
|
-
printf '%s\n' "${C_DIM} discipline around whatever executes${C_RESET}"
|
|
153
|
-
printf '\n'
|
|
154
|
-
printf '%s\n' "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (goblin-stack v$VERSION)${C_RESET}"
|
|
155
|
-
printf '%s\n' " ${C_MUTED}6 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
|
|
156
|
-
printf '\n'
|
|
177
|
+
# The logotype and the header ride the redraw buffer (rail_say): every later screen
|
|
178
|
+
# redraws logo + header + rail as one stable frame. Over a pipe rail_say is plain
|
|
179
|
+
# printf and nothing is buffered — the cascade keeps zero cursor motion.
|
|
157
180
|
CURRENT=1
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
rail_reset
|
|
182
|
+
rail_seed "${C_ACCENT}▄███▄ ▄██▄ ██▄ █ ▄█▄ █ █ ▄██▀ █████ ▄███▄ ▄███ █ █${C_RESET}"
|
|
183
|
+
rail_seed "${C_ACCENT}█ █ █ █ █▄▀█ █ █ █▄ █ ▀██▄ █ █ █ █ █ █${C_RESET}"
|
|
184
|
+
rail_seed "${C_ACCENT}█ ▄██ █ █ █▄▀█ █ █ █ ▄█ ▄█▀ █ █████ █ ██${C_RESET}"
|
|
185
|
+
rail_seed "${C_ACCENT}▀██▀▀ ▀██▀ ██▀ ▀▀▀▀ ▀█▀ █ █ ██▀▀ ▀ █ █ ▀██▀ █ █${C_RESET}"
|
|
186
|
+
rail_seed " ${C_MUTED}discipline around whatever executes${C_RESET}"
|
|
187
|
+
rail_seed ""
|
|
188
|
+
rail_seed "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (gobstack v$VERSION)${C_RESET}"
|
|
189
|
+
rail_seed " ${C_MUTED}6 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
|
|
190
|
+
rail_seed ""
|
|
191
|
+
if [ "$TTY_OUT" -eq 1 ]; then
|
|
192
|
+
rail_draw
|
|
193
|
+
else
|
|
194
|
+
cascade "welcome: gobstack v$VERSION — the guided first run"
|
|
195
|
+
fi
|
|
160
196
|
|
|
161
197
|
# Platform enum assembled from fragments (the MD_SLUG precedent): a literal tool name in
|
|
162
198
|
# bin/ trips MD-01's model-name pattern, and run-tests.sh greps this directory.
|
|
@@ -169,7 +205,7 @@ PLATFORMS="$PLA hermes copilot cursor opencode codex $GEM"
|
|
|
169
205
|
# ---------------------------------------------------------- screen 2: detect --
|
|
170
206
|
CURRENT=2
|
|
171
207
|
rail_rewind
|
|
172
|
-
if [ "$TTY_OUT" -eq 1 ]; then
|
|
208
|
+
if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
|
|
173
209
|
DET_ROWS=""
|
|
174
210
|
DET_N=0
|
|
175
211
|
for p in $PLATFORMS; do
|
|
@@ -193,19 +229,17 @@ for p in $PLATFORMS; do
|
|
|
193
229
|
DET_N=$((DET_N + 1))
|
|
194
230
|
fi
|
|
195
231
|
done
|
|
196
|
-
STEP_VAL[0]="
|
|
232
|
+
STEP_VAL[0]="$C_DIM$DET_N detected$C_RESET"
|
|
197
233
|
if [ "$TTY_OUT" -eq 1 ]; then
|
|
198
|
-
|
|
199
|
-
|
|
234
|
+
rail_say ""
|
|
235
|
+
rail_say " ✔ 1. platforms $C_TEXT$DET_N detected$C_RESET"
|
|
200
236
|
if [ "$DET_N" -eq 0 ]; then
|
|
201
|
-
|
|
202
|
-
RAIL_LINES=$((RAIL_LINES + 1))
|
|
237
|
+
rail_say " ${C_YELLOW}no platform detected here — the wizard will still install the harness; emit later with gob emit$C_RESET"
|
|
203
238
|
else
|
|
204
239
|
while IFS=$'\t' read -r p proj glob hit; do
|
|
205
240
|
[ -n "$p" ] || continue
|
|
206
241
|
case "$glob" in "~"*) glob="${glob/\~/$HOME}" ;; esac
|
|
207
|
-
|
|
208
|
-
RAIL_LINES=$((RAIL_LINES + 1))
|
|
242
|
+
rail_say " $C_GREEN✔$C_RESET $C_TEXT$p$C_RESET $glob $C_DIM($hit)$C_RESET"
|
|
209
243
|
done <<< "$DET_ROWS"
|
|
210
244
|
fi
|
|
211
245
|
else
|
|
@@ -215,23 +249,23 @@ fi
|
|
|
215
249
|
# ------------------------------------------------------------ screen 3: class -
|
|
216
250
|
CURRENT=3
|
|
217
251
|
rail_rewind
|
|
218
|
-
if [ "$TTY_OUT" -eq 1 ]; then
|
|
252
|
+
if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
|
|
219
253
|
CLASS_DEFAULT=1
|
|
220
254
|
if [ -z "$CLASS" ]; then
|
|
221
255
|
if [ "$TTY_IN" -eq 1 ]; then
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
rail_say ""
|
|
257
|
+
rail_say " ${C_MUTED}what kind of work does this repo do?$C_RESET"
|
|
258
|
+
rail_say " $C_TEXT 1 ▸ app shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
|
|
259
|
+
rail_say " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
|
|
260
|
+
rail_say " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
|
|
261
|
+
rail_say " $C_TEXT 4 research specs, replays, reference corpora ${C_DIM}(class D)$C_RESET"
|
|
262
|
+
rail_say " $C_TEXT 5 agent fleets, loops, unattended automation ${C_DIM}(class E)$C_RESET"
|
|
263
|
+
rail_say " $C_TEXT 6 desktop the shell you live in, perf budget ${C_DIM}(class F)$C_RESET"
|
|
264
|
+
rail_say " ${C_MUTED}↑ number + Enter · Enter = app · the class letter works too (A-F)$C_RESET"
|
|
231
265
|
printf ' '
|
|
232
266
|
read -r REPLY_CLASS
|
|
233
267
|
ASKED_ANY=1
|
|
234
|
-
|
|
268
|
+
rail_say ""
|
|
235
269
|
else
|
|
236
270
|
REPLY_CLASS="1"
|
|
237
271
|
cascade "class: app (default; --class app|service|game|research|agent|desktop or A-F)"
|
|
@@ -257,10 +291,9 @@ case "$CLASS" in
|
|
|
257
291
|
exit 2 ;;
|
|
258
292
|
esac
|
|
259
293
|
CLASS_NAME=$(ls "$SRC"/presets/"$CLASS"-*.yaml 2>/dev/null | head -n 1)
|
|
260
|
-
STEP_VAL[1]="
|
|
294
|
+
STEP_VAL[1]="$C_TEXT$CLASS ($([ -n "$CLASS_NAME" ] && basename "$CLASS_NAME" .yaml | cut -d- -f2-))$C_RESET"
|
|
261
295
|
if [ "$TTY_OUT" -eq 1 ]; then
|
|
262
|
-
|
|
263
|
-
RAIL_LINES=$((RAIL_LINES + 1))
|
|
296
|
+
rail_say " ✔ $((CURRENT - 1)). class $C_TEXT$CLASS$([ -n "$CLASS_NAME" ] && printf ' — %s' "$(basename "$CLASS_NAME" .yaml | cut -d- -f2-)")$C_RESET"
|
|
264
297
|
else
|
|
265
298
|
cascade "class: $CLASS"
|
|
266
299
|
fi
|
|
@@ -268,15 +301,15 @@ fi
|
|
|
268
301
|
# ----------------------------------------------------- screen 4: branch/email --
|
|
269
302
|
CURRENT=4
|
|
270
303
|
rail_rewind
|
|
271
|
-
if [ "$TTY_OUT" -eq 1 ]; then
|
|
304
|
+
if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
|
|
272
305
|
DEF_BRANCH=$( cd "$TARGET" && git rev-parse --abbrev-ref HEAD 2>/dev/null )
|
|
273
306
|
case "$DEF_BRANCH" in ""|HEAD) DEF_BRANCH=main ;; esac
|
|
274
307
|
DEF_EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
|
|
275
308
|
[ -n "$DEF_EMAIL" ] || DEF_EMAIL="you@example.com"
|
|
276
309
|
if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
|
|
277
310
|
if [ "$TTY_IN" -eq 1 ]; then
|
|
278
|
-
|
|
279
|
-
|
|
311
|
+
rail_say ""
|
|
312
|
+
rail_say " ${C_MUTED}the forge contract — the identity this repo commits with (CM-01)$C_RESET"
|
|
280
313
|
ask_value "default branch" "$DEF_BRANCH"; BRANCH="${BRANCH:-$REPLY}"
|
|
281
314
|
ask_value "owner email" "$DEF_EMAIL"; EMAIL="${EMAIL:-$REPLY}"
|
|
282
315
|
else
|
|
@@ -287,29 +320,27 @@ if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
|
|
|
287
320
|
fi
|
|
288
321
|
BRANCH="${BRANCH:-$DEF_BRANCH}"
|
|
289
322
|
EMAIL="${EMAIL:-$DEF_EMAIL}"
|
|
290
|
-
STEP_VAL[2]="
|
|
323
|
+
STEP_VAL[2]="$C_TEXT$BRANCH · $EMAIL$C_RESET"
|
|
291
324
|
if [ "$TTY_OUT" -eq 1 ]; then
|
|
292
|
-
|
|
293
|
-
RAIL_LINES=$((RAIL_LINES + 1))
|
|
325
|
+
rail_say " ✔ $((CURRENT - 1)). branch/email $C_TEXT$BRANCH · $EMAIL$C_RESET"
|
|
294
326
|
fi
|
|
295
327
|
|
|
296
328
|
# -------------------------------------------------------- screen 5: first gate -
|
|
297
329
|
CURRENT=5
|
|
298
330
|
rail_rewind
|
|
299
|
-
if [ "$TTY_OUT" -eq 1 ]; then
|
|
331
|
+
if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
|
|
300
332
|
GATE_DEFAULT="bash tests/run-tests.sh"
|
|
301
333
|
if [ -z "$GATE" ]; then
|
|
302
334
|
if [ "$TTY_IN" -eq 1 ]; then
|
|
303
|
-
|
|
304
|
-
|
|
335
|
+
rail_say ""
|
|
336
|
+
rail_say " ${C_MUTED}one real command that must stay green — suggested for a bash suite:$C_RESET"
|
|
305
337
|
if [ -f "$TARGET/tests/run-tests.sh" ]; then
|
|
306
|
-
|
|
338
|
+
rail_say " $C_DIM$GATE_DEFAULT$C_RESET"
|
|
307
339
|
ask_value "gate command" "$GATE_DEFAULT"; GATE="$REPLY"
|
|
308
340
|
else
|
|
309
341
|
ask_value "gate command" "true"; GATE="$REPLY"
|
|
310
342
|
if [ "$GATE" = "true" ]; then
|
|
311
|
-
|
|
312
|
-
Q_LINES=$((Q_LINES + 1))
|
|
343
|
+
rail_say " ${C_YELLOW}no tests/run-tests.sh found — a placeholder gate is allowed but it proves nothing; replace it in .goblin/goblin.yaml$C_RESET"
|
|
313
344
|
fi
|
|
314
345
|
fi
|
|
315
346
|
else
|
|
@@ -317,16 +348,15 @@ if [ -z "$GATE" ]; then
|
|
|
317
348
|
cascade "gate: $GATE (default)"
|
|
318
349
|
fi
|
|
319
350
|
fi
|
|
320
|
-
STEP_VAL[3]="
|
|
351
|
+
STEP_VAL[3]="$C_TEXT$GATE$C_RESET"
|
|
321
352
|
if [ "$TTY_OUT" -eq 1 ]; then
|
|
322
|
-
|
|
323
|
-
RAIL_LINES=$((RAIL_LINES + 1))
|
|
353
|
+
rail_say " ✔ $((CURRENT - 1)). first gate $C_TEXT$GATE$C_RESET"
|
|
324
354
|
fi
|
|
325
355
|
|
|
326
356
|
# --------------------------------------------------------- screen 6: the plan --
|
|
327
357
|
CURRENT=6
|
|
328
358
|
rail_rewind
|
|
329
|
-
if [ "$TTY_OUT" -eq 1 ]; then
|
|
359
|
+
if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
|
|
330
360
|
|
|
331
361
|
# The emit selection: pre-tick the DETECTED platforms (in doctor's enum order).
|
|
332
362
|
EMIT_SELECTED=""
|
|
@@ -354,19 +384,19 @@ fi
|
|
|
354
384
|
EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
|
|
355
385
|
|
|
356
386
|
if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
|
|
357
|
-
|
|
358
|
-
|
|
387
|
+
rail_say ""
|
|
388
|
+
rail_say " ${C_MUTED}emit now — the skills go into:$C_RESET"
|
|
359
389
|
for p in $PLATFORMS; do
|
|
360
390
|
hit=""
|
|
361
391
|
while IFS=$' ' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
|
|
362
392
|
[ -n "$hit" ] || continue
|
|
363
|
-
|
|
393
|
+
rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET"
|
|
364
394
|
done
|
|
365
|
-
|
|
395
|
+
rail_say " ${C_MUTED}Enter = these; or type platform names to override$C_RESET"
|
|
366
396
|
printf ' '
|
|
367
397
|
read -r REPLY_EMIT
|
|
368
398
|
ASKED_ANY=1
|
|
369
|
-
|
|
399
|
+
rail_say ""
|
|
370
400
|
if [ -n "$REPLY_EMIT" ]; then
|
|
371
401
|
EMIT_SELECTED=""
|
|
372
402
|
for w in $REPLY_EMIT; do
|
|
@@ -376,19 +406,21 @@ if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
|
|
|
376
406
|
done
|
|
377
407
|
fi
|
|
378
408
|
fi
|
|
379
|
-
STEP_VAL[4]="
|
|
409
|
+
STEP_VAL[4]="$C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
|
|
380
410
|
if [ "$TTY_OUT" -eq 1 ]; then
|
|
381
|
-
|
|
382
|
-
RAIL_LINES=$((RAIL_LINES + 1))
|
|
411
|
+
rail_say " ✔ $((CURRENT - 1)). emit $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
|
|
383
412
|
fi
|
|
384
413
|
|
|
385
414
|
# ---- the plan box: the wizard never writes until this has been on the screen ----
|
|
415
|
+
# Spacing: one blank line separates the rail from the box (the spacing pass); the box
|
|
416
|
+
# carries two-space inner padding on every row so the columns align across rows.
|
|
386
417
|
BOX=""
|
|
387
418
|
add() { BOX="$BOX$1
|
|
388
419
|
"; }
|
|
420
|
+
add ""
|
|
389
421
|
add " ${C_DIM}╭──────────────────────────────────────────────────────╮${C_RESET}"
|
|
390
422
|
add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}install${C_RESET} $C_DIM.goblin/ + the payload into$C_RESET $C_TEXT$TARGET$C_RESET"
|
|
391
|
-
add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}class${C_RESET} $C_TEXT$CLASS$C_RESET $C_DIM· branch $C_TEXT$BRANCH$C_RESET $C_DIM· email$C_RESET $C_TEXT$EMAIL$C_RESET"
|
|
423
|
+
add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}class${C_RESET} $C_TEXT$CLASS$C_RESET ${C_DIM}· branch $C_TEXT$BRANCH$C_RESET ${C_DIM}· email$C_RESET $C_TEXT$EMAIL$C_RESET"
|
|
392
424
|
add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}gate${C_RESET} $C_TEXT$GATE$C_RESET"
|
|
393
425
|
add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}emit${C_RESET} $C_TEXT$EMIT_N platform(s), scope $SCOPE$C_RESET$([ "$EMIT_N" -gt 0 ] && printf '%s' " $C_DIM($(printf '%s' "$EMIT_SELECTED" | awk '{$1=$1};1'))$C_RESET")"
|
|
394
426
|
add " ${C_DIM}╰──────────────────────────────────────────────────────╯${C_RESET}"
|
|
@@ -399,10 +431,11 @@ if [ "$DRYRUN" -eq 1 ]; then
|
|
|
399
431
|
fi
|
|
400
432
|
# A question the wizard genuinely asked (a screen that waited on read) buys the confirm;
|
|
401
433
|
# flag/non-tty runs already stated their answer as a flag — proceeding is the contract.
|
|
434
|
+
# Single-accent: the confirm prompt is muted + dim, never bold, never a second accent.
|
|
402
435
|
if [ "$YES" -eq 0 ] && [ "${ASKED_ANY:-0}" -eq 1 ]; then
|
|
403
436
|
printf '%s\n' "$BOX"
|
|
404
437
|
if [ "$TTY_IN" -eq 1 ]; then
|
|
405
|
-
printf ' %sproceed?%s %s[Enter = yes]%s ' "$
|
|
438
|
+
printf ' %sproceed?%s %s[Enter = yes]%s ' "$C_MUTED" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
406
439
|
read -r REPLY_GO
|
|
407
440
|
case "$REPLY_GO" in n*|N*) g_info "aborted — nothing was written"; exit 0 ;; esac
|
|
408
441
|
else
|
|
@@ -415,7 +448,14 @@ fi
|
|
|
415
448
|
# carry: the declared identity (branch/owner_email) and the first gate, patched into
|
|
416
449
|
# .goblin/goblin.yaml right after install renders it. The patch is fail-closed — the
|
|
417
450
|
# gate must read back through the engine's own parser or the run stops here.
|
|
418
|
-
|
|
451
|
+
# rail_note, not rail_say: engine output below is never redrawn over, and the wizard
|
|
452
|
+
# never rewinds past its own buffer by counting lines it did not print.
|
|
453
|
+
if [ "$TTY_OUT" -eq 1 ]; then
|
|
454
|
+
rail_note ""
|
|
455
|
+
rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS${C_RESET}"
|
|
456
|
+
else
|
|
457
|
+
printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
|
|
458
|
+
fi
|
|
419
459
|
bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --yes
|
|
420
460
|
EXIT_INSTALL=$?
|
|
421
461
|
if [ "$EXIT_INSTALL" -ne 0 ]; then
|
|
@@ -452,20 +492,24 @@ fi
|
|
|
452
492
|
|
|
453
493
|
EMITTED=0
|
|
454
494
|
for p in $EMIT_SELECTED; do
|
|
455
|
-
[ "$TTY_OUT" -eq 0 ]
|
|
495
|
+
if [ "$TTY_OUT" -eq 0 ]; then
|
|
496
|
+
printf 'gob init [run] goblin-emit --platform %s --scope %s\n' "$p" "$SCOPE"
|
|
497
|
+
else
|
|
498
|
+
rail_note " ${C_MUTED}running: goblin-emit --platform $p --scope $SCOPE${C_RESET}"
|
|
499
|
+
fi
|
|
456
500
|
bash "$SRC/bin/goblin-emit" --platform "$p" --scope "$SCOPE" --target "$TARGET"
|
|
457
501
|
EXIT_EMIT=$?
|
|
458
502
|
if [ "$EXIT_EMIT" -ne 0 ]; then
|
|
459
|
-
g_err "emit $p failed (exit $EXIT_EMIT) — run '
|
|
503
|
+
g_err "emit $p failed (exit $EXIT_EMIT) — run 'gob emit --platform $p --scope $SCOPE' to see the refusal again"
|
|
460
504
|
exit "$EXIT_EMIT"
|
|
461
505
|
fi
|
|
462
506
|
EMITTED=$((EMITTED + 1))
|
|
463
507
|
done
|
|
464
508
|
|
|
465
509
|
# --------------------------------------------------- screen 6b: done + verify --
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
510
|
+
# Spacing pass: exactly one blank line before each done-screen section; the engine's
|
|
511
|
+
# verify output and this screen's own summary stay visually separated.
|
|
512
|
+
if [ "$TTY_OUT" -eq 0 ]; then
|
|
469
513
|
printf 'gob init [verify] .goblin/bin/goblin-verify\n'
|
|
470
514
|
fi
|
|
471
515
|
if [ "$DRYRUN" -eq 0 ]; then
|
|
@@ -489,16 +533,16 @@ if [ "$DRYRUN" -eq 0 ]; then
|
|
|
489
533
|
done
|
|
490
534
|
printf '\n'
|
|
491
535
|
if [ "$VERIFY_RC" -eq 0 ]; then
|
|
492
|
-
printf ' %
|
|
536
|
+
printf ' %sgob verify: %s passed, %s failed, %s skipped · exit 0%s\n' "$C_GREEN" "$PASSED" "$FAILED" "$SKIPPED" "$C_RESET"
|
|
493
537
|
else
|
|
494
|
-
printf ' %
|
|
538
|
+
printf ' %sgob verify: %s passed, %s failed, %s skipped · exit %s%s\n' "$C_RED" "$PASSED" "$FAILED" "$SKIPPED" "$VERIFY_RC" "$C_RESET"
|
|
495
539
|
fi
|
|
496
|
-
printf '\n'
|
|
497
540
|
else
|
|
498
541
|
g_info "gob init [verify] exit $VERIFY_RC: $PASSED passed, $FAILED failed, $ADVISORY advisory, $SKIPPED skipped"
|
|
499
542
|
fi
|
|
500
543
|
|
|
501
|
-
printf '
|
|
544
|
+
printf '\n'
|
|
545
|
+
printf ' %snext:%s\n' "$C_MUTED" "$C_RESET"
|
|
502
546
|
printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
|
|
503
547
|
printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
|
|
504
548
|
printf ' 3. %snot emitted here: gob emit --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
|
package/bin/goblin-install
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# goblin-install — drop the harness into a target repo, idempotently.
|
|
3
3
|
#
|
|
4
|
-
# Usage:
|
|
4
|
+
# Usage: gob install --target <dir> [options] (script: bin/goblin-install)
|
|
5
5
|
# --target <dir> required; the repo root to install into
|
|
6
6
|
# --class A|B|C|D|E|F required unless --uninstall or --re-pin; anything else needs an explicit class
|
|
7
7
|
# --models <path> model mapping file (default: $GOBLIN_MODELS -> ~/projects/fleet-model.yaml)
|
package/bin/goblin-upgrade
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# goblin-upgrade — the 0.4.4 per-repo → global migration (W3
|
|
2
|
+
# goblin-upgrade — the 0.4.4 per-repo → global engine migration (W3).
|
|
3
3
|
#
|
|
4
|
-
#
|
|
4
|
+
# gob upgrade [--target <dir>] [--dry-run] [--yes] [--engine-dir <path>]
|
|
5
5
|
#
|
|
6
6
|
# What it does (the corrected 7-step sequence, W3-SPEC §2 — the record is rewritten
|
|
7
7
|
# BEFORE the deletion, and every verify runs against a committed tree, CM-03):
|
|
@@ -230,7 +230,7 @@ if [ "${RECORD_OK:-0}" -eq 1 ] && [ -z "${MODE:-}" ]; then
|
|
|
230
230
|
R8_MSG="upgrade: partial migration detected - the record says global but $PAYLOAD_N engine files are still here. fix: resume by removing them (the engine lives at ${BLOCK_DIR:-$ED}), or roll back: git revert <commit A>"
|
|
231
231
|
elif [ "$BLOCK_MODE" != "global" ] && [ "$RECORD_ENGINE_N" -lt 18 ]; then
|
|
232
232
|
MODE="refuse-R9" # hand-migration: entries PARTIALLY or fully dropped, no block
|
|
233
|
-
R9_MSG="upgrade: $REPO's record is missing its engine: block and carries only $RECORD_ENGINE_N of the 18 engine entries - dropped $((18 - RECORD_ENGINE_N)) by hand, a state
|
|
233
|
+
R9_MSG="upgrade: $REPO's record is missing its engine: block and carries only $RECORD_ENGINE_N of the 18 engine entries - dropped $((18 - RECORD_ENGINE_N)) by hand, a state gob upgrade did not produce. fix: restore the record (git checkout .goblin/installed.json) and re-run"
|
|
234
234
|
elif [ "$PAYLOAD_N" -eq 0 ]; then
|
|
235
235
|
MODE="refuse-R5" # fresh-shape record but no payload to lift
|
|
236
236
|
R5_MSG="upgrade: no engine at $ED and no vendored payload to lift it from - install the engine first (goblin-install a fresh repo, or set --engine-dir)"
|
|
@@ -244,7 +244,7 @@ fi
|
|
|
244
244
|
|
|
245
245
|
# ---- dry-run report -------------------------------------------------------------
|
|
246
246
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
247
|
-
printf '
|
|
247
|
+
printf 'gob upgrade --dry-run — target %s\n' "$REPO"
|
|
248
248
|
printf ' engine-dir: %s\n' "$ED"
|
|
249
249
|
case "$MODE" in
|
|
250
250
|
migrate)
|
|
@@ -447,7 +447,7 @@ if [ -f "$GATE" ] && grep -q '\.goblin/bin/goblin-lib.sh' "$GATE"; then
|
|
|
447
447
|
awk '
|
|
448
448
|
/^# shellcheck source=\/dev\/null$/ && !done {
|
|
449
449
|
print "# shellcheck source=/dev/null"
|
|
450
|
-
print "# The engine went global (
|
|
450
|
+
print "# The engine went global (gob upgrade): resolve it exactly as"
|
|
451
451
|
print "# goblin-verify does - engine_dir:, then the machine default."
|
|
452
452
|
print "CONFIG=\"$ROOT/.goblin/goblin.yaml\""
|
|
453
453
|
print "ENGINE_DIR=$(sed -n \"s/^engine_dir:[[:space:]]*//p\" \"$CONFIG\" | head -n 1)"
|
package/bin/goblin-verify
CHANGED
|
@@ -1363,7 +1363,7 @@ check_sc_07() {
|
|
|
1363
1363
|
local rec="$ROOT/.goblin/audit.tsv" wfile="$ROOT/.goblin/audit-waiver.tsv"
|
|
1364
1364
|
if [ ! -f "$rec" ]; then
|
|
1365
1365
|
if [ "$ENGINE_MODE" = "global" ]; then
|
|
1366
|
-
printf 'no audit record yet. Run `
|
|
1366
|
+
printf 'no audit record yet. Run `gob audit` once, deliberately, then commit\n.goblin/audit.tsv: SC-07 reads the record and never the network\n'
|
|
1367
1367
|
else
|
|
1368
1368
|
printf 'no audit record yet. Run .goblin/bin/goblin-audit once, deliberately, then commit\n.goblin/audit.tsv: SC-07 reads the record and never the network\n'
|
|
1369
1369
|
fi
|
package/bin/goblin.js
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
// and a marketplace packager stripping the executable bit breaks the symlink, not
|
|
7
7
|
// the spawnSync path below.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
9
|
+
// gob verify [--only <id,...>] ... -> bin/goblin-verify
|
|
10
|
+
// gob bans [...] -> bin/goblin-bans
|
|
11
|
+
// gob audit [...] -> bin/goblin-audit
|
|
12
|
+
// gob upgrade [...] -> bin/goblin-upgrade (W3)
|
|
13
|
+
// gob doctor [...] -> bin/goblin-doctor (W4a)
|
|
14
|
+
// gob emit [...] -> bin/goblin-emit (W4a)
|
|
15
|
+
// gob init [...] -> bin/goblin-init (W6, the first-run wizard)
|
|
16
|
+
// gob uninstall [--target <dir>] -> bin/goblin-install --uninstall
|
|
17
|
+
// gob install [...] -> bin/goblin-install (the one legacy fallback)
|
|
18
18
|
// no args | -h/--help | any other unrecognized first arg
|
|
19
19
|
// -> this file's short usage, exit 2. A bare `goblin`
|
|
20
20
|
// used to fall through into the installer; a typo
|
|
@@ -50,23 +50,23 @@ const [cmd, ...rest] = process.argv.slice(2);
|
|
|
50
50
|
// No args, a help flag, or an unrecognized first arg: short usage, exit 2. The one survivor of
|
|
51
51
|
// the old catch-all fallback is the literal `install` first arg — bare `goblin` mapped to the
|
|
52
52
|
// installer through npm's bin default, and a typo (`goblin inti`) silently installed into
|
|
53
|
-
// whatever directory the shell sat in. A bare subcommand-less `
|
|
53
|
+
// whatever directory the shell sat in. A bare subcommand-less `gob install ...` keeps the
|
|
54
54
|
// installer; everything else stops here and names the word it did not know.
|
|
55
55
|
function usage() {
|
|
56
56
|
process.stderr.write(
|
|
57
57
|
[
|
|
58
|
-
"
|
|
58
|
+
"gob <command>",
|
|
59
59
|
"",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
60
|
+
" gob init start here — the guided first step (detect, class, emit, first verify)",
|
|
61
|
+
" gob verify run the rule matrix against the current repo",
|
|
62
|
+
" gob bans run the ban list (per-pattern red lines over the source tree)",
|
|
63
|
+
" gob audit check recorded dependency claims against live advisory feeds",
|
|
64
|
+
" gob upgrade migrate a repo to the shared global engine at ~/.goblin/engine",
|
|
65
|
+
" gob doctor one detection/drift run across the agent platforms",
|
|
66
|
+
" gob emit write the skills + context block for one platform",
|
|
67
|
+
" gob uninstall --target . remove exactly what an install wrote (preimages)",
|
|
68
68
|
"",
|
|
69
|
-
"start here:
|
|
69
|
+
"start here: gob init",
|
|
70
70
|
"uninstall: npm uninstall -g @techgoblin/gobstack",
|
|
71
71
|
"",
|
|
72
72
|
].join("\n"));
|
|
@@ -77,7 +77,7 @@ if (cmd === undefined || cmd.startsWith("-")) {
|
|
|
77
77
|
process.exit(2);
|
|
78
78
|
}
|
|
79
79
|
if (!SCRIPT[cmd] && cmd !== "install" && cmd !== "uninstall") {
|
|
80
|
-
process.stderr.write(`
|
|
80
|
+
process.stderr.write(`gob: unrecognized command: ${cmd}\n\n`);
|
|
81
81
|
usage();
|
|
82
82
|
process.exit(2);
|
|
83
83
|
}
|
|
@@ -87,7 +87,7 @@ let extra = [];
|
|
|
87
87
|
if (cmd === "install") {
|
|
88
88
|
target = "goblin-install"; // the one legacy fallback, kept verbatim
|
|
89
89
|
} else if (cmd === "uninstall") {
|
|
90
|
-
// `
|
|
90
|
+
// `gob uninstall --target <dir>` routes into the installer's uninstall job — the shape
|
|
91
91
|
// docs/GUIDE.md and README already promise. `--uninstall` is appended FIRST so the user's
|
|
92
92
|
// own `--target <dir>` and options still parse, and a stray literal `--uninstall` cannot
|
|
93
93
|
// appear twice.
|