@techierathore/techieflow 1.1.0 → 1.1.1
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/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +15 -7
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/guard-db.sh +106 -9
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +4 -4
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +1 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +6 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -0
- package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/html-render-shell.md +2 -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-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-build-list.py +16 -1
- package/.tfcore/utils/tf-build.sh +75 -0
- package/.tfcore/utils/tf-checklist-edit.py +21 -3
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +6 -2
- package/.tfcore/utils/tf-doc-check.py +83 -21
- package/.tfcore/utils/tf-doc-tests.sh +40 -3
- package/.tfcore/utils/tf-emit.sh +40 -10
- package/.tfcore/utils/tf-fix-close.sh +13 -7
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +355 -27
- package/.tfcore/utils/tf-mockup-parity.sh +14 -3
- package/.tfcore/utils/tf-phase.sh +24 -4
- package/.tfcore/utils/tf-render-html.py +19 -3
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-facts.py +90 -15
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +12 -1
- package/.tfcore/utils/tf-verify-boot.sh +274 -19
- package/.tfcore/utils/tf-verify-emit.sh +37 -17
- package/.tfcore/utils/tf-verify-list.py +219 -20
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +178 -20
- package/.tfcore/utils/tf-verify-screens.sh +17 -4
- package/.tfcore/utils/tf-verify-tests.sh +107 -17
- package/.tfcore/utils/tf-verify-verdict.py +118 -15
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_feedback.py +79 -16
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +2 -2
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.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-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
package/.tfcore/utils/tf-emit.sh
CHANGED
|
@@ -1015,16 +1015,34 @@ def _window_opencode(t0, t1):
|
|
|
1015
1015
|
return None
|
|
1016
1016
|
dbp = ptr.get("db_path")
|
|
1017
1017
|
sid = ptr.get("session_id")
|
|
1018
|
-
if not dbp or not
|
|
1018
|
+
if not dbp or not os.path.isfile(dbp):
|
|
1019
1019
|
return None
|
|
1020
1020
|
try:
|
|
1021
1021
|
import sqlite3
|
|
1022
1022
|
db = sqlite3.connect("file:%s?mode=ro" % dbp, uri=True)
|
|
1023
|
-
|
|
1023
|
+
# OpenCode 2 keeps sessions in session_v2 and messages in session_message; the old tables
|
|
1024
|
+
# stopped on 2026-09-27, so every OpenCode record since read tokens_scope "none". Both are read.
|
|
1025
|
+
tables = {r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
1026
|
+
pairs = db.execute("SELECT id, parent_id FROM session").fetchall() if "session" in tables else []
|
|
1027
|
+
if "session_v2" in tables:
|
|
1028
|
+
pairs += db.execute("SELECT id, parent_id FROM session_v2").fetchall()
|
|
1024
1029
|
kids = {}
|
|
1025
1030
|
for i, p in pairs:
|
|
1026
1031
|
kids.setdefault(p, []).append(i)
|
|
1027
|
-
|
|
1032
|
+
# The roots: the pointer's session, and every root session OpenCode 2 recorded in THIS project's
|
|
1033
|
+
# folder. Each *develop-end-to-end phase runs in a new session and the plugin's pointer kept the
|
|
1034
|
+
# first one, so the later phases read "none" (2026-10-08). Messages are still taken only inside
|
|
1035
|
+
# the record's own window.
|
|
1036
|
+
roots = {sid} if sid else set()
|
|
1037
|
+
if "session_v2" in tables:
|
|
1038
|
+
here = os.path.realpath(root)
|
|
1039
|
+
for i, p_, dr in db.execute("SELECT id, parent_id, directory FROM session_v2"):
|
|
1040
|
+
if not p_ and dr and os.path.realpath(dr) == here:
|
|
1041
|
+
roots.add(i)
|
|
1042
|
+
if not roots:
|
|
1043
|
+
db.close()
|
|
1044
|
+
return None
|
|
1045
|
+
tree, queue = set(roots), list(roots)
|
|
1028
1046
|
while queue:
|
|
1029
1047
|
for c in kids.get(queue.pop(), []):
|
|
1030
1048
|
if c not in tree:
|
|
@@ -1036,15 +1054,20 @@ def _window_opencode(t0, t1):
|
|
|
1036
1054
|
# Same rule as the Claude branch: a CHILD session counts as a subagent run
|
|
1037
1055
|
# for THIS window only if it produced output inside it.
|
|
1038
1056
|
child_out = {}
|
|
1039
|
-
|
|
1040
|
-
|
|
1057
|
+
rows = []
|
|
1058
|
+
if "message" in tables:
|
|
1059
|
+
rows += db.execute("SELECT session_id, data FROM message WHERE time_created BETWEEN ? AND ?", (t0, t1)).fetchall()
|
|
1060
|
+
if "session_message" in tables:
|
|
1061
|
+
rows += [(a, b) for a, b in db.execute(
|
|
1062
|
+
"SELECT session_id, data FROM session_message WHERE type = 'assistant' AND time_created BETWEEN ? AND ?", (t0, t1))]
|
|
1063
|
+
for msid, data in rows:
|
|
1041
1064
|
if msid not in tree:
|
|
1042
1065
|
continue
|
|
1043
1066
|
try:
|
|
1044
1067
|
d = json.loads(data)
|
|
1045
1068
|
except Exception:
|
|
1046
1069
|
continue
|
|
1047
|
-
if d.get("role")
|
|
1070
|
+
if d.get("role") not in (None, "assistant"):
|
|
1048
1071
|
continue
|
|
1049
1072
|
t = d.get("tokens") or {}
|
|
1050
1073
|
cache = t.get("cache") or {}
|
|
@@ -1054,9 +1077,11 @@ def _window_opencode(t0, t1):
|
|
|
1054
1077
|
tot["cr"] += cache.get("read") or 0
|
|
1055
1078
|
tot["cw"] += cache.get("write") or 0
|
|
1056
1079
|
cost += d.get("cost") or 0
|
|
1057
|
-
if msid
|
|
1080
|
+
if msid not in roots and o:
|
|
1058
1081
|
child_out[msid] = child_out.get(msid, 0) + o
|
|
1059
|
-
|
|
1082
|
+
mo = d.get("model") if isinstance(d.get("model"), dict) else {}
|
|
1083
|
+
prov = d.get("providerID") or mo.get("providerID") or ""
|
|
1084
|
+
m = (prov + "/" if prov else "") + (d.get("modelID") or mo.get("id") or "")
|
|
1060
1085
|
if m:
|
|
1061
1086
|
models[m] = models.get(m, 0) + o
|
|
1062
1087
|
db.close()
|
|
@@ -1463,7 +1488,7 @@ _ALLOWED = {
|
|
|
1463
1488
|
"routed", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
1464
1489
|
"cost_usd", "tokens_scope", "attempt", "subagent_runs", "tokens_out_subagents",
|
|
1465
1490
|
"model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts",
|
|
1466
|
-
"billing_mode", "cost_source", "fallback_from"},
|
|
1491
|
+
"billing_mode", "cost_source", "fallback_from", "app_phase"},
|
|
1467
1492
|
# A correction, not a deletion: it names a run record that is wrong by the pair every
|
|
1468
1493
|
# run carries (`cmd` + `started`) and says why, so every figure can skip it while the
|
|
1469
1494
|
# record itself stays where it is. Written by --void-run, which refuses one that names
|
|
@@ -1474,7 +1499,7 @@ _ALLOWED = {
|
|
|
1474
1499
|
"tier", "tier_model", "model", "models", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1475
1500
|
"tokens_cache_write", "cost_usd", "tokens_scope", "model_tokens_out", "routed",
|
|
1476
1501
|
"subagent_runs", "tokens_out_subagents", "session_id",
|
|
1477
|
-
"billing_mode", "cost_source", "fallback_from"},
|
|
1502
|
+
"billing_mode", "cost_source", "fallback_from", "app_phase"},
|
|
1478
1503
|
("sessions", "session"): {"session_id", "model", "duration_s", "input_tokens", "output_tokens",
|
|
1479
1504
|
"cache_read_tokens", "cache_creation_tokens", "cost_usd", "children_sessions"},
|
|
1480
1505
|
("commits", "commit"): {"sha", "files", "insertions", "deletions", "subject_prefix", "branch"},
|
|
@@ -1519,6 +1544,11 @@ def _shape_ok(rec):
|
|
|
1519
1544
|
if kind == "run" and rec.get("build_result") not in (None, "pass", "fail", "not-run"):
|
|
1520
1545
|
sys.stdout.write("tf-emit: REFUSED — build_result must be pass, fail or not-run (SCHEMA.md §2); got %r. Put the detail in the checklist Remarks or Known blockers, not here. Nothing was appended.\n" % (rec.get("build_result"),))
|
|
1521
1546
|
return False
|
|
1547
|
+
# one REQ's verdict, never a suite note: Lekhak hand-wrote four {"gate":"verify-suite","result":…}
|
|
1548
|
+
# records with neither, and the report counted each as a failure (TF-020)
|
|
1549
|
+
if kind == "gate" and not rec.get("backfilled") and not (rec.get("req_id") and rec.get("verdict")):
|
|
1550
|
+
sys.stdout.write("tf-emit: REFUSED — a gate record is one REQ's verdict and needs both req_id and verdict (SCHEMA.md §3). A suite result (unit tests, the whole browser run) belongs in the run record's notes, not here. Nothing was appended.\n")
|
|
1551
|
+
return False
|
|
1522
1552
|
if kind == "miss" and not rec.get("miss_id"):
|
|
1523
1553
|
sys.stdout.write("tf-emit: REFUSED — a miss record needs a miss_id; get one with: bash .tfcore/utils/tf-emit.sh --next-miss-id. Nothing was appended.\n")
|
|
1524
1554
|
return False
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# bash .tfcore/utils/tf-fix-close.sh <App> [--started <ISO>] [--reqs REQ-UI-009,REQ-FN-014]
|
|
5
5
|
# [--subagents trblazeui,builder] [--build pass|fail|not-run] [--files N]
|
|
6
|
+
# [--cmd triage-and-fix] # the command the run record is filed under
|
|
6
7
|
# bash .tfcore/utils/tf-fix-close.sh <App> --misses MISS-App-20260901-06,MISS-App-20260830-03 \
|
|
7
8
|
# --fix-cmd amend-docs [--verdict Verified]
|
|
8
9
|
#
|
|
@@ -24,13 +25,16 @@
|
|
|
24
25
|
# re-verify). A row no verify graded gets no miss-fix and is listed. A row with no open miss is listed,
|
|
25
26
|
# not invented: a fix on a defect nobody logged is a triage gap, so open it first with tf-triage.sh
|
|
26
27
|
# demote or tf-log-miss.sh, then re-run this.
|
|
28
|
+
# --cmd names the command the run record is filed under (default fix-issues): *triage-and-fix passes
|
|
29
|
+
# --cmd triage-and-fix, so its run is counted as itself and no second record over the same window is
|
|
30
|
+
# needed, which the overlap rule refused (TrBlazeUI TF-004). The miss-fix records keep fix_cmd fix-issues.
|
|
27
31
|
# --reqs defaults to every row those verifies graded. The start defaults to the fix's marker, or its
|
|
28
32
|
# "outer" entry when a chained verify replaced it. Telemetry never blocks: refusals are printed, exit 0.
|
|
29
33
|
set -u
|
|
30
34
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
31
35
|
[[ $# -ge 1 && "$1" != "-h" && "$1" != "--help" ]] || { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 3; }
|
|
32
36
|
APP="$1"; shift
|
|
33
|
-
STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""; MISSES=""; FIXCMD="fix-issues"; VERDICT=""
|
|
37
|
+
STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""; MISSES=""; FIXCMD="fix-issues"; VERDICT=""; RUNCMD="fix-issues"
|
|
34
38
|
while [[ $# -gt 0 ]]; do
|
|
35
39
|
case "$1" in
|
|
36
40
|
--started) STARTED="${2:-}"; shift 2 ;;
|
|
@@ -41,6 +45,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
41
45
|
--misses) MISSES="${2:-}"; shift 2 ;;
|
|
42
46
|
--fix-cmd) FIXCMD="${2:-}"; shift 2 ;;
|
|
43
47
|
--verdict) VERDICT="${2:-}"; shift 2 ;;
|
|
48
|
+
--cmd) RUNCMD="${2:-}"; shift 2 ;;
|
|
44
49
|
*) echo "tf-fix-close: unknown argument $1" >&2; exit 3 ;;
|
|
45
50
|
esac
|
|
46
51
|
done
|
|
@@ -51,9 +56,10 @@ except Exception: m={}
|
|
|
51
56
|
o=m.get('outer') or {}
|
|
52
57
|
print(m.get('started','') if m.get('cmd')!='verify-phase' or not o.get('started') else o['started'])" 2>/dev/null)"
|
|
53
58
|
TF_APP="$APP" TF_STARTED="$STARTED" TF_REQS="$REQS" TF_SUBS="$SUBS" TF_BUILD="$BUILD" TF_FILES="$FILES" \
|
|
54
|
-
TF_MISSES="$MISSES" TF_FIXCMD="$FIXCMD" TF_VERDICT="$VERDICT" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
|
|
59
|
+
TF_MISSES="$MISSES" TF_FIXCMD="$FIXCMD" TF_RUNCMD="$RUNCMD" TF_VERDICT="$VERDICT" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
|
|
55
60
|
import datetime, json, os, subprocess
|
|
56
61
|
EMIT = os.environ["TF_EMIT"]; app = os.environ["TF_APP"]; started = os.environ["TF_STARTED"]
|
|
62
|
+
cmd = os.environ.get("TF_RUNCMD") or "fix-issues"
|
|
57
63
|
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
58
64
|
def q(*a): return subprocess.run(["bash", EMIT, *a], capture_output=True, text=True).stdout.strip()
|
|
59
65
|
def emit(stream, rec):
|
|
@@ -121,7 +127,7 @@ inner, voided = [], set()
|
|
|
121
127
|
for r in jsonl("runs.jsonl"):
|
|
122
128
|
if r.get("kind") == "run-void":
|
|
123
129
|
voided.add((r.get("cmd"), r.get("started")))
|
|
124
|
-
elif r.get("kind", "run") == "run" and r.get("app") == app and r.get("cmd")
|
|
130
|
+
elif r.get("kind", "run") == "run" and r.get("app") == app and r.get("cmd") not in ("fix-issues", cmd) and not r.get("backfilled") \
|
|
125
131
|
and started and (r.get("started") or "") >= started and (r.get("ended") or "") and r["ended"] <= now:
|
|
126
132
|
inner.append((r["started"], r["ended"], r.get("cmd")))
|
|
127
133
|
inner = sorted(w for w in inner if (w[2], w[0]) not in voided)
|
|
@@ -130,11 +136,11 @@ for s, e, _c in inner:
|
|
|
130
136
|
if t and s > t: gaps.append((t, s))
|
|
131
137
|
t = max(t, e) if t else e
|
|
132
138
|
if not started or not gaps or t < now: gaps.append((t, now))
|
|
133
|
-
existing = {r.get("started") for r in jsonl("runs.jsonl") if r.get("kind", "run") == "run" and r.get("cmd") ==
|
|
139
|
+
existing = {r.get("started") for r in jsonl("runs.jsonl") if r.get("kind", "run") == "run" and r.get("cmd") == cmd}
|
|
134
140
|
wrote = had = 0
|
|
135
141
|
for i, (s, e) in enumerate(gaps):
|
|
136
142
|
first = i == 0
|
|
137
|
-
run = {"kind": "run", "app": app, "cmd":
|
|
143
|
+
run = {"kind": "run", "app": app, "cmd": cmd, "mode": "fix", "ended": e, "reqs_touched": reqs if first else [],
|
|
138
144
|
"reqs_count": len(reqs) if first else 0, "subagents": subs if first else [],
|
|
139
145
|
"files_written": (int(files) if files.isdigit() else len(reqs)) if first else 0, "build_result": os.environ["TF_BUILD"]}
|
|
140
146
|
if s: run["started"] = s
|
|
@@ -143,7 +149,7 @@ for i, (s, e) in enumerate(gaps):
|
|
|
143
149
|
had += 1; continue
|
|
144
150
|
wrote += emit("runs", run)
|
|
145
151
|
if inner:
|
|
146
|
-
print(f"
|
|
152
|
+
print(f"{cmd}: recorded around {len(inner)} chained run(s) ({', '.join(sorted({c for _s, _e, c in inner}))}) in {len(gaps)} segment(s)")
|
|
147
153
|
closed, none, unverified = 0, [], []
|
|
148
154
|
for rid in reqs:
|
|
149
155
|
if rid not in rows:
|
|
@@ -158,7 +164,7 @@ for rid in reqs:
|
|
|
158
164
|
if started: rec["fix_run_id"] = started
|
|
159
165
|
if emit("misses", rec): closed += 1
|
|
160
166
|
state = "already there" if had == len(gaps) else "written" if wrote + had == len(gaps) else "not written"
|
|
161
|
-
print(f"
|
|
167
|
+
print(f"{cmd}: run record {state}; {closed} miss-fix record(s) with the verifier's verdict"
|
|
162
168
|
+ (f"; no open miss on {', '.join(none)} (log it first, then re-run)" if none else "")
|
|
163
169
|
+ (f"; no verify graded {', '.join(unverified)} during this fix, so its miss stays open with no miss-fix" if unverified else ""))
|
|
164
170
|
PY
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-maccatalyst-check.sh — the Mac Catalyst rules a MAUI project must meet before its Mac app can
|
|
3
|
+
# start (coding-standards-dotnet.md §9; 2026-10-07). Reads files only: runs the same on Windows (WSL,
|
|
4
|
+
# Git Bash), Linux and a Mac, so a team without a Mac finds these before the Mac build does.
|
|
5
|
+
#
|
|
6
|
+
# bash .tfcore/utils/tf-maccatalyst-check.sh [<csproj> ...] (default: every project with a
|
|
7
|
+
# net*-maccatalyst target)
|
|
8
|
+
#
|
|
9
|
+
# FAIL the scene manifest: Platforms/MacCatalyst/Info.plist has no UIApplicationSceneManifest, or
|
|
10
|
+
# names a scene delegate no class in the project registers as a MauiUISceneDelegate. On
|
|
11
|
+
# macOS 27 such an app quits at its first window (the stock `dotnet new maui` template has none).
|
|
12
|
+
# WARN the keychain: an entitlements file asks for keychain-access-groups and the project names no
|
|
13
|
+
# provisioning profile and no Debug entitlements file without it. macOS refuses to start a
|
|
14
|
+
# locally signed (ad hoc) build that asks for it ("Launch failed", POSIX 163).
|
|
15
|
+
# WARN zoom (§9b): a project drawing its screens in a BlazorWebView, Mac or Windows head, whose code has
|
|
16
|
+
# no zoom wiring for that head: Mac, WKWebView.PageZoom and a "Zoom In" menu command; Windows,
|
|
17
|
+
# WebView2's IsZoomControlEnabled set by the app; either, the level kept through Preferences. Copy
|
|
18
|
+
# .tfcore/standards/blazor-zoom.cs.txt. A WARN, so tf-verify-boot.sh still boots such a head.
|
|
19
|
+
# Prints one line per finding and an OK line per clean project. A Windows-only MAUI project is read
|
|
20
|
+
# for the zoom rule alone.
|
|
21
|
+
# Exit 0 no FAIL (WARNs allowed) · 1 a FAIL · 3 usage.
|
|
22
|
+
set -u
|
|
23
|
+
case "${1:-}" in -h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
|
|
24
|
+
|
|
25
|
+
PROJECTS=("$@")
|
|
26
|
+
if [[ ${#PROJECTS[@]} -eq 0 ]]; then
|
|
27
|
+
while IFS= read -r p; do PROJECTS+=("$p"); done < <(
|
|
28
|
+
find . -maxdepth 5 -name '*.csproj' -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' \
|
|
29
|
+
-not -path './tests/.artifacts/*' 2>/dev/null | sed 's#^\./##' | sort)
|
|
30
|
+
fi
|
|
31
|
+
fails=0; seen=0
|
|
32
|
+
for P in ${PROJECTS[@]+"${PROJECTS[@]}"}; do
|
|
33
|
+
[[ -f "$P" ]] || { echo "tf-maccatalyst-check: $P does not exist" >&2; exit 3; }
|
|
34
|
+
grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$P" || continue
|
|
35
|
+
mac=0; win=0
|
|
36
|
+
grep -qE 'net[0-9.]+-maccatalyst' "$P" && mac=1
|
|
37
|
+
grep -qE 'net[0-9.]+-windows' "$P" && win=1
|
|
38
|
+
[[ $mac -eq 1 || $win -eq 1 ]] || continue
|
|
39
|
+
seen=$((seen+1)); D="$(dirname "$P")"; M="$D/Platforms/MacCatalyst"; found=0
|
|
40
|
+
# --- zoom in a Blazor Hybrid head (§9b): read before the Mac-only rules, which a Windows-only project skips
|
|
41
|
+
CODE=(-rqs --include='*.cs' --exclude-dir=bin --exclude-dir=obj)
|
|
42
|
+
if grep "${CODE[@]}" --include='*.xaml' -E '<BlazorWebView|new BlazorWebView' "$D" \
|
|
43
|
+
|| grep -qs 'Microsoft.AspNetCore.Components.WebView.Maui' "$P"; then
|
|
44
|
+
heads=()
|
|
45
|
+
if [[ $mac -eq 1 ]] && ! { grep "${CODE[@]}" 'PageZoom' "$D" && grep "${CODE[@]}" '"Zoom In"' "$D"; }; then heads+=("Mac (no WKWebView.PageZoom and View › Zoom In)"); fi
|
|
46
|
+
if [[ $win -eq 1 ]] && ! grep "${CODE[@]}" 'IsZoomControlEnabled' "$D"; then heads+=("Windows (WebView2's zoom not handled)"); fi
|
|
47
|
+
if [[ ${#heads[@]} -eq 0 ]] && ! grep -rlis --include='*.cs' --exclude-dir=bin --exclude-dir=obj 'zoom' "$D" | xargs grep -ls 'Preferences' >/dev/null 2>&1; then heads+=("the level is not kept in Preferences"); fi
|
|
48
|
+
if [[ ${#heads[@]} -gt 0 ]]; then
|
|
49
|
+
echo "WARN $P: a Blazor Hybrid head without OS zoom: $(IFS=';'; echo "${heads[*]}" | sed 's/;/; /g'). Cmd/Ctrl+=, -, 0 must zoom 50% to 300% and keep the level; copy .tfcore/standards/blazor-zoom.cs.txt (coding-standards-dotnet.md §9b)"
|
|
50
|
+
found=1
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
if [[ $mac -eq 0 ]]; then
|
|
54
|
+
[[ $found -eq 0 ]] && echo "OK $P: zoom wired (no Mac Catalyst target)"
|
|
55
|
+
continue
|
|
56
|
+
fi
|
|
57
|
+
# --- the scene manifest and its delegate
|
|
58
|
+
PL="$M/Info.plist"
|
|
59
|
+
if [[ ! -f "$PL" ]]; then
|
|
60
|
+
echo "FAIL $P: $PL is missing"; fails=$((fails+1)); found=1
|
|
61
|
+
elif ! grep -q 'UIApplicationSceneManifest' "$PL"; then
|
|
62
|
+
echo "FAIL $P: $PL has no UIApplicationSceneManifest; on macOS 27 the app quits at its first window. Add the manifest and a SceneDelegate : MauiUISceneDelegate (coding-standards-dotnet.md §9)"
|
|
63
|
+
fails=$((fails+1)); found=1
|
|
64
|
+
else
|
|
65
|
+
# the delegate the manifest names: the <string> after UISceneDelegateClassName
|
|
66
|
+
DEL="$(tr -d '\n\r' < "$PL" | grep -oE '<key>UISceneDelegateClassName</key>[[:space:]]*<string>[^<]+' | head -1 | sed 's/.*<string>//')"
|
|
67
|
+
if [[ -n "$DEL" ]] && ! grep -rqsE "Register\(\"$DEL\"\)" "$D" --include='*.cs'; then
|
|
68
|
+
echo "FAIL $P: Info.plist names the scene delegate $DEL, and no class in the project carries [Register(\"$DEL\")] (a class deriving from MauiUISceneDelegate)"
|
|
69
|
+
fails=$((fails+1)); found=1
|
|
70
|
+
elif [[ -n "$DEL" ]] && ! grep -rqs 'MauiUISceneDelegate' "$D" --include='*.cs'; then
|
|
71
|
+
echo "FAIL $P: [Register(\"$DEL\")] is there, but no class derives from MauiUISceneDelegate"
|
|
72
|
+
fails=$((fails+1)); found=1
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
# --- the keychain permission on a locally signed build
|
|
76
|
+
ENTS=()
|
|
77
|
+
while IFS= read -r e; do [[ -n "$e" ]] && ENTS+=("$D/$(tr '\\' '/' <<<"$e")"); done < <(
|
|
78
|
+
grep -oE '<CodesignEntitlements[^>]*>[^<]+' "$P" | sed 's/<CodesignEntitlements[^>]*>//') # a Condition= one too
|
|
79
|
+
[[ ${#ENTS[@]} -eq 0 && -f "$M/Entitlements.plist" ]] && ENTS=("$M/Entitlements.plist")
|
|
80
|
+
with=0; without=0
|
|
81
|
+
for e in ${ENTS[@]+"${ENTS[@]}"}; do
|
|
82
|
+
[[ -f "$e" ]] || continue
|
|
83
|
+
if grep -q 'keychain-access-groups' "$e"; then with=1; else without=1; fi
|
|
84
|
+
done
|
|
85
|
+
if [[ $with -eq 1 && $without -eq 0 ]] && ! grep -q '<CodesignProvisioningProfile>' "$P"; then
|
|
86
|
+
echo "WARN $P: the entitlements ask for keychain-access-groups and the project names no provisioning profile; macOS will not start a locally signed build that asks for it. Sign with a development certificate and profile, or give Debug builds an entitlements file without it (coding-standards-dotnet.md §9)"
|
|
87
|
+
found=1
|
|
88
|
+
fi
|
|
89
|
+
[[ $found -eq 0 ]] && echo "OK $P: scene manifest and delegate in place; no keychain permission a local build cannot have; zoom wired or not a Blazor Hybrid head"
|
|
90
|
+
done
|
|
91
|
+
[[ $seen -eq 0 ]] && echo "NONE no MAUI project with a Mac Catalyst or Windows target"
|
|
92
|
+
[[ $fails -eq 0 ]]
|