@techierathore/techieflow 1.0.3 → 1.1.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/.claude/commands/TechieFlow/agents/analyst.md +5 -8
- package/.claude/commands/TechieFlow/agents/architect.md +1 -10
- package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
- package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +181 -9
- package/.tfcore/agents/analyst.md +5 -8
- package/.tfcore/agents/architect.md +1 -10
- package/.tfcore/agents/flow-master.md +1 -0
- package/.tfcore/agents/verifier.md +1 -0
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +27 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +6 -5
- package/.tfcore/tasks/build-phase.md +1 -1
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +3 -3
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +7 -3
- package/.tfcore/tasks/handoff-phase.md +20 -18
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +61 -1
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +435 -21
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +99 -11
- package/.tfcore/utils/tf-build-list.sh +5 -3
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +242 -10
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +413 -19
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +113 -31
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +105 -18
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +24 -21
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +33 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +80 -2
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +356 -0
- package/.tfcore/utils/tf-split-brd.py +17 -2
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +133 -44
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +254 -43
- package/.tfcore/utils/tf-verify-screens.sh +18 -4
- package/.tfcore/utils/tf-verify-tests.sh +272 -28
- package/.tfcore/utils/tf-verify-verdict.py +59 -11
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +19 -5
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +24 -0
- package/package.json +1 -1
- package/scripts/install.mjs +4 -0
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# tf-verify-boot.sh — start the application for a verify, reach it, and stop it (Sitting 4c, 2026-09-06).
|
|
3
3
|
#
|
|
4
|
-
# bash .tfcore/utils/tf-verify-boot.sh start [--head web|windows|static] [--project <csproj>]
|
|
5
|
-
# [--port N] [--config Release] [--static <dir>]
|
|
6
|
-
#
|
|
7
|
-
# bash .tfcore/utils/tf-verify-boot.sh
|
|
4
|
+
# bash .tfcore/utils/tf-verify-boot.sh start [--head web|windows|static] [--project <csproj>] [--dry-run]
|
|
5
|
+
# [--port N] [--config Release] [--static <dir>] [--probe-path /healthz]
|
|
6
|
+
# [--environment Development] (web: ASPNETCORE_ENVIRONMENT)
|
|
7
|
+
# bash .tfcore/utils/tf-verify-boot.sh stop [--port N]
|
|
8
|
+
# bash .tfcore/utils/tf-verify-boot.sh status [--port N]
|
|
8
9
|
#
|
|
9
10
|
# Heads:
|
|
10
|
-
# web a project on Microsoft.NET.Sdk.Web:
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# web a project on Microsoft.NET.Sdk.Web: published by `tf-build.sh publish` (its rungs, one
|
|
12
|
+
# build at a time) into tests/.artifacts/verify/run-<port>/, and that copy is run on the
|
|
13
|
+
# side that built it, reading its settings from the project folder as `dotnet run` does.
|
|
14
|
+
# Builders side by side never serve each other's half-written build (TF-043). A standalone
|
|
15
|
+
# WebAssembly project is still started with `dotnet run`. Prints BOOTED mode=base url=….
|
|
13
16
|
# windows a MAUI Blazor Hybrid head (UseMaui + a net*-windows target): started Windows-side with
|
|
14
17
|
# WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222, the DevTools port
|
|
15
18
|
# relayed to every interface by tf-cdp-relay.ps1, and reached from WSL over CDP. Prints
|
|
@@ -19,13 +22,19 @@
|
|
|
19
22
|
# their rows are recorded as not verified, never as static-only passes.
|
|
20
23
|
# Without --head the script picks web when a web project exists, else windows when a MAUI project
|
|
21
24
|
# with a windows target exists, else NONE. --project names the project when there are several.
|
|
22
|
-
# The state goes to tests/.artifacts/verify/boot
|
|
23
|
-
#
|
|
25
|
+
# The state goes to tests/.artifacts/verify/boot-<port>.json and the app log to app-<port>.log, one pair
|
|
26
|
+
# per app, so builders booting side by side never empty or stop each other's (TF-034); boot.json is a
|
|
27
|
+
# copy of the latest start's, which the verdict reads. `stop --port N` stops that app only; a bare
|
|
28
|
+
# `stop` stops the one boot.json names, and refuses while two or more are running. `stop` kills what
|
|
29
|
+
# `start` started and nothing else. Never asks anyone to start anything.
|
|
24
30
|
# Exit 0 booted / stopped · 2 NONE (reason printed; kind=build-error, host or no-driver) · 3 usage.
|
|
25
31
|
set -u
|
|
26
32
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
27
|
-
|
|
33
|
+
source "$HERE/tf-portable.sh" # bash-4 and GNU-only spellings go through the shim (stock Mac)
|
|
34
|
+
DIR="tests/.artifacts/verify"; STATE="$DIR/boot.json"; LOG="$DIR/app.log"; KEYED=""
|
|
28
35
|
mkdir -p "$DIR"
|
|
36
|
+
# once the port is known: this app's own state file and log (TF-034)
|
|
37
|
+
keyed() { KEYED="$DIR/boot-$1.json"; LOG="$DIR/app-$1.log"; : > "$LOG"; }
|
|
29
38
|
|
|
30
39
|
PLATFORM="linux"
|
|
31
40
|
U="$(uname -s 2>/dev/null || echo windows)"
|
|
@@ -39,29 +48,38 @@ winarg() { # a Windows path as a cmd.exe argument: bare when it has no space (a
|
|
|
39
48
|
json_escape() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; }
|
|
40
49
|
|
|
41
50
|
write_state() { # head mode url pids rung project reason kind
|
|
42
|
-
python3 - "$@" <<'PY'
|
|
43
|
-
import json, sys, datetime
|
|
51
|
+
TF_BOOT_FILES="$STATE $KEYED" TF_BOOT_LOG="$LOG" python3 - "$@" <<'PY'
|
|
52
|
+
import json, os, sys, datetime
|
|
44
53
|
head, mode, url, pids, rung, project, reason, kind = sys.argv[1:9]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
s = {"head": head, "mode": mode, "url": url, "pids": [int(p) for p in pids.split() if p.isdigit()],
|
|
55
|
+
"rung": rung, "project": project, "reason": reason, "reason_kind": kind, "stopped": False,
|
|
56
|
+
"platform": sys.argv[9] if len(sys.argv) > 9 else "", "log": os.environ["TF_BOOT_LOG"],
|
|
57
|
+
"started": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}
|
|
58
|
+
for f in os.environ["TF_BOOT_FILES"].split():
|
|
59
|
+
json.dump(s, open(f, "w"), indent=1)
|
|
50
60
|
PY
|
|
51
61
|
}
|
|
62
|
+
set_state() { # key value-as-JSON: one more field on this start's state files
|
|
63
|
+
TF_BOOT_FILES="$STATE $KEYED" python3 -c 'import json,os,sys
|
|
64
|
+
for p in os.environ["TF_BOOT_FILES"].split():
|
|
65
|
+
s=json.load(open(p)); s[sys.argv[1]]=json.loads(sys.argv[2]); json.dump(s,open(p,"w"),indent=1)' "$1" "$2"
|
|
66
|
+
}
|
|
52
67
|
|
|
53
68
|
find_projects() { # prints csproj paths under src/, source/, the root and one level down; never tests
|
|
54
69
|
find . -maxdepth 4 -name '*.csproj' -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' \
|
|
55
70
|
-not -path './tests/*' -not -path '*/test/*' -not -path '*Tests/*' -not -path '*.Tests/*' 2>/dev/null | sed 's#^\./##' | sort
|
|
56
71
|
}
|
|
57
72
|
is_web() { grep -qE 'Microsoft\.NET\.Sdk\.Web|Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$1"; }
|
|
58
|
-
is_maui_win() { grep -qiE '<UseMaui
|
|
73
|
+
is_maui_win() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-windows' "$1"; }
|
|
59
74
|
|
|
60
75
|
poll_http() { # url seconds pid-to-watch(optional) -> 0 when it answers
|
|
61
76
|
local url="$1" secs="$2" pid="${3:-}" i=0 code
|
|
62
77
|
while [[ $i -lt $secs ]]; do
|
|
63
78
|
code="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "$url" 2>/dev/null || true)"
|
|
64
|
-
|
|
79
|
+
# Any HTTP answer means the app is serving; this asks whether it is up, not whether the page is
|
|
80
|
+
# right. A Web API with nothing at / answers 404, and was polled for 120 s, called "not brought up"
|
|
81
|
+
# and stopped while its log said "Application started" (AppManager TF-004).
|
|
82
|
+
[[ "$code" =~ ^[1-5][0-9][0-9]$ ]] && return 0
|
|
65
83
|
if [[ -n "$pid" ]] && ! kill -0 "$pid" 2>/dev/null; then return 1; fi
|
|
66
84
|
sleep 2; i=$((i+2))
|
|
67
85
|
done
|
|
@@ -71,13 +89,46 @@ WRONG_RUNG='NETSDK1178|Microsoft\.(iOS|Android|MacCatalyst|tvOS)\.Sdk|Workload I
|
|
|
71
89
|
CODE_ERR='error (CS|RZ|BL|XC|XLS)[0-9]{3,5}|error MSB[0-9]{4}: .*(does not exist|could not be found)|error NU1'
|
|
72
90
|
|
|
73
91
|
cmd="${1:-}"; shift || true
|
|
92
|
+
SPORT=""; [[ "${1:-}" == "--port" && "$cmd" != "start" ]] && SPORT="${2:-}"
|
|
74
93
|
case "$cmd" in
|
|
75
|
-
status)
|
|
94
|
+
status) f="$STATE"; [[ -n "$SPORT" ]] && f="$DIR/boot-$SPORT.json"
|
|
95
|
+
if [[ -f "$f" ]]; then cat "$f"; else echo "NONE no boot state${SPORT:+ for port $SPORT}"; fi; exit 0 ;;
|
|
76
96
|
stop)
|
|
77
|
-
[[ -
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
if [[ -n "$SPORT" ]]; then
|
|
98
|
+
[[ -f "$DIR/boot-$SPORT.json" ]] || { echo "STOPPED nothing was started on port $SPORT"; exit 0; }
|
|
99
|
+
else
|
|
100
|
+
[[ -f "$STATE" ]] || { echo "STOPPED nothing was started"; exit 0; }
|
|
101
|
+
# a bare stop while several apps run could stop another builder's: name the port instead
|
|
102
|
+
live="$(python3 - "$DIR" <<'PY'
|
|
103
|
+
import glob, json, os, re, sys
|
|
104
|
+
out = []
|
|
105
|
+
for f in sorted(glob.glob(os.path.join(sys.argv[1], "boot-*.json"))):
|
|
106
|
+
try:
|
|
107
|
+
s = json.load(open(f))
|
|
108
|
+
except Exception:
|
|
109
|
+
continue
|
|
110
|
+
alive = False
|
|
111
|
+
for p in s.get("pids", []):
|
|
112
|
+
try:
|
|
113
|
+
os.kill(p, 0)
|
|
114
|
+
alive = True
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
if not s.get("stopped") and alive:
|
|
118
|
+
out.append(re.sub(r"^boot-(.+)[.]json$", r"\1", os.path.basename(f)))
|
|
119
|
+
print(" ".join(out))
|
|
120
|
+
PY
|
|
121
|
+
)"
|
|
122
|
+
if [[ $(wc -w <<<"$live") -ge 2 ]]; then
|
|
123
|
+
echo "NOT-STOPPED $(( $(wc -w <<<"$live") )) apps are running (ports ${live// /, }); stop yours with: bash .tfcore/utils/tf-verify-boot.sh stop --port <n>"; exit 2
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
python3 - "$DIR" "$SPORT" <<'PY'
|
|
127
|
+
import json, os, re, signal, subprocess, sys, time
|
|
128
|
+
d, port = sys.argv[1], sys.argv[2]
|
|
129
|
+
main = os.path.join(d, "boot.json")
|
|
130
|
+
path = os.path.join(d, f"boot-{port}.json") if port else main
|
|
131
|
+
s = json.load(open(path))
|
|
81
132
|
for pid in s.get("pids", []):
|
|
82
133
|
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
83
134
|
try:
|
|
@@ -94,18 +145,64 @@ if image:
|
|
|
94
145
|
if s.get("win_port"):
|
|
95
146
|
ps = f"Get-NetTCPConnection -LocalPort {s['win_port']} -State Listen -ErrorAction SilentlyContinue | ForEach-Object {{ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }}"
|
|
96
147
|
subprocess.run(["powershell.exe", "-NoProfile", "-Command", ps], capture_output=True)
|
|
148
|
+
# An app whose starter died is nobody's child any more, so the kills above miss it and it keeps its
|
|
149
|
+
# port and its files. The published copy names it: stop whatever runs from run-<port>/ (TF-043).
|
|
150
|
+
m = re.search(r":(\d+)$", s.get("url") or "")
|
|
151
|
+
run_port = port or (m.group(1) if m else "")
|
|
152
|
+
if run_port:
|
|
153
|
+
mark = os.path.join(os.getcwd(), d, f"run-{run_port}") + os.sep
|
|
154
|
+
try:
|
|
155
|
+
procs = [p for p in os.listdir("/proc") if p.isdigit()]
|
|
156
|
+
except Exception:
|
|
157
|
+
procs = []
|
|
158
|
+
cmds = [] # (pid, command line)
|
|
159
|
+
for p in procs:
|
|
160
|
+
try:
|
|
161
|
+
cmd = open(f"/proc/{p}/cmdline", "rb").read().replace(b"\0", b" ").decode(errors="replace")
|
|
162
|
+
except Exception:
|
|
163
|
+
continue
|
|
164
|
+
cmds.append((int(p), cmd))
|
|
165
|
+
if not procs:
|
|
166
|
+
# No /proc (macOS): the same list from ps, full width so the run-<port>/ path is not cut off.
|
|
167
|
+
try:
|
|
168
|
+
out = subprocess.run(["ps", "-A", "-ww", "-o", "pid=,command="], capture_output=True).stdout.decode(errors="replace")
|
|
169
|
+
except Exception:
|
|
170
|
+
out = ""
|
|
171
|
+
for line in out.splitlines():
|
|
172
|
+
pid, _, cmd = line.strip().partition(" ")
|
|
173
|
+
if pid.isdigit():
|
|
174
|
+
cmds.append((int(pid), cmd))
|
|
175
|
+
for pid, cmd in cmds:
|
|
176
|
+
if mark in cmd and pid != os.getpid():
|
|
177
|
+
try:
|
|
178
|
+
os.kill(pid, signal.SIGKILL)
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
97
181
|
s["stopped"] = True
|
|
98
|
-
json.dump(s, open(
|
|
182
|
+
json.dump(s, open(path, "w"), indent=1)
|
|
183
|
+
# the same app under its other name: boot.json and its own boot-<port>.json
|
|
184
|
+
twins = [main] if port else ([os.path.join(d, f"boot-{m.group(1)}.json")] if m else [])
|
|
185
|
+
for f in twins:
|
|
186
|
+
try:
|
|
187
|
+
o = json.load(open(f))
|
|
188
|
+
if o.get("pids") == s.get("pids"):
|
|
189
|
+
o["stopped"] = True
|
|
190
|
+
json.dump(o, open(f, "w"), indent=1)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
99
193
|
print(f"STOPPED head={s.get('head')} mode={s.get('mode')} pids={s.get('pids')}")
|
|
100
194
|
PY
|
|
101
195
|
exit 0 ;;
|
|
102
196
|
start) ;;
|
|
103
|
-
*) sed -n '2,
|
|
197
|
+
*) sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 3 ;;
|
|
104
198
|
esac
|
|
105
199
|
|
|
106
|
-
HEAD=""; PROJECT=""; PORT=""; CONFIG=""; STATIC=""
|
|
200
|
+
HEAD=""; PROJECT=""; PORT=""; CONFIG=""; STATIC=""; PROBE="/"; DRYRUN=0; ENVNAME=""
|
|
107
201
|
while [[ $# -gt 0 ]]; do
|
|
108
202
|
case "$1" in
|
|
203
|
+
--environment) ENVNAME="${2:-}"; shift 2 ;; # the web head's ASPNETCORE_ENVIRONMENT (Lekhak TF-001)
|
|
204
|
+
--probe-path) PROBE="${2:-/}"; [[ "$PROBE" == /* ]] || PROBE="/$PROBE"; shift 2 ;;
|
|
205
|
+
--dry-run) DRYRUN=1; shift ;; # print which project and head would boot, and stop (TF-010)
|
|
109
206
|
--head) HEAD="${2:-}"; shift 2 ;;
|
|
110
207
|
--project) PROJECT="${2:-}"; shift 2 ;;
|
|
111
208
|
--port) PORT="${2:-}"; shift 2 ;;
|
|
@@ -114,7 +211,6 @@ while [[ $# -gt 0 ]]; do
|
|
|
114
211
|
*) echo "tf-verify-boot: unknown argument $1" >&2; exit 3 ;;
|
|
115
212
|
esac
|
|
116
213
|
done
|
|
117
|
-
: > "$LOG"
|
|
118
214
|
|
|
119
215
|
# ---- static ---------------------------------------------------------------------------
|
|
120
216
|
if [[ "$HEAD" == "static" ]]; then
|
|
@@ -123,29 +219,58 @@ if [[ "$HEAD" == "static" ]]; then
|
|
|
123
219
|
if curl -s -o /dev/null -m 2 "http://localhost:$PORT/" 2>/dev/null; then
|
|
124
220
|
echo "NONE head=static reason=port $PORT is already in use; stop that process or pass --port"; exit 2
|
|
125
221
|
fi
|
|
222
|
+
keyed "$PORT"
|
|
126
223
|
nohup python3 -m http.server "$PORT" --directory "$STATIC" --bind 127.0.0.1 > "$LOG" 2>&1 &
|
|
127
224
|
PID=$!
|
|
128
225
|
if poll_http "http://localhost:$PORT/" 20 "$PID"; then
|
|
129
226
|
write_state static base "http://localhost:$PORT" "$PID" "python3 http.server" "$STATIC" "" "" "$PLATFORM"
|
|
130
|
-
echo "BOOTED head=static mode=base url=http://localhost:$PORT pid=$PID log=$LOG"; exit 0
|
|
227
|
+
echo "BOOTED head=static mode=base url=http://localhost:$PORT pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
|
|
131
228
|
fi
|
|
132
229
|
kill "$PID" 2>/dev/null; write_state static none "" "" "" "$STATIC" "http.server did not answer on $PORT" host
|
|
133
230
|
echo "NONE head=static reason=http.server did not answer on $PORT (log $LOG)"; exit 2
|
|
134
231
|
fi
|
|
135
232
|
|
|
136
233
|
# ---- pick the project and the head ------------------------------------------------------
|
|
137
|
-
|
|
234
|
+
tf_read_lines ALL < <(find_projects)
|
|
138
235
|
WEB=(); WIN=()
|
|
139
|
-
for p in "${ALL[@]}"; do is_web "$p" && WEB+=("$p"); is_maui_win "$p" && WIN+=("$p"); done
|
|
236
|
+
for p in ${ALL[@]+"${ALL[@]}"}; do is_web "$p" && WEB+=("$p"); is_maui_win "$p" && WIN+=("$p"); done
|
|
237
|
+
# Several web projects: the one that serves screens. The first in sorted order booted AppManager's
|
|
238
|
+
# external API, which has no screens, in front of its admin site (AppManager TF-010). A project
|
|
239
|
+
# serves screens when its own folder holds .razor or .cshtml pages, or it references a project
|
|
240
|
+
# built with the Razor SDK. One such project is taken and said so; several, or none, stop with the
|
|
241
|
+
# candidates named, because a guess between them grades the wrong app.
|
|
242
|
+
serves_screens() { # csproj -> 0 when it draws pages
|
|
243
|
+
local d; d="$(dirname "$1")"
|
|
244
|
+
find "$d" -maxdepth 3 \( -name '*.razor' -o -name '*.cshtml' \) -not -path '*/bin/*' -not -path '*/obj/*' -print -quit 2>/dev/null | grep -q . && return 0
|
|
245
|
+
local ref
|
|
246
|
+
while read -r ref; do
|
|
247
|
+
ref="$(sed 's#\\#/#g' <<<"$ref")"
|
|
248
|
+
[[ -f "$d/$ref" ]] && grep -qE 'Microsoft\.NET\.Sdk\.Razor' "$d/$ref" && return 0
|
|
249
|
+
done < <(grep -oE '<ProjectReference[^>]*Include="[^"]+"' "$1" 2>/dev/null | sed -E 's/.*Include="([^"]+)".*/\1/')
|
|
250
|
+
return 1
|
|
251
|
+
}
|
|
252
|
+
pick_web() { # sets PROJECT from WEB, or prints NONE and exits
|
|
253
|
+
if [[ ${#WEB[@]} -le 1 ]]; then PROJECT="${WEB[0]:-}"; return; fi
|
|
254
|
+
local ui=()
|
|
255
|
+
for p in "${WEB[@]}"; do serves_screens "$p" && ui+=("$p"); done
|
|
256
|
+
if [[ ${#ui[@]} -eq 1 ]]; then
|
|
257
|
+
PROJECT="${ui[0]}"
|
|
258
|
+
echo "tf-verify-boot: ${#WEB[@]} web projects; $PROJECT serves screens (.razor/.cshtml pages or a Razor SDK reference), the other(s) do not — booting it; --project names another"
|
|
259
|
+
else
|
|
260
|
+
write_state web none "" "" "" "" "${#WEB[@]} web projects and no single one serving screens" host "$PLATFORM"
|
|
261
|
+
echo "NONE head=web reason=${#WEB[@]} web projects (${WEB[*]}) and ${#ui[@]} of them serve screens${ui[*]:+ (${ui[*]})}; name one with --project"; exit 2
|
|
262
|
+
fi
|
|
263
|
+
}
|
|
140
264
|
if [[ -n "$PROJECT" ]]; then
|
|
141
265
|
[[ -f "$PROJECT" ]] || { echo "NONE reason=--project $PROJECT does not exist"; exit 3; }
|
|
142
266
|
if [[ -z "$HEAD" ]]; then is_web "$PROJECT" && HEAD=web; [[ -z "$HEAD" ]] && is_maui_win "$PROJECT" && HEAD=windows; fi
|
|
143
267
|
elif [[ -z "$HEAD" ]]; then
|
|
144
|
-
if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web;
|
|
268
|
+
if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web; pick_web
|
|
145
269
|
elif [[ ${#WIN[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN[0]}"; fi
|
|
146
270
|
else
|
|
147
|
-
case "$HEAD" in web)
|
|
271
|
+
case "$HEAD" in web) pick_web ;; windows) PROJECT="${WIN[0]:-}" ;; esac
|
|
148
272
|
fi
|
|
273
|
+
if [[ $DRYRUN -eq 1 ]]; then echo "PICK head=${HEAD:-none} project=${PROJECT:-none}"; exit 0; fi
|
|
149
274
|
case "$HEAD" in
|
|
150
275
|
android|ios|maccatalyst)
|
|
151
276
|
write_state "$HEAD" none "" "" "" "${PROJECT:-}" "no driver for the $HEAD head ships in this framework version" no-driver "$PLATFORM"
|
|
@@ -171,6 +296,98 @@ if [[ "$HEAD" == "web" ]]; then
|
|
|
171
296
|
echo "NONE head=web reason=port $PORT is already in use; stop that process or pass --port"; exit 2
|
|
172
297
|
fi
|
|
173
298
|
URL="http://localhost:$PORT"
|
|
299
|
+
keyed "$PORT"
|
|
300
|
+
# TF-043. `dotnet run` served the shared bin/ and obj/, which the next builder rewrites under a
|
|
301
|
+
# running app: on TfLens, 2026-09-12, four apps side by side, one sent its stylesheet as 200 with
|
|
302
|
+
# 0 bytes, one answered 500, and one ran a dll older than its own source. Each looked healthy and
|
|
303
|
+
# every measurement taken on it was wrong. So the app runs from a copy of its own, published by
|
|
304
|
+
# tf-build.sh, which lets one build run at a time; settings, secrets and files beside the project
|
|
305
|
+
# are read from the project folder, as `dotnet run` reads them.
|
|
306
|
+
if ! grep -q 'Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$PROJECT"; then
|
|
307
|
+
ROOTDIR="$PWD"; RUN="$DIR/run-$PORT"
|
|
308
|
+
rm -rf "$RUN"
|
|
309
|
+
echo "### publish $PROJECT into $RUN" >> "$LOG"
|
|
310
|
+
# Debug unless --config says otherwise: `dotnet publish` alone builds Release, which is not the app
|
|
311
|
+
# `dotnet run` ran, and a Release publish over a Debug-built tree stamped the pages with one scope
|
|
312
|
+
# and the stylesheet with another (TfLens cluster E, in TF-043)
|
|
313
|
+
pub="$(bash "$HERE/tf-build.sh" publish "$PROJECT" -- -o "$RUN" -c "${CONFIG:-Debug}" 2>&1)"; prc=$?
|
|
314
|
+
printf '%s\n' "$pub" >> "$LOG"
|
|
315
|
+
verdict="$(grep -E '^(PASS|FAIL|NOT-RUN)' <<<"$pub" | tail -1)"
|
|
316
|
+
if [[ $prc -eq 1 ]]; then
|
|
317
|
+
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$pub" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
|
|
318
|
+
write_state web none "" "" "tf-build.sh publish" "$PROJECT" "build error: ${first:-$verdict}" build-error "$PLATFORM"
|
|
319
|
+
echo "NONE head=web kind=build-error reason=the code does not build: ${first:-$verdict} (log $LOG)"; exit 2
|
|
320
|
+
elif [[ $prc -ne 0 || ! -f "$RUN/$ASM.dll" ]]; then
|
|
321
|
+
reason="${verdict:-the publish wrote no $ASM.dll into $RUN}"
|
|
322
|
+
write_state web none "" "" "tf-build.sh publish" "$PROJECT" "$reason" host "$PLATFORM"
|
|
323
|
+
echo "NONE head=web kind=host reason=$reason (log $LOG)"; exit 2
|
|
324
|
+
fi
|
|
325
|
+
# what `dotnet run` sets from the launch profile; the environment above all, which picks the
|
|
326
|
+
# settings file and whether the secrets are read
|
|
327
|
+
tf_read_lines LSENV < <(TF_BOOT_ENV="$ENVNAME" python3 - "$PDIR/Properties/launchSettings.json" <<'PY'
|
|
328
|
+
import json, os, sys
|
|
329
|
+
try:
|
|
330
|
+
profiles = json.load(open(sys.argv[1], encoding="utf-8-sig")).get("profiles", {})
|
|
331
|
+
except Exception:
|
|
332
|
+
profiles = {}
|
|
333
|
+
env = next((dict(p.get("environmentVariables") or {}) for p in profiles.values() if p.get("commandName") == "Project"), {})
|
|
334
|
+
env.setdefault("ASPNETCORE_ENVIRONMENT", "Development")
|
|
335
|
+
if os.environ.get("ASPNETCORE_ENVIRONMENT"):
|
|
336
|
+
env["ASPNETCORE_ENVIRONMENT"] = os.environ["ASPNETCORE_ENVIRONMENT"]
|
|
337
|
+
if os.environ.get("TF_BOOT_ENV"):
|
|
338
|
+
env["ASPNETCORE_ENVIRONMENT"] = os.environ["TF_BOOT_ENV"]
|
|
339
|
+
for k, v in env.items():
|
|
340
|
+
print(f"{k}={v}")
|
|
341
|
+
PY
|
|
342
|
+
)
|
|
343
|
+
WIN=""
|
|
344
|
+
case "$verdict" in
|
|
345
|
+
*"via cmd.exe"*|*"via winrun"*|*"via powershell.exe"*)
|
|
346
|
+
WIN=1; WPD="$(winarg "$(wslpath -w "$PDIR")")"; WDLL="$(winarg "$(wslpath -w "$RUN/$ASM.dll")")"
|
|
347
|
+
WWEB=""; [[ -d "$RUN/wwwroot" ]] && WWEB="--webroot $(winarg "$(wslpath -w "$RUN/wwwroot")")"
|
|
348
|
+
sets=""; for kv in ${LSENV[@]+"${LSENV[@]}"}; do sets+="set $kv&& "; done
|
|
349
|
+
nohup cmd.exe /c "cd /d $WPD && ${sets}dotnet $WDLL --urls $URL --contentRoot $WPD $WWEB" >> "$LOG" 2>&1 < /dev/null &
|
|
350
|
+
PID=$!; label="cmd.exe /c dotnet" ;;
|
|
351
|
+
*)
|
|
352
|
+
runner="dotnet"; [[ "$verdict" == *"via ~/.dotnet/dotnet"* ]] && runner="$HOME/.dotnet/dotnet"
|
|
353
|
+
# Lekhak TF-001. A copy run on the WSL side reads user-secrets from ~/.microsoft/usersecrets,
|
|
354
|
+
# while `dotnet user-secrets set` on Windows wrote them to %APPDATA%\Microsoft\UserSecrets:
|
|
355
|
+
# "Required configuration value(s) not set" on an app that starts under `dotnet run`. The
|
|
356
|
+
# secrets reader looks under $APPDATA first on any system, so when only the Windows store
|
|
357
|
+
# holds this project's secrets, the app is pointed at it.
|
|
358
|
+
SID="$(grep -oE '<UserSecretsId>[^<]+' "$PROJECT" 2>/dev/null | head -1 | sed 's/<UserSecretsId>//')"
|
|
359
|
+
if [[ -n "$SID" && ! -f "$HOME/.microsoft/usersecrets/$SID/secrets.json" ]] && [[ "$PLATFORM" == wsl || -n "${TF_WIN_APPDATA:-}" ]]; then
|
|
360
|
+
WAD="${TF_WIN_APPDATA:-}"
|
|
361
|
+
if [[ -z "$WAD" ]] && command -v cmd.exe >/dev/null 2>&1; then
|
|
362
|
+
WAD="$(cmd.exe /c 'echo %APPDATA%' 2>/dev/null | tr -d '\r')"; [[ -n "$WAD" ]] && WAD="$(wslpath -u "$WAD" 2>/dev/null)"
|
|
363
|
+
fi
|
|
364
|
+
if [[ -n "$WAD" && -f "$WAD/Microsoft/UserSecrets/$SID/secrets.json" ]]; then
|
|
365
|
+
LSENV+=("APPDATA=$WAD")
|
|
366
|
+
echo "tf-verify-boot: user-secrets $SID are in the Windows store only; the app reads them from $WAD/Microsoft/UserSecrets"
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
WEBR=(); [[ -d "$RUN/wwwroot" ]] && WEBR=(--webroot "$ROOTDIR/$RUN/wwwroot")
|
|
370
|
+
# the redirections belong to the whole background group and the group becomes the app: a group
|
|
371
|
+
# left waiting on its app kept this script's output open, so a caller reading it waited for ever
|
|
372
|
+
( cd "$PDIR" || exit 1; exec env ${LSENV[@]+"${LSENV[@]}"} nohup "$runner" "$ROOTDIR/$RUN/$ASM.dll" --urls "$URL" --contentRoot "$PWD" ${WEBR[@]+"${WEBR[@]}"} ) \
|
|
373
|
+
>> "$ROOTDIR/$LOG" 2>&1 < /dev/null &
|
|
374
|
+
PID=$!; label="$([[ "$runner" == dotnet ]] && echo dotnet || echo '~/.dotnet/dotnet')" ;;
|
|
375
|
+
esac
|
|
376
|
+
if poll_http "$URL$PROBE" 120 "$PID"; then
|
|
377
|
+
write_state web base "$URL" "$PID" "$label (published copy)" "$PROJECT" "" "" "$PLATFORM"
|
|
378
|
+
set_state run_dir "$(json_escape "$RUN")"
|
|
379
|
+
[[ -n "$WIN" ]] && set_state win_port "$PORT"
|
|
380
|
+
echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT copy=$RUN pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
|
|
381
|
+
fi
|
|
382
|
+
pkill -P "$PID" 2>/dev/null; kill "$PID" 2>/dev/null
|
|
383
|
+
[[ -n "$WIN" ]] && powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
|
|
384
|
+
last="$(grep -vE '^[[:space:]]*$' "$LOG" | tail -3 | tr '\n' ' ' | cut -c1-240)"
|
|
385
|
+
# say what the log says: "no rung brought it up" sent a reader to the build when the app had started
|
|
386
|
+
said=""; grep -q 'Now listening on' "$LOG" && said=" although its log says it is listening"
|
|
387
|
+
write_state web none "" "" "$label (published copy)" "$PROJECT" "the published copy did not answer on $URL$PROBE within 120 s$said" host "$PLATFORM"
|
|
388
|
+
echo "NONE head=web kind=host reason=the published copy of $PROJECT did not answer on $URL$PROBE within 120 s$said; last lines: $last (log $LOG)"; exit 2
|
|
389
|
+
fi
|
|
390
|
+
# a standalone WebAssembly project has no server of its own to publish: `dotnet run` serves it
|
|
174
391
|
rungs=()
|
|
175
392
|
case "$PLATFORM" in
|
|
176
393
|
wsl) command -v dotnet >/dev/null 2>&1 && rungs+=("dotnet"); [[ -x "$HOME/.dotnet/dotnet" ]] && rungs+=("$HOME/.dotnet/dotnet"); command -v cmd.exe >/dev/null 2>&1 && rungs+=("cmd.exe") ;;
|
|
@@ -183,23 +400,26 @@ if [[ "$HEAD" == "web" ]]; then
|
|
|
183
400
|
case "$r" in
|
|
184
401
|
cmd.exe)
|
|
185
402
|
WP="$(winarg "$(wslpath -w "$PROJECT")")"
|
|
186
|
-
nohup cmd.exe /c "dotnet run --project $WP ${CFGARGS[*]} --urls $URL" >> "$LOG" 2>&1 < /dev/null &
|
|
403
|
+
nohup cmd.exe /c "dotnet run --project $WP ${CFGARGS[*]:-} --urls $URL" >> "$LOG" 2>&1 < /dev/null &
|
|
187
404
|
PID=$!; label="cmd.exe /c dotnet" ;;
|
|
188
405
|
*)
|
|
189
|
-
ASPNETCORE_ENVIRONMENT="${ASPNETCORE_ENVIRONMENT:-Development}" nohup "$r" run --project "$PROJECT" "${CFGARGS[@]}" --urls "$URL" >> "$LOG" 2>&1 < /dev/null &
|
|
406
|
+
ASPNETCORE_ENVIRONMENT="${ENVNAME:-${ASPNETCORE_ENVIRONMENT:-Development}}" nohup "$r" run --project "$PROJECT" ${CFGARGS[@]+"${CFGARGS[@]}"} --urls "$URL" >> "$LOG" 2>&1 < /dev/null &
|
|
190
407
|
PID=$!; label="$r" ;;
|
|
191
408
|
esac
|
|
192
409
|
tried+=("$label")
|
|
193
|
-
if poll_http "$URL
|
|
410
|
+
if poll_http "$URL$PROBE" 120 "$PID"; then
|
|
194
411
|
WP_PORT=""; [[ "$r" == "cmd.exe" ]] && WP_PORT="$PORT"
|
|
195
412
|
write_state web base "$URL" "$PID" "$label" "$PROJECT" "" "" "$PLATFORM"
|
|
196
|
-
[[ -n "$WP_PORT" ]] &&
|
|
197
|
-
echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT pid=$PID log=$LOG"; exit 0
|
|
413
|
+
[[ -n "$WP_PORT" ]] && set_state win_port "$WP_PORT"
|
|
414
|
+
echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
|
|
198
415
|
fi
|
|
199
416
|
pkill -P "$PID" 2>/dev/null; kill "$PID" 2>/dev/null
|
|
417
|
+
# killing cmd.exe on this side leaves the Windows-side app running and holding the build output
|
|
418
|
+
# (TF-034): stop whatever listens on THIS start's port, never by program name
|
|
419
|
+
[[ "$r" == "cmd.exe" ]] && powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
|
|
200
420
|
seg="$(awk '/^### rung: /{p=0} index($0,"### rung: '"$r"'")==1{p=1} p' "$LOG")"
|
|
201
421
|
if grep -qE "$CODE_ERR" <<<"$seg" && ! grep -qE "$WRONG_RUNG" <<<"$seg"; then
|
|
202
|
-
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's
|
|
422
|
+
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
|
|
203
423
|
write_state web none "" "" "$label" "$PROJECT" "build error: $first" build-error "$PLATFORM"
|
|
204
424
|
echo "NONE head=web kind=build-error reason=the code does not build via $label: $first (log $LOG)"; exit 2
|
|
205
425
|
fi
|
|
@@ -212,10 +432,10 @@ fi
|
|
|
212
432
|
[[ "$PLATFORM" == "wsl" || "$PLATFORM" == "windows" ]] || { write_state windows none "" "" "" "$PROJECT" "a Windows head runs only from WSL or Windows" host "$PLATFORM"; echo "NONE head=windows reason=a Windows head runs only from WSL or Windows, this is $PLATFORM"; exit 2; }
|
|
213
433
|
command -v cmd.exe >/dev/null 2>&1 || { write_state windows none "" "" "" "$PROJECT" "cmd.exe not reachable" host "$PLATFORM"; echo "NONE head=windows reason=cmd.exe is not reachable from this shell"; exit 2; }
|
|
214
434
|
TFM="$(grep -oE 'net[0-9.]+-windows[0-9.]*' "$PROJECT" | head -1)"
|
|
215
|
-
CDP_PORT=9222; RELAY_PORT="${PORT:-9223}"
|
|
435
|
+
CDP_PORT=9222; RELAY_PORT="${PORT:-9223}"; keyed "$RELAY_PORT"
|
|
216
436
|
WP="$(winarg "$(wslpath -w "$PROJECT")")"; RELAY="$(wslpath -w "$HERE/tf-cdp-relay.ps1")"
|
|
217
437
|
echo "### windows head: $PROJECT -f $TFM" >> "$LOG"
|
|
218
|
-
nohup cmd.exe /c "set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=$CDP_PORT&& dotnet run --project $WP -f $TFM ${CFGARGS[*]}" >> "$LOG" 2>&1 < /dev/null &
|
|
438
|
+
nohup cmd.exe /c "set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=$CDP_PORT&& dotnet run --project $WP -f $TFM ${CFGARGS[*]:-}" >> "$LOG" 2>&1 < /dev/null &
|
|
219
439
|
APP_PID=$!
|
|
220
440
|
nohup powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$RELAY" -ListenPort "$RELAY_PORT" -TargetPort "$CDP_PORT" >> "$DIR/relay.log" 2>&1 < /dev/null &
|
|
221
441
|
RELAY_PID=$!
|
|
@@ -230,7 +450,7 @@ while [[ $i -lt 300 && -z "$URL" ]]; do
|
|
|
230
450
|
if ! kill -0 "$APP_PID" 2>/dev/null; then
|
|
231
451
|
seg="$(cat "$LOG")"
|
|
232
452
|
if grep -qE "$CODE_ERR" <<<"$seg"; then
|
|
233
|
-
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's
|
|
453
|
+
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
|
|
234
454
|
kill "$RELAY_PID" 2>/dev/null
|
|
235
455
|
write_state windows none "" "" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "build error: $first" build-error "$PLATFORM"
|
|
236
456
|
echo "NONE head=windows kind=build-error reason=the code does not build: $first (log $LOG)"; exit 2
|
|
@@ -245,6 +465,6 @@ if [[ -z "$URL" ]]; then
|
|
|
245
465
|
echo "NONE head=windows kind=host reason=the app's DevTools port never answered on ${HOSTS[*]}:$RELAY_PORT within 300 s (log $LOG, relay $DIR/relay.log)"; exit 2
|
|
246
466
|
fi
|
|
247
467
|
write_state windows cdp "$URL" "$APP_PID $RELAY_PID" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "" "" "$PLATFORM"
|
|
248
|
-
|
|
468
|
+
set_state win_image "\"$ASM.exe\""
|
|
249
469
|
echo "BOOTED head=windows mode=cdp url=$URL project=$PROJECT tfm=$TFM pids=$APP_PID,$RELAY_PID image=$ASM.exe log=$LOG"
|
|
250
470
|
exit 0
|
|
@@ -39,6 +39,26 @@ v = json.load(open(os.path.join(d, "verdicts.json"), encoding="utf-8"))
|
|
|
39
39
|
run_id = started or v.get("started") or ""
|
|
40
40
|
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
41
41
|
|
|
42
|
+
# A verify chained inside another command is handed that command's start as "the step-0 time", and
|
|
43
|
+
# its record then covered the caller's whole window, so the caller's own record was refused (TF-052).
|
|
44
|
+
# This run's start is its own marker's; with no verify-phase marker (step 0 skipped), when the list
|
|
45
|
+
# was written.
|
|
46
|
+
mk = {}
|
|
47
|
+
try:
|
|
48
|
+
pj = os.path.join(".tfcore", ".session", "phase.json")
|
|
49
|
+
if datetime.datetime.now().timestamp() - os.path.getmtime(pj) < 24 * 3600:
|
|
50
|
+
mk = json.load(open(pj, encoding="utf-8")) or {}
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
own, caller = "", (mk.get("outer") or {}).get("cmd") or "an earlier command"
|
|
54
|
+
if mk.get("cmd") == "verify-phase":
|
|
55
|
+
own = mk.get("started") or ""
|
|
56
|
+
elif mk.get("started") and mk.get("started") == run_id and os.path.isfile(os.path.join(d, "list.json")):
|
|
57
|
+
own, caller = datetime.datetime.fromtimestamp(os.path.getmtime(os.path.join(d, "list.json")), datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), mk.get("cmd")
|
|
58
|
+
if own and run_id and own > run_id:
|
|
59
|
+
print(f"tf-verify-emit: {run_id} is when {caller} started; this verify started {own} and is recorded from then")
|
|
60
|
+
run_id = own
|
|
61
|
+
|
|
42
62
|
def emit(stream, rec):
|
|
43
63
|
p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
|
|
44
64
|
msg = (p.stdout + p.stderr).strip()
|
|
@@ -4,17 +4,30 @@
|
|
|
4
4
|
# bash .tfcore/utils/tf-verify-env.sh [--check]
|
|
5
5
|
#
|
|
6
6
|
# Ensures, in the current project: package.json; the playwright and @playwright/test packages;
|
|
7
|
-
# the Chromium browser; playwright.config.ts with its output pinned under tests/.artifacts
|
|
7
|
+
# the Chromium browser; playwright.config.ts with its output pinned under tests/.artifacts/ and its
|
|
8
|
+
# address read from BASE_URL, which tf-verify-tests.sh --base sets (TF-031);
|
|
8
9
|
# the .gitignore lines for everything this tooling generates. --check reports and changes nothing.
|
|
9
10
|
# Prints one line per item and READY at the end, or NOT-READY with the one command the owner
|
|
10
11
|
# must run once (only when a browser install needs sudo and sudo is unavailable).
|
|
11
12
|
# Exit 0 ready · 1 not ready · 2 node or npm missing.
|
|
12
13
|
set -u
|
|
14
|
+
source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh" # tf_sed_inplace: GNU and BSD sed
|
|
13
15
|
CHECK=0; [[ "${1:-}" == "--check" ]] && CHECK=1
|
|
14
16
|
command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 || { echo "NOT-READY node and npm are required"; exit 2; }
|
|
15
17
|
ok=1
|
|
16
18
|
|
|
17
19
|
say() { echo " $*"; }
|
|
20
|
+
# cfg_sub FILE ALL ERE TEXT: replace the first match of ERE on each line (ALL=1), or only on the
|
|
21
|
+
# first line that matches (ALL=0, what GNU sed's 0,/re/ address did), with TEXT; \n in TEXT is a
|
|
22
|
+
# newline. awk, so it reads the same on GNU and BSD systems.
|
|
23
|
+
cfg_sub() {
|
|
24
|
+
local tmp
|
|
25
|
+
tmp="$(mktemp "$1.XXXXXX")" || return 1
|
|
26
|
+
awk -v all="$2" -v re="$3" -v rep="$4" '
|
|
27
|
+
(all == 1 || !done) && match($0, re) { $0 = substr($0, 1, RSTART - 1) rep substr($0, RSTART + RLENGTH); done = 1 }
|
|
28
|
+
{ print }' "$1" > "$tmp" && cat "$tmp" > "$1"
|
|
29
|
+
rm -f "$tmp"
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
# package.json
|
|
20
33
|
if [[ ! -f package.json ]]; then
|
|
@@ -55,7 +68,7 @@ export default defineConfig({
|
|
|
55
68
|
testDir: '"'"'./tests/verify'"'"',
|
|
56
69
|
outputDir: '"'"'./tests/.artifacts/test-results'"'"',
|
|
57
70
|
reporter: '"'"'line'"'"',
|
|
58
|
-
use: { headless: true, screenshot: '"'"'only-on-failure'"'"', trace: '"'"'retain-on-failure'"'"' },
|
|
71
|
+
use: { baseURL: process.env.BASE_URL, headless: true, screenshot: '"'"'only-on-failure'"'"', trace: '"'"'retain-on-failure'"'"' },
|
|
59
72
|
});'
|
|
60
73
|
if [[ ! -f playwright.config.ts ]]; then
|
|
61
74
|
if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: missing"; ok=0; else printf '%s\n' "$CFG" > playwright.config.ts; say "playwright.config.ts: written"; fi
|
|
@@ -63,13 +76,29 @@ elif ! grep -q "tests/.artifacts" playwright.config.ts; then
|
|
|
63
76
|
if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: outputDir not under tests/.artifacts"; ok=0
|
|
64
77
|
else
|
|
65
78
|
if grep -q "outputDir" playwright.config.ts; then
|
|
66
|
-
|
|
79
|
+
tf_sed_inplace "s#outputDir:[^,]*,#outputDir: './tests/.artifacts/test-results',#" playwright.config.ts
|
|
67
80
|
else
|
|
68
|
-
|
|
81
|
+
cfg_sub playwright.config.ts 1 "defineConfig[(][{]" "defineConfig({\n outputDir: './tests/.artifacts/test-results',"
|
|
69
82
|
fi
|
|
70
83
|
say "playwright.config.ts: outputDir pinned under tests/.artifacts"
|
|
71
84
|
fi
|
|
72
85
|
else say "playwright.config.ts: present, output pinned"; fi
|
|
86
|
+
# the address: tf-verify-tests.sh --base passes it as BASE_URL, which Playwright never reads by
|
|
87
|
+
# itself; a config that ignores it tests whatever is on its default port, an older build included
|
|
88
|
+
# (TfLens TF-031, 2026-09-11). A baseURL of the project's own is kept, as the fallback.
|
|
89
|
+
if [[ -f playwright.config.ts ]] && ! grep -q "BASE_URL" playwright.config.ts; then
|
|
90
|
+
if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: does not read BASE_URL, so --base is ignored"; ok=0
|
|
91
|
+
elif grep -qE "baseURL[\"']?[[:space:]]*:" playwright.config.ts; then
|
|
92
|
+
tf_sed_inplace -E "s#(baseURL[\"']?[[:space:]]*:[[:space:]]*)#\1process.env.BASE_URL || #" playwright.config.ts
|
|
93
|
+
say "playwright.config.ts: baseURL now reads BASE_URL first, its own address after"
|
|
94
|
+
elif grep -qE "use[[:space:]]*:[[:space:]]*\{" playwright.config.ts; then
|
|
95
|
+
cfg_sub playwright.config.ts 0 "use[[:space:]]*:[[:space:]]*[{]" "use: { baseURL: process.env.BASE_URL,"
|
|
96
|
+
say "playwright.config.ts: baseURL added, read from BASE_URL"
|
|
97
|
+
else
|
|
98
|
+
cfg_sub playwright.config.ts 0 "defineConfig[(][{]" "defineConfig({\n use: { baseURL: process.env.BASE_URL },"
|
|
99
|
+
say "playwright.config.ts: baseURL added, read from BASE_URL"
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
73
102
|
|
|
74
103
|
# .gitignore
|
|
75
104
|
missing=()
|