@antelopejs/dms 0.16.1 → 0.17.0
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/CHANGELOG.md +43 -0
- package/README.md +1 -1
- package/bin/control/base.mjs +3 -3
- package/bin/control/commands-verify.mjs +1 -29
- package/bin/control/renderer.test.mjs +0 -57
- package/bin/control/verify/decode.mjs +0 -2
- package/bin/control/verify/decode.test.mjs +1 -1
- package/bin/control/verify/doors.test.mjs +78 -33
- package/bin/control/verify/judgement.mjs +0 -1
- package/bin/control/verify/map.mjs +4 -51
- package/bin/control/verify/skill.mjs +4 -6
- package/bin/control/verify/types.mjs +3 -4
- package/bin/control/verify/verdict.mjs +8 -51
- package/bin/dms-control.mjs +0 -1
- package/dist/store/cache.d.ts +5 -1
- package/dist/store/cache.js +23 -14
- package/dist/store/cache.js.map +1 -1
- package/dist/store/frontend-compatibility.d.ts +3 -1
- package/dist/store/frontend-compatibility.js +6 -8
- package/dist/store/frontend-compatibility.js.map +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/interfaces.d.ts +3 -2
- package/dist/store/interfaces.js +16 -14
- package/dist/store/interfaces.js.map +1 -1
- package/dist/store/listing.js +6 -4
- package/dist/store/listing.js.map +1 -1
- package/dist/store/operations.js +1 -1
- package/dist/store/operations.js.map +1 -1
- package/dist/store/registry.d.ts +2 -1
- package/dist/store/registry.js +7 -26
- package/dist/store/registry.js.map +1 -1
- package/dist/store/requirements.js +3 -3
- package/dist/store/requirements.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/utils/storedAccounts.ts +62 -0
- package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +12 -9
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +30 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/HeaderActionIcon.vue +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/useHeaderActions.ts +9 -0
- package/frontend-vue/layers/dms-layout/app/plugins/seo.ts +7 -0
- package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +10 -0
- package/frontend-vue/package.json +1 -1
- package/package.json +3 -3
- package/skills/dms-project/DEV-SERVERS.md +30 -4
- package/skills/dms-project/SKILL.md +8 -1
- package/skills/dms-project/scripts/dms-dev-usage.sh +5 -2
- package/skills/dms-project/scripts/dms-dev.sh +82 -69
- package/bin/control/verify/seeded.mjs +0 -217
|
@@ -1,45 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# dms-dev — start / stop / restart the AntelopeJS DMS dev servers of ONE project.
|
|
3
3
|
# Run from the DMS backend package dir (DMS_BACK_DIR). By default it invokes that project's
|
|
4
|
-
# `pnpm dev` / `pnpm frontend:dev` scripts
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Those script names aren't shipped by the DMS, so override the actual commands with
|
|
8
|
-
# DMS_BACK_CMD / DMS_FRONT_CMD when a project starts its servers differently.
|
|
4
|
+
# `pnpm dev` / `pnpm frontend:dev` scripts (`ajs project dev …` and `ajs dms dev`, which
|
|
5
|
+
# auto-discovers the backend). Those script names aren't shipped by the DMS: override the
|
|
6
|
+
# commands with DMS_BACK_CMD / DMS_FRONT_CMD when a project starts its servers differently.
|
|
9
7
|
#
|
|
10
|
-
# Scope:
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# dev servers on the same machine are never touched.
|
|
8
|
+
# Scope: it only signals the processes of DMS_BACK_DIR. The pids it starts are recorded in
|
|
9
|
+
# DMS_BACK_DIR/.antelope/dms-dev.pid; without that file it falls back to the dev processes
|
|
10
|
+
# whose working directory or command line lies under DMS_BACK_DIR.
|
|
14
11
|
#
|
|
15
|
-
# Windows (Git Bash, MSYS2, Cygwin):
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# a server's whole tree with taskkill /T /F.
|
|
12
|
+
# Windows (Git Bash, MSYS2, Cygwin): ps lists only their own processes, while the servers run
|
|
13
|
+
# as native node.exe. There it reads the Windows process table (Get-CimInstance
|
|
14
|
+
# Win32_Process), works in Windows pids, and stops a server's tree with taskkill /T /F.
|
|
19
15
|
|
|
20
16
|
set -uo pipefail
|
|
21
17
|
|
|
22
18
|
DMS_BACK_DIR="${DMS_BACK_DIR:-}"
|
|
23
19
|
BACK_CMD="${DMS_BACK_CMD:-pnpm dev}"
|
|
24
20
|
FRONT_CMD="${DMS_FRONT_CMD:-pnpm frontend:dev}"
|
|
25
|
-
BACK_LOG="${DMS_BACK_LOG
|
|
26
|
-
FRONT_LOG="${DMS_FRONT_LOG
|
|
27
|
-
BACK_READY_PATTERN='Server started, listening'
|
|
21
|
+
BACK_LOG="${DMS_BACK_LOG:-}"
|
|
22
|
+
FRONT_LOG="${DMS_FRONT_LOG:-}"
|
|
23
|
+
BACK_READY_PATTERN='Server started, listening|Port [0-9]+ in use, listening on'
|
|
28
24
|
FRONT_READY_PATTERN='Server ready on|Local:.*localhost:'
|
|
29
|
-
#
|
|
30
|
-
# the wait immediately instead of burning the full timeout.
|
|
25
|
+
# A match of these aborts the wait at once instead of burning the full timeout.
|
|
31
26
|
BACK_ERROR_PATTERN='Failed to load module|Module load failed|Error loading module|UnhandledPromiseRejection|Cannot find module|ENOENT.*package\.json|TypeError:.*at .*ajs|FATAL'
|
|
32
27
|
FRONT_ERROR_PATTERN='Cannot find module|Failed to compile|Vite error|ENOENT.*package\.json|EADDRINUSE'
|
|
33
28
|
WAIT_TIMEOUT="${DMS_DEV_TIMEOUT:-180}"
|
|
34
29
|
STOP_GRACE="${DMS_DEV_STOP_GRACE:-10}"
|
|
35
30
|
DEV_JSON_WAIT=5
|
|
36
|
-
DEFAULT_BACK_URL=
|
|
37
|
-
DEFAULT_FRONT_URL=
|
|
31
|
+
DEFAULT_BACK_URL="http://localhost:${DMS_BACK_PORT:-5010}"
|
|
32
|
+
DEFAULT_FRONT_URL="http://localhost:${DMS_FRONT_PORT:-3001}"
|
|
38
33
|
|
|
39
|
-
#
|
|
40
|
-
# DMS_BACK_DIR scope check: on its own it would match every project on the machine.
|
|
34
|
+
# Candidates when no pid file exists; always combined with the DMS_BACK_DIR scope check.
|
|
41
35
|
PROC_PATTERN='antelope-runner|ajs project (run|dev)|(^|[[:space:]/])(ajs dms|ajs-dms) dev|dms-frontend/dist/index\.js dev|pnpm( run)? (dev|frontend:dev)'
|
|
42
|
-
# The same,
|
|
36
|
+
# The same, per server, for restarting only one.
|
|
43
37
|
BACK_PROC_PATTERN='antelope-runner|ajs project (run|dev)|pnpm( run)? dev'
|
|
44
38
|
FRONT_PROC_PATTERN='(^|[[:space:]/])(ajs dms|ajs-dms) dev|dms-frontend/dist/index\.js dev|pnpm( run)? frontend:dev'
|
|
45
39
|
|
|
@@ -48,8 +42,8 @@ case "$(uname -s 2>/dev/null)" in
|
|
|
48
42
|
*) IS_WINDOWS="" ;;
|
|
49
43
|
esac
|
|
50
44
|
|
|
51
|
-
# One line per process: "pid<TAB>ppid<TAB>created<TAB>command line"
|
|
52
|
-
#
|
|
45
|
+
# One line per process: "pid<TAB>ppid<TAB>created<TAB>command line"; the fixed-width UTC
|
|
46
|
+
# creation time tells a child from an older process holding a dead parent's pid.
|
|
53
47
|
# shellcheck disable=SC2016 # PowerShell code, not shell expansions
|
|
54
48
|
WIN_PROCESS_QUERY='[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
55
49
|
$tab = [char]9
|
|
@@ -62,8 +56,7 @@ Get-CimInstance Win32_Process | ForEach-Object {
|
|
|
62
56
|
PROJECT_ROOT=""
|
|
63
57
|
PID_FILE=""
|
|
64
58
|
LAUNCHED_PID=""
|
|
65
|
-
# Windows only: the process table
|
|
66
|
-
# lowercase Windows path with forward slashes, the form command lines are compared in.
|
|
59
|
+
# Windows only: the process table and the project root as a lowercase forward-slash path.
|
|
67
60
|
WIN_TABLE=""
|
|
68
61
|
WIN_ROOT=""
|
|
69
62
|
|
|
@@ -85,13 +78,15 @@ require_dms_back_dir() {
|
|
|
85
78
|
fi
|
|
86
79
|
PROJECT_ROOT="$(cd "$DMS_BACK_DIR" && pwd -P)"
|
|
87
80
|
PID_FILE="$PROJECT_ROOT/.antelope/dms-dev.pid"
|
|
81
|
+
BACK_LOG="${BACK_LOG:-$PROJECT_ROOT/.antelope/logs/back.log}"
|
|
82
|
+
FRONT_LOG="${FRONT_LOG:-$PROJECT_ROOT/.antelope/logs/front.log}"
|
|
88
83
|
[ -z "$IS_WINDOWS" ] || WIN_ROOT="$(cygpath -m "$PROJECT_ROOT" | tr '[:upper:]' '[:lower:]')"
|
|
89
84
|
}
|
|
90
85
|
|
|
91
86
|
is_alive() { [ -n "$1" ] && kill -0 "$1" 2>/dev/null; }
|
|
92
87
|
|
|
93
|
-
# Reads the Windows process table
|
|
94
|
-
#
|
|
88
|
+
# Reads the Windows process table for the lookups that follow; call it in the shell that
|
|
89
|
+
# runs them, since a command substitution's copy is lost with it.
|
|
95
90
|
refresh_process_table() {
|
|
96
91
|
[ -n "$IS_WINDOWS" ] || return 0
|
|
97
92
|
WIN_TABLE="$(powershell.exe -NoProfile -NonInteractive -EncodedCommand \
|
|
@@ -108,7 +103,7 @@ win_alive() {
|
|
|
108
103
|
|
|
109
104
|
win_args() { printf '%s\n' "$WIN_TABLE" | awk -F'\t' -v pid="$1" '$1 == pid { print $4; exit }'; }
|
|
110
105
|
|
|
111
|
-
# A native process
|
|
106
|
+
# A native process has no working directory: only its command line names the project.
|
|
112
107
|
win_in_project() {
|
|
113
108
|
local args
|
|
114
109
|
# shellcheck disable=SC1003 # tr's escaped backslash, not a quote
|
|
@@ -117,8 +112,7 @@ win_in_project() {
|
|
|
117
112
|
return 1
|
|
118
113
|
}
|
|
119
114
|
|
|
120
|
-
# The given Windows pids plus
|
|
121
|
-
# process groups), each child created after its parent.
|
|
115
|
+
# The given Windows pids plus their descendants, each child created after its parent.
|
|
122
116
|
win_expand_tree() {
|
|
123
117
|
printf '%s\n' "$WIN_TABLE" | awk -F'\t' -v roots="$*" '
|
|
124
118
|
BEGIN { n = split(roots, r, " "); for (i = 1; i <= n; i++) keep[r[i]] = 1 }
|
|
@@ -155,7 +149,7 @@ is_in_project() {
|
|
|
155
149
|
return 1
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
# The given roots plus
|
|
152
|
+
# The given roots plus their descendants and process-group members.
|
|
159
153
|
expand_tree() {
|
|
160
154
|
ps -A -o pid=,ppid=,pgid= | awk -v roots="$*" '
|
|
161
155
|
BEGIN { n = split(roots, r, " "); for (i = 1; i <= n; i++) keep[r[i]] = 1 }
|
|
@@ -170,8 +164,7 @@ expand_tree() {
|
|
|
170
164
|
}'
|
|
171
165
|
}
|
|
172
166
|
|
|
173
|
-
# The live pids the pid file records, of every server or
|
|
174
|
-
# as Windows pids.
|
|
167
|
+
# The live pids the pid file records, of every server or the one named (Windows pids on Windows).
|
|
175
168
|
pid_file_roots() {
|
|
176
169
|
[ -f "$PID_FILE" ] || return 0
|
|
177
170
|
local only="${1:-}" name pid
|
|
@@ -212,8 +205,7 @@ server_pattern() {
|
|
|
212
205
|
esac
|
|
213
206
|
}
|
|
214
207
|
|
|
215
|
-
# Every process of this project's servers, or of the one named (
|
|
216
|
-
# Windows they are Windows pids, read from the table refresh_process_table last read.
|
|
208
|
+
# Every process of this project's servers, or of the one named (Windows pids on Windows).
|
|
217
209
|
project_pids() {
|
|
218
210
|
local name="${1:-}" roots
|
|
219
211
|
roots="$(pid_file_roots "$name")"
|
|
@@ -248,7 +240,7 @@ self_ancestors() {
|
|
|
248
240
|
done
|
|
249
241
|
}
|
|
250
242
|
|
|
251
|
-
# An agent spawned by the backend (e.g.
|
|
243
|
+
# An agent spawned by the backend (e.g. dms-ai) would take its own host down.
|
|
252
244
|
refuse_if_hosting_self() {
|
|
253
245
|
local hosting
|
|
254
246
|
hosting="$(printf '%s\n' "$@" | grep -Fx -f <(self_ancestors) | head -1)"
|
|
@@ -284,7 +276,7 @@ signal_and_wait() {
|
|
|
284
276
|
return 1
|
|
285
277
|
}
|
|
286
278
|
|
|
287
|
-
#
|
|
279
|
+
# A console node process handles no SIGTERM on Windows: each tree is ended at once.
|
|
288
280
|
win_kill_and_wait() {
|
|
289
281
|
local pid survivors
|
|
290
282
|
for pid in "$@"; do taskkill //T //F //PID "$pid" >/dev/null 2>&1; done
|
|
@@ -334,22 +326,22 @@ stop_servers() {
|
|
|
334
326
|
log "stopped."
|
|
335
327
|
}
|
|
336
328
|
|
|
337
|
-
#
|
|
338
|
-
|
|
329
|
+
# Runs in a launch subshell: a non-empty port becomes that child's PORT alone.
|
|
330
|
+
enter_project() { cd "$PROJECT_ROOT" && { [ -z "$1" ] || export PORT="$1"; }; }
|
|
331
|
+
|
|
332
|
+
# Detaches the command into its own session and records its pid; `exec` keeps the pid stable.
|
|
339
333
|
launch_detached() {
|
|
340
|
-
local name="$1" cmd="$2" log_file="$3"
|
|
334
|
+
local name="$1" cmd="$2" log_file="$3" port="${4:-}"
|
|
335
|
+
local -a detach=(nohup)
|
|
341
336
|
if command -v setsid >/dev/null 2>&1; then
|
|
342
|
-
|
|
343
|
-
(cd "$PROJECT_ROOT" && exec setsid nohup $cmd >> "$log_file" 2>&1 < /dev/null) &
|
|
337
|
+
detach=(setsid nohup)
|
|
344
338
|
elif command -v perl >/dev/null 2>&1; then
|
|
345
339
|
# macOS ships no setsid(1); perl's POSIX::setsid does the same before exec.
|
|
346
|
-
|
|
347
|
-
(cd "$PROJECT_ROOT" && exec perl -MPOSIX -e 'POSIX::setsid(); exec { $ARGV[0] } @ARGV or die "exec $ARGV[0]: $!\n"' \
|
|
348
|
-
nohup $cmd >> "$log_file" 2>&1 < /dev/null) &
|
|
349
|
-
else
|
|
350
|
-
# shellcheck disable=SC2086
|
|
351
|
-
(cd "$PROJECT_ROOT" && exec nohup $cmd >> "$log_file" 2>&1 < /dev/null) &
|
|
340
|
+
detach=(perl -MPOSIX -e 'POSIX::setsid(); exec { $ARGV[0] } @ARGV or die "exec $ARGV[0]: $!\n"' nohup)
|
|
352
341
|
fi
|
|
342
|
+
mkdir -p "$(dirname "$log_file")" && : > "$log_file"
|
|
343
|
+
# shellcheck disable=SC2086 # word-splitting of the command is intentional
|
|
344
|
+
(enter_project "$port" && exec "${detach[@]}" $cmd >> "$log_file" 2>&1 < /dev/null) &
|
|
353
345
|
LAUNCHED_PID=$!
|
|
354
346
|
mkdir -p "$(dirname "$PID_FILE")"
|
|
355
347
|
echo "$name $LAUNCHED_PID" >> "$PID_FILE"
|
|
@@ -361,16 +353,18 @@ fail_start() {
|
|
|
361
353
|
tail -40 "$log_file" | sed 's/^/ /'
|
|
362
354
|
}
|
|
363
355
|
|
|
356
|
+
fatal_in() {
|
|
357
|
+
grep -qE "$3" "$2" 2>/dev/null || return 1
|
|
358
|
+
log "fatal error detected in $2:"
|
|
359
|
+
grep -nE "$3" "$2" | head -5 | sed 's/^/ /'
|
|
360
|
+
fail_start "$1" "$2" "fatal error"
|
|
361
|
+
}
|
|
362
|
+
|
|
364
363
|
wait_for() {
|
|
365
364
|
local log_file="$1" pattern="$2" label="$3" error_pattern="$4" pid="$5" started
|
|
366
365
|
started=$(date +%s)
|
|
367
366
|
while ! grep -qE "$pattern" "$log_file" 2>/dev/null; do
|
|
368
|
-
|
|
369
|
-
log "fatal error detected in $log_file:"
|
|
370
|
-
grep -nE "$error_pattern" "$log_file" | head -5 | sed 's/^/ /'
|
|
371
|
-
fail_start "$label" "$log_file" "fatal error"
|
|
372
|
-
return 1
|
|
373
|
-
fi
|
|
367
|
+
fatal_in "$label" "$log_file" "$error_pattern" && return 1
|
|
374
368
|
if ! is_alive "$pid"; then
|
|
375
369
|
fail_start "$label" "$log_file" "process $pid exited before becoming ready"
|
|
376
370
|
return 1
|
|
@@ -398,8 +392,8 @@ read_dev_json() {
|
|
|
398
392
|
' "$1" 2>/dev/null
|
|
399
393
|
}
|
|
400
394
|
|
|
401
|
-
# The project may live in DMS_BACK_DIR or one level below
|
|
402
|
-
#
|
|
395
|
+
# The project may live in DMS_BACK_DIR or one level below; only a dev.json whose writer is
|
|
396
|
+
# alive and ours is trusted, since it outlives its backend.
|
|
403
397
|
dev_json_url() {
|
|
404
398
|
local file pid url
|
|
405
399
|
for file in "$PROJECT_ROOT/.antelope/dev.json" "$PROJECT_ROOT"/*/.antelope/dev.json; do
|
|
@@ -412,8 +406,7 @@ dev_json_url() {
|
|
|
412
406
|
done
|
|
413
407
|
}
|
|
414
408
|
|
|
415
|
-
# Whether a pid a server wrote down itself
|
|
416
|
-
# and one of this project's.
|
|
409
|
+
# Whether a pid a server wrote down itself is alive and one of this project's.
|
|
417
410
|
is_project_process() {
|
|
418
411
|
if [ -z "$IS_WINDOWS" ]; then
|
|
419
412
|
is_alive "$1" && is_in_project "$1"
|
|
@@ -432,7 +425,7 @@ backend_url() {
|
|
|
432
425
|
sleep 1
|
|
433
426
|
url="$(dev_json_url)"
|
|
434
427
|
done
|
|
435
|
-
[ -n "$url" ] || url="$(grep -oE "
|
|
428
|
+
[ -n "$url" ] || url="$(grep -oE "listening on https?://[^ /]+" "$BACK_LOG" | tail -1 | awk '{print $NF}')"
|
|
436
429
|
echo "${url:-$DEFAULT_BACK_URL (assumed: no .antelope/dev.json found)}"
|
|
437
430
|
}
|
|
438
431
|
|
|
@@ -442,24 +435,42 @@ frontend_url() {
|
|
|
442
435
|
echo "${url:-$DEFAULT_FRONT_URL (assumed: no ready line with a URL)}"
|
|
443
436
|
}
|
|
444
437
|
|
|
438
|
+
# The ready line can precede the module load; waits until the dashboard's first request
|
|
439
|
+
# succeeds (401/403 count: it sends the credential). Direct, like the dashboard's fetch.
|
|
440
|
+
# No curl, or nothing accepting connections (a stub): skips.
|
|
441
|
+
wait_for_manifest() {
|
|
442
|
+
local url="$1/dms/frontend?renderer=vue&rendererVersion=3" pid="$2" code rc
|
|
443
|
+
command -v curl >/dev/null 2>&1 || return 0
|
|
444
|
+
while :; do
|
|
445
|
+
code="$(curl -s --noproxy '*' -o /dev/null -m 5 -w '%{http_code}' "$url")"; rc=$?
|
|
446
|
+
case "$rc:$code" in 7:* | *:200 | *:401 | *:403) return 0 ;; esac
|
|
447
|
+
fatal_in backend "$BACK_LOG" "$BACK_ERROR_PATTERN" && return 1
|
|
448
|
+
is_alive "$pid" || { fail_start backend "$BACK_LOG" "process $pid exited before serving $url"; return 1; }
|
|
449
|
+
[ $(($(date +%s) - BACK_STARTED)) -le "$WAIT_TIMEOUT" ] ||
|
|
450
|
+
{ fail_start backend "$BACK_LOG" "$url still answered ${code} after ${WAIT_TIMEOUT}s"; return 1; }
|
|
451
|
+
sleep 1
|
|
452
|
+
done
|
|
453
|
+
}
|
|
454
|
+
|
|
445
455
|
start_backend() {
|
|
456
|
+
local url
|
|
446
457
|
log "starting backend in $PROJECT_ROOT (logs → $BACK_LOG)…"
|
|
447
|
-
|
|
448
|
-
launch_detached backend "$BACK_CMD" "$BACK_LOG"
|
|
458
|
+
BACK_STARTED=$(date +%s)
|
|
459
|
+
launch_detached backend "$BACK_CMD" "$BACK_LOG" "${DMS_BACK_PORT:-}"
|
|
449
460
|
wait_for "$BACK_LOG" "$BACK_READY_PATTERN" "backend" "$BACK_ERROR_PATTERN" "$LAUNCHED_PID" || return 1
|
|
450
|
-
|
|
461
|
+
url="$(backend_url)"
|
|
462
|
+
case "$url" in *" "*) ;; *) wait_for_manifest "$url" "$LAUNCHED_PID" || return 1 ;; esac
|
|
463
|
+
log "backend ready on $url."
|
|
451
464
|
}
|
|
452
465
|
|
|
453
466
|
start_frontend() {
|
|
454
467
|
log "starting frontend in $PROJECT_ROOT (logs → $FRONT_LOG)…"
|
|
455
|
-
|
|
456
|
-
launch_detached frontend "$FRONT_CMD" "$FRONT_LOG"
|
|
468
|
+
launch_detached frontend "$FRONT_CMD" "$FRONT_LOG" "${DMS_FRONT_PORT:-}"
|
|
457
469
|
wait_for "$FRONT_LOG" "$FRONT_READY_PATTERN" "frontend" "$FRONT_ERROR_PATTERN" "$LAUNCHED_PID" || return 1
|
|
458
470
|
log "frontend ready on $(frontend_url)."
|
|
459
471
|
}
|
|
460
472
|
|
|
461
|
-
#
|
|
462
|
-
# one keeps running.
|
|
473
|
+
# Restarts the server named (backend or frontend); the other keeps running.
|
|
463
474
|
restart_server() {
|
|
464
475
|
local name="$1"
|
|
465
476
|
stop_servers "$name" || true
|
|
@@ -484,7 +495,7 @@ start_servers() {
|
|
|
484
495
|
|
|
485
496
|
status() {
|
|
486
497
|
require_dms_back_dir status
|
|
487
|
-
log "dev processes of $PROJECT_ROOT:"
|
|
498
|
+
log "dev processes of $PROJECT_ROOT (logs: $BACK_LOG, $FRONT_LOG):"
|
|
488
499
|
local targets
|
|
489
500
|
refresh_process_table
|
|
490
501
|
targets="$(project_pids)"
|
|
@@ -541,12 +552,14 @@ case "$cmd" in
|
|
|
541
552
|
status
|
|
542
553
|
;;
|
|
543
554
|
back-log)
|
|
555
|
+
[ -n "$BACK_LOG" ] || require_dms_back_dir back-log
|
|
544
556
|
tail -n "${2:-60}" "$BACK_LOG"
|
|
545
557
|
;;
|
|
546
558
|
""|-h|--help|help)
|
|
547
559
|
usage
|
|
548
560
|
;;
|
|
549
561
|
front-log)
|
|
562
|
+
[ -n "$FRONT_LOG" ] || require_dms_back_dir front-log
|
|
550
563
|
tail -n "${2:-60}" "$FRONT_LOG"
|
|
551
564
|
;;
|
|
552
565
|
*)
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
cpSync,
|
|
4
|
-
existsSync,
|
|
5
|
-
mkdtempSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join, relative, resolve } from "node:path";
|
|
11
|
-
import { git } from "../git.mjs";
|
|
12
|
-
import { CONTROL_BIN } from "../stack.mjs";
|
|
13
|
-
import { readFeatureResults } from "./decode.mjs";
|
|
14
|
-
import { runPrepare } from "./skill.mjs";
|
|
15
|
-
|
|
16
|
-
const OUTPUT_LIMIT = 64 * 1024 * 1024;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {string} cwd Where to run it
|
|
20
|
-
* @param {string} program The program
|
|
21
|
-
* @param {string[]} args Its arguments
|
|
22
|
-
* @returns {import("node:child_process").SpawnSyncReturns<string>} The run
|
|
23
|
-
*/
|
|
24
|
-
function run(cwd, program, args) {
|
|
25
|
-
return spawnSync(program, args, {
|
|
26
|
-
cwd,
|
|
27
|
-
encoding: "utf8",
|
|
28
|
-
env: { ...process.env, CI: "1" },
|
|
29
|
-
maxBuffer: OUTPUT_LIMIT,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {string} cwd Where to run it
|
|
35
|
-
* @param {string} program The program
|
|
36
|
-
* @param {string[]} args Its arguments
|
|
37
|
-
* @returns {string} Its trimmed output
|
|
38
|
-
*/
|
|
39
|
-
function mustRun(cwd, program, args) {
|
|
40
|
-
const result = run(cwd, program, args);
|
|
41
|
-
if (result.status !== 0) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`${program} ${args.join(" ")} failed in ${cwd}:\n${result.stdout}${result.stderr}`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
return result.stdout.trim();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A scratch worktree of HEAD with the current skill copied in, the patch
|
|
51
|
-
* applied when there is one, and the config's `prepare` commands run. The
|
|
52
|
-
* worktree is the skill's repository, which is also the renderer checkout
|
|
53
|
-
* when the config names a `renderer`: the patch changes what the run serves.
|
|
54
|
-
*
|
|
55
|
-
* @param {import("./skill.mjs").Skill} skill The skill
|
|
56
|
-
* @param {string} label What the worktree is for
|
|
57
|
-
* @param {string | null} patch The patch file, or `null` for the control
|
|
58
|
-
* @returns {{ worktree: string, skill: string }} The worktree and its skill copy
|
|
59
|
-
*/
|
|
60
|
-
function prepareWorktree(skill, label, patch) {
|
|
61
|
-
const worktree = mkdtempSync(
|
|
62
|
-
join(tmpdir(), `${skill.name}-seeded-${label}-`),
|
|
63
|
-
);
|
|
64
|
-
git(["worktree", "add", "--detach", "--force", worktree, "HEAD"], skill.repo);
|
|
65
|
-
const copy = join(worktree, relative(skill.repo, skill.dir));
|
|
66
|
-
rmSync(copy, { recursive: true, force: true });
|
|
67
|
-
cpSync(skill.dir, copy, { recursive: true });
|
|
68
|
-
if (patch) mustRun(worktree, "git", ["apply", patch]);
|
|
69
|
-
runPrepare(skill, worktree);
|
|
70
|
-
return { worktree, skill: copy };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @param {import("./skill.mjs").Skill} skill The skill
|
|
75
|
-
* @param {string} worktree The scratch worktree
|
|
76
|
-
*/
|
|
77
|
-
function removeWorktree(skill, worktree) {
|
|
78
|
-
git(["worktree", "remove", "--force", worktree], skill.repo);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The `up` arguments for a scratch worktree: the config's playground and
|
|
83
|
-
* renderer, resolved in the worktree unless absolute.
|
|
84
|
-
*
|
|
85
|
-
* @param {import("./skill.mjs").Skill} skill The skill
|
|
86
|
-
* @param {{ worktree: string, skill: string }} prepared The scratch worktree and its skill copy
|
|
87
|
-
* @returns {string[]} The arguments
|
|
88
|
-
*/
|
|
89
|
-
export function upArguments(skill, prepared) {
|
|
90
|
-
const { playground, renderer } = skill.config;
|
|
91
|
-
return [
|
|
92
|
-
"up",
|
|
93
|
-
"--playground",
|
|
94
|
-
resolve(prepared.worktree, playground),
|
|
95
|
-
...(renderer ? ["--renderer", resolve(prepared.worktree, renderer)] : []),
|
|
96
|
-
"--skill",
|
|
97
|
-
prepared.skill,
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @param {{ worktree: string, skill: string }} prepared The scratch worktree and its skill copy
|
|
103
|
-
* @param {import("./skill.mjs").Skill} skill The skill
|
|
104
|
-
* @param {string} feature The feature
|
|
105
|
-
* @returns {{ run: string, results: import("./types.mjs").FeatureResult[], output: string }} The run folder, its results and output
|
|
106
|
-
*/
|
|
107
|
-
function runFeatureIn(prepared, skill, feature) {
|
|
108
|
-
/**
|
|
109
|
-
* @param {string[]} args The dms-control arguments
|
|
110
|
-
* @returns {import("node:child_process").SpawnSyncReturns<string>} The run
|
|
111
|
-
*/
|
|
112
|
-
const control = (args) =>
|
|
113
|
-
run(prepared.worktree, process.execPath, [CONTROL_BIN, ...args]);
|
|
114
|
-
const started = control(upArguments(skill, prepared));
|
|
115
|
-
const dir = started.stdout.trim().split("\n").at(-1) ?? "";
|
|
116
|
-
if (started.status !== 0 || !existsSync(join(dir, "run.json"))) {
|
|
117
|
-
return {
|
|
118
|
-
run: dir,
|
|
119
|
-
results: [],
|
|
120
|
-
output: `up failed:\n${started.stdout}${started.stderr}`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
const journeys = control([
|
|
124
|
-
"journey",
|
|
125
|
-
dir,
|
|
126
|
-
feature,
|
|
127
|
-
"--skill",
|
|
128
|
-
prepared.skill,
|
|
129
|
-
]);
|
|
130
|
-
control(["down", dir]);
|
|
131
|
-
const file = join(dir, "evidence", feature, "feature.json");
|
|
132
|
-
const results = readFeatureResults(file);
|
|
133
|
-
return { run: dir, results, output: `${journeys.stdout}${journeys.stderr}` };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* @param {import("./types.mjs").FeatureResult} result A journey result
|
|
138
|
-
* @returns {boolean} Whether one of its checks failed
|
|
139
|
-
*/
|
|
140
|
-
function failsACheck(result) {
|
|
141
|
-
return result.steps.some((step) => step.checks.some((check) => !check.pass));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* @param {string} feature The feature
|
|
146
|
-
* @param {ReturnType<typeof runFeatureIn>} control The unpatched run
|
|
147
|
-
* @param {ReturnType<typeof runFeatureIn>} seeded The patched run
|
|
148
|
-
* @returns {import("./types.mjs").SeededOutcome} The outcome
|
|
149
|
-
*/
|
|
150
|
-
function judge(feature, control, seeded) {
|
|
151
|
-
const passedInControl = new Set(
|
|
152
|
-
control.results
|
|
153
|
-
.filter((result) => result.status === "PASS")
|
|
154
|
-
.map((result) => result.journey),
|
|
155
|
-
);
|
|
156
|
-
const failed = seeded.results.filter((result) => result.status === "FAIL");
|
|
157
|
-
const caughtBy = failed
|
|
158
|
-
.filter(
|
|
159
|
-
(result) => passedInControl.has(result.journey) && failsACheck(result),
|
|
160
|
-
)
|
|
161
|
-
.map((result) => result.journey);
|
|
162
|
-
return {
|
|
163
|
-
feature,
|
|
164
|
-
caught: caughtBy.length > 0,
|
|
165
|
-
run: seeded.run,
|
|
166
|
-
controlRun: control.run,
|
|
167
|
-
caughtBy,
|
|
168
|
-
failingInControl: control.results
|
|
169
|
-
.filter((result) => result.status === "FAIL")
|
|
170
|
-
.map((result) => result.journey),
|
|
171
|
-
harnessErrors: failed
|
|
172
|
-
.filter((result) => !failsACheck(result))
|
|
173
|
-
.map((result) => result.journey),
|
|
174
|
-
output: seeded.output,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Runs each feature's journeys twice from scratch worktrees of HEAD: once
|
|
180
|
-
* unpatched as the control, once with `seeded-breaks/<feature>.patch`. A
|
|
181
|
-
* break is caught only when a journey that passes in the control fails one
|
|
182
|
-
* of its checks with the patch; a failure without a failing check is a
|
|
183
|
-
* harness error and does not count. Worktrees are removed; both runs keep
|
|
184
|
-
* their evidence, and `seeded.json` in the patched run records the outcome.
|
|
185
|
-
*
|
|
186
|
-
* @param {import("./skill.mjs").Skill} skill The skill
|
|
187
|
-
* @param {string[]} features The features
|
|
188
|
-
* @returns {import("./types.mjs").SeededOutcome[]} One outcome per feature
|
|
189
|
-
*/
|
|
190
|
-
export function runSeededBreaks(skill, features) {
|
|
191
|
-
const control = prepareWorktree(skill, "control", null);
|
|
192
|
-
try {
|
|
193
|
-
return features.map((feature) => {
|
|
194
|
-
const unpatched = runFeatureIn(control, skill, feature);
|
|
195
|
-
const patch = join(skill.seededBreaksDir, `${feature}.patch`);
|
|
196
|
-
const seeded = prepareWorktree(skill, feature, patch);
|
|
197
|
-
try {
|
|
198
|
-
const outcome = judge(
|
|
199
|
-
feature,
|
|
200
|
-
unpatched,
|
|
201
|
-
runFeatureIn(seeded, skill, feature),
|
|
202
|
-
);
|
|
203
|
-
if (existsSync(outcome.run)) {
|
|
204
|
-
writeFileSync(
|
|
205
|
-
join(outcome.run, "evidence", "seeded.json"),
|
|
206
|
-
`${JSON.stringify(outcome, null, 2)}\n`,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
return outcome;
|
|
210
|
-
} finally {
|
|
211
|
-
removeWorktree(skill, seeded.worktree);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
} finally {
|
|
215
|
-
removeWorktree(skill, control.worktree);
|
|
216
|
-
}
|
|
217
|
-
}
|