@topy-ai/maggie 0.7.51 → 0.7.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh-TW.md +2 -2
- package/README.md +12 -3
- package/bin/maggie.js +1 -1
- package/bundled-skills/maggie-design/SKILL.md +13 -0
- package/bundled-skills/maggie-mobile-app/SKILL.md +21 -4
- package/bundled-tools/clis/maggie_design.py +3 -1
- package/bundled-tools/clis/maggie_design_lint.py +68 -1
- package/bundled-tools/clis/maggie_mobile.py +20 -6
- package/package.json +1 -1
package/README-zh-TW.md
CHANGED
|
@@ -29,7 +29,7 @@ npx @topy-ai/maggie init --scope project --agent all
|
|
|
29
29
|
npx @topy-ai/maggie doctor --scope project --project .
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
目前 npm 版本為 `0.7.51
|
|
32
|
+
目前 npm 版本為 `0.7.52`:加入三項 mobile lane 後續修正——`maggie mobile preflight` 會在 non-interactive `eas update` script 缺少 `--environment` 時失敗,並在 build 後修改 `eas.json` 可能改變 fingerprint(OTA 更新會無聲地送不到該 build)時警告;`maggie mobile plan` 一律帶 `--environment`,每次 update 前先跑 `eas fingerprint:compare`;新增 `maggie design safe-area-check`,標出 iOS modal route 與 React Native `<Modal>` 內的 `<SafeAreaView>`(可能量到 0 的 top inset)。前一版 `0.7.51` 完成 batch 28:17 條 hosted feedback,來自一個跨平台產品開發 session(同一套 Expo 程式碼支援 web、iOS、Android,加上具備 MCP 與 ChatGPT/Claude app 的 AI 助理、產品自己的 CLI/MCP 套件、第三方 skills catalog 以及多租戶 assistant bridge host)。`maggie design app-validate` 改讀每個 viewport 的 evidence(scroll owner、固定 header/footer),只有 `app-init --pane list-detail:1024:2` 宣告的多欄版面才允許多個 scroll owner,並以 PNG 實際尺寸對照 plan viewport;`app-init --capabilities none|camera,…`(或 `--camera none`)讓非相機頁面不必填相機狀態,`--pattern` 加入新的 app UI patterns 驗收規則。新增 `maggie design app-collect`:在同一個瀏覽器分頁逐一以真實尺寸渲染已登入的 app 路由、略過 React Native Web 的包裝層計算 scroll owner、證明固定 shell,並寫出 `runtime-evidence.json` 與截圖(可 `--cdp-url` 接上已登入的 Chrome,或用專用 QA profile,`--read-only` 阻擋寫入)。新增 `maggie qa app-preflight`:偵測以 `CI=1` 或從錯誤資料夾啟動的 Metro/Expo dev server,並對會輪替的 refresh token 與連到 production API 的本機 server 發出警告。新增 `maggie-mobile-app` skill 與 `maggie mobile preflight|plan`(EAS build profile、TestFlight / Play 內部測試、EAS Update channel 與 fingerprint runtime policy、上架審核規則、建置前成本提示;`--auto-submit` 缺 `ascAppId` 或 `appVersion` policy 配上 remote build number 時在花費額度前就失敗)。新增 `maggie-assistant-tools` skill 與 `maggie assistant`(單一 tool registry、風險等級與 gate、與 model description 分開的使用者文案、由風險推導 MCP annotations、自動產生「助理能做與不能做」頁面、audit 檢查與簽章一次性 approval token 參考實作)。新增 `maggie product release-check|smoke|mcp-check|review-status`(產品 npm CLI/MCP 套件 metadata、`server.json`/`mcpName`、打包檔案機密掃描、安裝後 binary smoke、OAuth protected-resource metadata、ChatGPT/Claude/商店審核追蹤)、`maggie skills scaffold|validate|package|smoke`(第三方 skills catalog 打包成分類 plugin、all-in-one plugin 與 Claude Code `marketplace.json`)、`maggie deployment bridge-plan|bridge-collect-script|bridge-check`(多租戶 bridge host);`maggie feedback session-summary` 支援多個 repo 與一般句子形式的 commit。MaggieDash 0.2.12 從 ORA host 上游通用修正。前一版 `0.7.50`:`maggie update` 改為 idempotent,內容已與套件相同的檔案不再改寫(保留時間戳),摘要分別列出 changed/unchanged/preserved 數量,只有真的保留了本機修改時才顯示 preserved 提示。前一版 `0.7.49` 完成 batch 27:33 條 2026-09-09 至 09-16 建立但從未提交的本機 feedback drafts,已逐條對照目前程式碼(其中兩條已被既有功能滿足)。新增離線 `maggie ops notifications`(通知 catalogue 合約、consent/retention/durable lead delivery 規則、provider template drift、只檢查變數名稱的 Resend preflight、queue health 門檻與 operator alerts、dry-run retention plan);`maggie localization email-matrix` 與 `maggie localization lifecycle`(locale 優先順序、未翻譯/缺漏 locale、變數與 HTML/text 一致、market CTA route、consent-aware 行銷 email 預設不發送);Deployment 的 `--keep-releases` 統一 retention policy、`auth-canary`、資料庫 owner 無法讀取 release 路徑時的 streamed migration、`collect` 單一 readiness report;Google failure remediation(`maggie ops google-diagnose`)與唯讀 `google-ads-preflight`/`google-ads-keyword-ideas`;Social Share `reconcile`(timeout 後不重複發文)、`maggie-marketing-run-v1` 合約、命名排程層與只管制對外發布的 regulated-content review;`maggie design icon-glyph-check`、`maggie design verify`、`maggie service import-stages-validate`/`import-readiness`/`fact-provenance`;以及 `--release-metadata` 版本一致性檢查、`maggie docs rollout-status` 與 stdin batch 輸入。前一版 `0.7.48` 處理 2026-10-10 的 27 條 hosted feedback(batch 26):Social Share 新增 `adapters`(每個 provider 一組 canonical OAuth scope,LinkedIn 不同 scope 會撤銷舊 token;共用 OAuth client 警告)、`connections`(撤銷或無法解密的 token 標為 `needs_reconnect`,偵測會過期的簽名頭像 URL)、`queue`(slot-based 排程:not-before、最新優先、campaign 輪流與每日上限、DST-safe)與 `campaigns`(idea stage Start gate);Deployment 新增 hash-only `secret-fingerprint`/`secret-parity`、`env-isolation`(本機 `.env` 指向 production database 時警告)、release retention 與 worker-before-enqueue 發布順序;QA 在未明確同意時阻擋對 production API 的寫入情境,並以 `maggie qa cors-preflight` 只用 OPTIONS 檢查每個 HTTP method;Design 新增 `pressable-check`、`layer-check`(Tailwind v4 unlayered CSS)、onboarding wizard/picker 模式與 `maggie-palette-roles-v1` palette generate/validate;Marketplace 加入 screenshot 完整性與像素預算、從 CSS 量測 palette、asset provenance、中性 `displayName`(全部模板已補齊)、template studio、`maggie-template-content-v1` 合約與圖片生成成本/無文字審查 gate;Booking 新增唯讀 Stripe live setup runbook 與 gift voucher lifecycle 合約(`maggie booking voucher-contract --ledger`);Feedback 新增 `improvement` 類型與 `maggie feedback session-summary`。MaggieDash 0.2.11 會保留尚未部署 template 的 outbox email 而不是 dead-letter,並從 ORA host 上游通用修正。本版也包含 Booking 十個 email events 合約與 CLI validator 同步及安全輸出的 `arrivalNote`。前一版 `0.7.47` 處理 2026-09-26 五條新 feedback:project-scope
|
|
33
33
|
`maggie init` 會建立本地 memory store;context 預設只讀取本 project scope;memory hook
|
|
34
34
|
在 local Python adapter 不存在時 fallback 到已安裝 CLI;重複 init 保留既有檔案與時間戳;
|
|
35
35
|
`maggie feedback list` 的 `--project` 可放在子命令前或後。Generated VPS runner 現在記錄
|
|
@@ -113,7 +113,7 @@ structure drift 或重複 treatment product 時 fail closed。
|
|
|
113
113
|
deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
|
|
114
114
|
publish 與 production deployment 需要明確確認。
|
|
115
115
|
|
|
116
|
-
目前 npm 版本為 `0.7.
|
|
116
|
+
目前 npm 版本為 `0.7.52`,包含 batch 28 產品 app、assistant tools、mobile、product release、skills catalog 與 bridge host 功能、batch 26、27 feedback 修正與 idempotent `maggie update`與 Booking email-template 合約/CLI validator 同步。本版完成 2026-09-24 booking feedback batch:Calendar grid/free-slot entry 與 host modal focus-restoration evidence。本版新增 Starter marketplace provisioning plan;本版完成 feedback batches 22、23:加入 declarative browser interaction evidence、design markup/style/content-key gate、compiler source anchoring、entity double-escape protection、`update --help` read-only、noindex hypothetical indexability、可過期 baseline acknowledgement、DOM-only link extraction regression,以及 MaggieDash 0.2.10 的 content override、短期 browser session、CDN-safe public-session cache contracts。authentication 和 isolated dev server 仍由 host 負責。`maggie-booking` workflow、傳統 email/password、Stripe、Resend、public booking flow 及既有安全邊界維持不變;provider-bound video generation 與 transactional delivery 仍記錄在 issue #39,前一批延後功能仍在 issue #38,不會直接成為 active memory。0.7.33 記錄並審查一筆更正回饋;確認 0.7.32 已正確處理 batch index,沒有修改已驗證的 shared behavior。0.7.32 加入 feedback 一基索引支援、MaggieDash panel 的 source/freshness/error evidence、Consent Mode 與實際 write/no-op reconciliation、SEO baseline recapture、sitemap origin rebasing、content-diff scope、migration ledger reconciliation、deployment credential preflight、release provenance/runtime preflight、icon release gate、opt-in blog auto-publish gate,以及 least-privilege VPS deployer。也修復 top-level `maggie feedback` dispatcher parity。0.7.31 加入 update 差異摘要、atomic installer copy、subset install manifest merge,以及 inventory drift 時 doctor non-zero。0.7.30 補上 feedback tracking issue closeout gate 與 batch 14/15 audit ledger。0.7.29 修正 npm package landing page 使用英文 README;0.7.28 加入 versioned Gemini model policy、明確 fallback 與 provenance、bundled-first CLI
|
|
117
117
|
dispatch、`maggie --version`、feedback batch review 聚合與重複偵測、marketplace enrichment evidence、booking
|
|
118
118
|
worker/resolver evidence,以及 read-only migration preflight。它也包含 host-owned mobile app surface contract、signed-in
|
|
119
119
|
camera-state QA、直接 Astro route resolution、correlated feedback batch,以及
|
package/README.md
CHANGED
|
@@ -89,7 +89,16 @@ Maggie keeps the existing project foundation and asks for decisions before
|
|
|
89
89
|
shared routes, analytics, or publishing boundaries change. The current
|
|
90
90
|
package ships 22 installable skills and a local-first MaggieDash foundation.
|
|
91
91
|
|
|
92
|
-
This package is `0.7.
|
|
92
|
+
This package is `0.7.52`. It adds three follow-ups to the mobile
|
|
93
|
+
lane that arrived during the 0.7.51 release: `maggie mobile preflight` fails
|
|
94
|
+
on non-interactive `eas update` scripts without `--environment` and warns
|
|
95
|
+
when `eas.json` can change the fingerprint after a build (OTA updates would
|
|
96
|
+
silently stop reaching it); `maggie mobile plan` always sets `--environment`
|
|
97
|
+
and gates every update on `eas fingerprint:compare`; and the new
|
|
98
|
+
`maggie design safe-area-check` flags `<SafeAreaView>` inside iOS modal routes
|
|
99
|
+
and React Native `<Modal>`, where it can measure a 0 top inset.
|
|
100
|
+
|
|
101
|
+
The 0.7.51 release closed batch 28: 17 hosted feedback
|
|
93
102
|
records from a cross-platform product session (one Expo codebase for web,
|
|
94
103
|
iOS and Android, an assistant with MCP and ChatGPT/Claude app surfaces,
|
|
95
104
|
product CLI/MCP packages, a third-party skills catalog, and a multi-tenant
|
|
@@ -778,8 +787,8 @@ artifact schemas.
|
|
|
778
787
|
Recommended upgrade sequence for the current release:
|
|
779
788
|
|
|
780
789
|
```bash
|
|
781
|
-
npx @topy-ai/maggie@0.7.
|
|
782
|
-
npx @topy-ai/maggie@0.7.
|
|
790
|
+
npx @topy-ai/maggie@0.7.52 update --scope project --project . --force
|
|
791
|
+
npx @topy-ai/maggie@0.7.52 cleanup --scope project --project .
|
|
783
792
|
```
|
|
784
793
|
|
|
785
794
|
Maintainers should pass npm credentials through the repository helper, never
|
package/bin/maggie.js
CHANGED
|
@@ -99,7 +99,7 @@ Usage:
|
|
|
99
99
|
maggie dash structure-validate --model FILE --evidence FILE
|
|
100
100
|
maggie design progressive-check|css-check|css-cascade-check|style-check --evidence FILE
|
|
101
101
|
maggie design dash-init|dash-validate|sample-init|sample-validate [options]
|
|
102
|
-
maggie design pressable-check|layer-check --project PATH
|
|
102
|
+
maggie design pressable-check|layer-check|safe-area-check --project PATH
|
|
103
103
|
maggie design palette generate --canvas HEX --text HEX --accent HEX | palette validate --palette FILE
|
|
104
104
|
maggie seo privacy-check --evidence FILE
|
|
105
105
|
maggie content FILE --source PROVIDER --project PATH --confirm
|
|
@@ -370,6 +370,19 @@ when `fullPage: true`). A top-level `shell.scrollOwnerCount` is ignored with a
|
|
|
370
370
|
warning, and a signature-only or scaled screenshot fails. The machine-readable
|
|
371
371
|
contract is
|
|
372
372
|
[mobile-app-surface-v1.schema.json](../../bundled-contracts/maggie-design/mobile-app-surface-v1.schema.json).
|
|
373
|
+
Native iOS modals need one more check. `<SafeAreaView>` inside an Expo Router
|
|
374
|
+
modal route (`presentation: "transparentModal"`, `"modal"`, …) or a React
|
|
375
|
+
Native `<Modal>` can measure a 0 top inset on iOS, putting the header and
|
|
376
|
+
Close under the notch or Dynamic Island; web screenshots at 390/768/1440
|
|
377
|
+
cannot show it:
|
|
378
|
+
|
|
379
|
+
maggie design safe-area-check --project apps/app
|
|
380
|
+
|
|
381
|
+
It flags those uses; pad with `useSafeAreaInsets()` from the root
|
|
382
|
+
`SafeAreaProvider` instead (`// maggie-allow-safe-area-view` marks a reviewed
|
|
383
|
+
exception). `app-init` adds an `ios-safe-area` scenario that needs a notched
|
|
384
|
+
device or simulator screenshot.
|
|
385
|
+
|
|
373
386
|
Keep `maggie design pressable-check` in the same loop for React Native Web
|
|
374
387
|
apps: together they caught nested pressables and shell regressions during
|
|
375
388
|
repeated sheet and modal changes.
|
|
@@ -58,6 +58,11 @@ with `--config-json` when the config is dynamic), `eas.json` and
|
|
|
58
58
|
future binary would accept any JS update, including one that needs a new
|
|
59
59
|
native module. Use one top-level `{"policy": "fingerprint"}`.
|
|
60
60
|
|
|
61
|
+
It also fails when a `package.json` script runs `eas update --non-interactive`
|
|
62
|
+
without `--environment` (EAS refuses it), and warns when an update script has
|
|
63
|
+
no `eas fingerprint:compare` gate or when `eas.json` is not in
|
|
64
|
+
`.fingerprintignore` under the fingerprint policy.
|
|
65
|
+
|
|
61
66
|
It warns about missing export-compliance (`usesNonExemptEncryption`), an
|
|
62
67
|
unpinned `cli.version`, per-platform-only runtime versions (what
|
|
63
68
|
`eas update:configure` writes) and tab-indented `app.json` that
|
|
@@ -88,10 +93,22 @@ prints the exact commands with the cost notice and runs nothing:
|
|
|
88
93
|
→ TestFlight (iOS) or the Play internal track (Android). Add testers in
|
|
89
94
|
App Store Connect / Play Console; internal testing needs no review.
|
|
90
95
|
2. Without `--auto-submit`: `npx eas-cli submit -p ios --profile production --latest`.
|
|
91
|
-
3. JS-only fixes: `npx eas-cli
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
3. JS-only fixes: first `npx eas-cli fingerprint:compare --build-id <installed build>`
|
|
97
|
+
— it must report no difference — then
|
|
98
|
+
`npx eas-cli update --channel production --environment production --platform ios --non-interactive --message "…"`.
|
|
99
|
+
`--environment` is mandatory in non-interactive mode. A native change (new
|
|
100
|
+
module, permission, SDK upgrade) needs a new build; the fingerprint policy
|
|
101
|
+
stops an incompatible update from reaching old binaries.
|
|
102
|
+
|
|
103
|
+
### The fingerprint includes `eas.json`
|
|
104
|
+
|
|
105
|
+
Editing `eas.json` after a build — even only adding `submit.ios.ascAppId` —
|
|
106
|
+
changes the local fingerprint. An update published from that tree targets a
|
|
107
|
+
runtime no installed build has, and EAS reports no error. Run
|
|
108
|
+
`fingerprint:compare` against the target build before every update; if only
|
|
109
|
+
`eas.json` differs, publish from the build-time `eas.json` (restore it, publish,
|
|
110
|
+
restore the current file) and add `eas.json` to `.fingerprintignore` with the
|
|
111
|
+
next native build so it stops affecting the runtime.
|
|
95
112
|
|
|
96
113
|
Show the plan's `costNotice` and wait for confirmation before step 1.
|
|
97
114
|
|
|
@@ -356,6 +356,7 @@ def app_surface_init(project: Path, route: str, auth_mode: str, force: bool, con
|
|
|
356
356
|
for name in capabilities:
|
|
357
357
|
if name != "camera":
|
|
358
358
|
scenarios.append({"id": f"{name}-permission", "viewport": "mobile", "assertions": [f"{name} permission is explained before the system prompt", "denied permission leaves an actionable fallback"]})
|
|
359
|
+
scenarios.append({"id": "ios-safe-area", "viewport": "notched iPhone (device or simulator)", "assertions": ["header and Close sit below the status bar / Dynamic Island in every modal and sheet", "insets come from useSafeAreaInsets(), not SafeAreaView inside a modal (`maggie design safe-area-check`)", "web screenshots cannot prove this; record a device screenshot"]})
|
|
359
360
|
for pattern in patterns:
|
|
360
361
|
scenarios.append({"id": f"pattern-{pattern}", "viewport": "all", "assertions": APP_PATTERN_ACCEPTANCE[pattern]})
|
|
361
362
|
if auth_mode != "none":
|
|
@@ -1003,6 +1004,7 @@ commands:
|
|
|
1003
1004
|
markup-check validate optional style attributes, content keys, and entities
|
|
1004
1005
|
pressable-check flag pressables/buttons nested in another pressable (JSX/TSX)
|
|
1005
1006
|
layer-check flag unlayered CSS that overrides Tailwind v4 utilities
|
|
1007
|
+
safe-area-check flag SafeAreaView inside iOS modal routes / RN Modal
|
|
1006
1008
|
palette generate or validate a semantic palette role contract
|
|
1007
1009
|
verify run automatable design-job steps and record evidence
|
|
1008
1010
|
icon-glyph-check render icons and prove font load and glyph paint
|
|
@@ -1010,7 +1012,7 @@ commands:
|
|
|
1010
1012
|
reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
|
|
1011
1013
|
""")
|
|
1012
1014
|
return 0
|
|
1013
|
-
if len(sys.argv) > 1 and sys.argv[1] in {"pressable-check", "layer-check"}:
|
|
1015
|
+
if len(sys.argv) > 1 and sys.argv[1] in {"pressable-check", "layer-check", "safe-area-check"}:
|
|
1014
1016
|
from maggie_design_lint import main as lint_main
|
|
1015
1017
|
return lint_main(sys.argv[1:])
|
|
1016
1018
|
if len(sys.argv) > 1 and sys.argv[1] == "verify":
|
|
@@ -12,6 +12,13 @@ Tailwind v4 emits utilities inside ``@layer utilities``; any unlayered rule
|
|
|
12
12
|
wins over every layer regardless of specificity, so an unlayered
|
|
13
13
|
``h1 { font-family }`` beats ``font-*`` and ``.ph { display: inline-block }``
|
|
14
14
|
beats ``hidden`` and ``group-open:inline``.
|
|
15
|
+
|
|
16
|
+
``safe-area-check`` flags ``<SafeAreaView>`` used as the inset source inside an
|
|
17
|
+
Expo Router modal route (``presentation: "transparentModal"``, ``"modal"``,
|
|
18
|
+
``"fullScreenModal"``, ...) or a React Native ``<Modal>``. On iOS those
|
|
19
|
+
surfaces can measure a 0 top inset, so the header and Close button sit under
|
|
20
|
+
the notch or Dynamic Island; web screenshots cannot show it. Read insets with
|
|
21
|
+
``useSafeAreaInsets()`` from the root ``SafeAreaProvider`` instead.
|
|
15
22
|
"""
|
|
16
23
|
|
|
17
24
|
from __future__ import annotations
|
|
@@ -249,6 +256,61 @@ def layer_check(project: Path, paths: list[Path], force_v4: bool) -> dict[str, o
|
|
|
249
256
|
return {"schemaVersion": "maggie-design-layer-check.v1", "passed": not errors, "status": "checked", "checkedFiles": len(files), "errors": len(errors), "warnings": len(findings) - len(errors), "findings": findings}
|
|
250
257
|
|
|
251
258
|
|
|
259
|
+
MODAL_PRESENTATIONS = ("transparentModal", "containedTransparentModal", "modal", "containedModal", "fullScreenModal", "formSheet")
|
|
260
|
+
SAFE_AREA_ALLOW = "maggie-allow-safe-area-view"
|
|
261
|
+
SCREEN_TAG = re.compile(r"<(?:Stack|Tabs|Drawer)\.Screen\b(?:[^<>]|=>|\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\})*?/?>", re.S)
|
|
262
|
+
PRESENTATION = re.compile(r"presentation\s*:\s*[\"'](" + "|".join(MODAL_PRESENTATIONS) + r")[\"']")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _route_file(layout: Path, name: str) -> Path | None:
|
|
266
|
+
for candidate in (layout.parent / f"{name}.tsx", layout.parent / f"{name}.jsx", layout.parent / name / "index.tsx", layout.parent / name / "index.jsx"):
|
|
267
|
+
if candidate.is_file():
|
|
268
|
+
return candidate
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _line(source: str, index: int) -> int:
|
|
273
|
+
return source.count("\n", 0, index) + 1
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def safe_area_check(project: Path, paths: list[Path]) -> dict[str, object]:
|
|
277
|
+
project = project.resolve()
|
|
278
|
+
files = iter_files(paths or [project], (".tsx", ".jsx"))
|
|
279
|
+
modal_routes: dict[Path, str] = {}
|
|
280
|
+
for path in files:
|
|
281
|
+
source = _strip_jsx_comments(path.read_text(encoding="utf-8", errors="replace"))
|
|
282
|
+
for tag in SCREEN_TAG.finditer(source):
|
|
283
|
+
presentation = PRESENTATION.search(tag.group(0))
|
|
284
|
+
if not presentation:
|
|
285
|
+
continue
|
|
286
|
+
name = re.search(r"\bname\s*=\s*[\"']([^\"']+)[\"']", tag.group(0))
|
|
287
|
+
target = _route_file(path, name.group(1)) if name and path.stem == "_layout" else (path if not name else None)
|
|
288
|
+
if target:
|
|
289
|
+
modal_routes[target.resolve()] = presentation.group(1)
|
|
290
|
+
findings: list[dict[str, object]] = []
|
|
291
|
+
for path in files:
|
|
292
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
293
|
+
if SAFE_AREA_ALLOW in raw or "SafeAreaView" not in raw:
|
|
294
|
+
continue
|
|
295
|
+
source = _strip_jsx_comments(raw)
|
|
296
|
+
display = _display(path, project)
|
|
297
|
+
presentation = modal_routes.get(path.resolve())
|
|
298
|
+
if presentation:
|
|
299
|
+
match = re.search(r"<SafeAreaView\b", source)
|
|
300
|
+
if match:
|
|
301
|
+
findings.append({"file": display, "line": _line(source, match.start()), "kind": "modal-route", "presentation": presentation, "message": f"route presented as {presentation} uses <SafeAreaView>; on iOS it can measure a 0 top inset, so pad with useSafeAreaInsets() instead"})
|
|
302
|
+
continue
|
|
303
|
+
if re.search(r"import\s*\{[^}]*\bModal\b[^}]*\}\s*from\s*[\"']react-native[\"']", source):
|
|
304
|
+
for modal in re.finditer(r"<Modal\b", source):
|
|
305
|
+
end = source.find("</Modal>", modal.end())
|
|
306
|
+
inner = source[modal.end(): end if end >= 0 else len(source)]
|
|
307
|
+
safe = re.search(r"<SafeAreaView\b", inner)
|
|
308
|
+
if safe:
|
|
309
|
+
findings.append({"file": display, "line": _line(source, modal.end() + safe.start()), "kind": "rn-modal", "message": "<SafeAreaView> inside a React Native <Modal> can report a 0 top inset on iOS; use useSafeAreaInsets() padding"})
|
|
310
|
+
return {"schemaVersion": "maggie-design-safe-area-check.v1", "passed": not findings, "checkedFiles": len(files), "modalRoutes": sorted(_display(path, project) for path in modal_routes), "findings": findings,
|
|
311
|
+
"recommendation": "Wrap the app once in SafeAreaProvider; in modals read const insets = useSafeAreaInsets() and pad the header/footer. Verify on a notched iPhone (or simulator) — web screenshots cannot show the inset."}
|
|
312
|
+
|
|
313
|
+
|
|
252
314
|
def main(argv: list[str] | None = None) -> int:
|
|
253
315
|
parser = argparse.ArgumentParser(prog="maggie design", description="Static design checks that run before browser QA.")
|
|
254
316
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
@@ -260,9 +322,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
260
322
|
layers.add_argument("--project", type=Path, default=Path.cwd())
|
|
261
323
|
layers.add_argument("--css", type=Path, action="append", default=[], help="CSS file or directory to scan; defaults to the project")
|
|
262
324
|
layers.add_argument("--tailwind-v4", action="store_true", help="check even when no @import \"tailwindcss\" entry is found")
|
|
325
|
+
safe = sub.add_parser("safe-area-check", help="flag SafeAreaView inside Expo Router modal routes and React Native Modal (.tsx/.jsx)")
|
|
326
|
+
safe.add_argument("--project", type=Path, default=Path.cwd())
|
|
327
|
+
safe.add_argument("--path", type=Path, action="append", default=[], help="file or directory to scan; defaults to the project")
|
|
263
328
|
args = parser.parse_args(argv)
|
|
264
329
|
try:
|
|
265
|
-
if args.command == "
|
|
330
|
+
if args.command == "safe-area-check":
|
|
331
|
+
report = safe_area_check(args.project, args.path)
|
|
332
|
+
elif args.command == "pressable-check":
|
|
266
333
|
report = pressable_check(args.project, args.path, args.pressable)
|
|
267
334
|
else:
|
|
268
335
|
report = layer_check(args.project, args.css, args.tailwind_v4)
|
|
@@ -68,7 +68,7 @@ def runtime_policy(expo: dict, platform: str) -> tuple[object, str]:
|
|
|
68
68
|
return expo.get("runtimeVersion"), "expo.runtimeVersion"
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def check_profile(expo: dict, eas: dict, package: dict, profile: str, platforms: list[str], auto_submit: bool, raw_app_json: str | None) -> tuple[list[str], list[str], dict]:
|
|
71
|
+
def check_profile(expo: dict, eas: dict, package: dict, profile: str, platforms: list[str], auto_submit: bool, raw_app_json: str | None, app_dir_ignore_text: str = "") -> tuple[list[str], list[str], dict]:
|
|
72
72
|
errors: list[str] = []
|
|
73
73
|
warnings: list[str] = []
|
|
74
74
|
facts: dict = {"profile": profile, "platforms": platforms}
|
|
@@ -140,6 +140,17 @@ def check_profile(expo: dict, eas: dict, package: dict, profile: str, platforms:
|
|
|
140
140
|
warnings.append(f"android: submit.{profile}.android.serviceAccountKeyPath is not set; provide it or EXPO_ANDROID_SERVICE_ACCOUNT_KEY_PATH, and keep the key file out of git")
|
|
141
141
|
if android.get("track", "internal") not in {"internal", "alpha", "beta", "production"}:
|
|
142
142
|
errors.append("android: submit track must be internal, alpha, beta or production")
|
|
143
|
+
for script_name, command in (package.get("scripts", {}) or {}).items():
|
|
144
|
+
text = str(command)
|
|
145
|
+
if re.search(r"\beas(?:-cli)?\s+update\b(?!:)", text):
|
|
146
|
+
if "--non-interactive" in text and "--environment" not in text:
|
|
147
|
+
errors.append(f"package.json script {script_name!r} runs `eas update --non-interactive` without --environment; EAS refuses it ('The --environment flag must be set'). Add --environment matching the channel")
|
|
148
|
+
if "fingerprint:compare" not in text and uses_updates:
|
|
149
|
+
warnings.append(f"package.json script {script_name!r} publishes an update without `eas fingerprint:compare --build-id <build>`; a fingerprint mismatch sends the update to a runtime no installed build has")
|
|
150
|
+
if uses_updates and any(isinstance(runtime_policy(expo, p)[0], dict) and runtime_policy(expo, p)[0].get("policy") == "fingerprint" for p in platforms):
|
|
151
|
+
ignore = app_dir_ignore_text
|
|
152
|
+
if "eas.json" not in ignore.split():
|
|
153
|
+
warnings.append("runtime policy fingerprint includes eas.json: editing eas.json after a build (for example adding ascAppId) changes the fingerprint and silently stops OTA updates reaching that build. Add eas.json to .fingerprintignore with the next native build, and run `eas fingerprint:compare --build-id <build>` before every update")
|
|
143
154
|
if raw_app_json and "\t" in raw_app_json:
|
|
144
155
|
warnings.append("app.json is tab-indented; `eas update:configure` rewrites it with spaces (a large diff) — prefer editing runtimeVersion and updates by hand")
|
|
145
156
|
return errors, warnings, facts
|
|
@@ -154,7 +165,8 @@ def preflight(project: Path, app_dir: Path | None, config_json: Path | None, pro
|
|
|
154
165
|
raise ValueError("eas.json not found; run `npx eas-cli build:configure` once, then review the file")
|
|
155
166
|
eas = _load(eas_path)
|
|
156
167
|
package = _load(app / "package.json") if (app / "package.json").is_file() else {}
|
|
157
|
-
|
|
168
|
+
ignore = (app / ".fingerprintignore").read_text(encoding="utf-8") if (app / ".fingerprintignore").is_file() else ""
|
|
169
|
+
errors, warnings, facts = check_profile(expo, eas, package, profile, platforms, auto_submit, raw, ignore)
|
|
158
170
|
return {
|
|
159
171
|
"schemaVersion": SCHEMA,
|
|
160
172
|
"appDir": str(app.relative_to(project)) if app.is_relative_to(project) else ".",
|
|
@@ -173,7 +185,8 @@ def cost_notice(platforms: list[str], profile: str) -> str:
|
|
|
173
185
|
"build minutes even if a later step fails. EAS Update publishes are counted against monthly update users. Confirm before running.")
|
|
174
186
|
|
|
175
187
|
|
|
176
|
-
def plan_commands(profile: str, platforms: list[str], auto_submit: bool, channel: str | None) -> list[dict]:
|
|
188
|
+
def plan_commands(profile: str, platforms: list[str], auto_submit: bool, channel: str | None, environment: str | None = None) -> list[dict]:
|
|
189
|
+
environment = environment or (profile if profile in {"development", "preview", "production"} else "production")
|
|
177
190
|
platform_arg = "all" if len(platforms) == 2 else platforms[0]
|
|
178
191
|
commands = [{"step": "preflight", "command": f"maggie mobile preflight --profile {profile} --platform {platform_arg}" + (" --auto-submit" if auto_submit else ""), "spends": False}]
|
|
179
192
|
build = f"npx eas-cli build -p {platform_arg} --profile {profile} --non-interactive" + (" --auto-submit" if auto_submit else "")
|
|
@@ -182,8 +195,8 @@ def plan_commands(profile: str, platforms: list[str], auto_submit: bool, channel
|
|
|
182
195
|
for platform in platforms:
|
|
183
196
|
commands.append({"step": f"submit-{platform}", "command": f"npx eas-cli submit -p {platform} --profile {profile} --latest --non-interactive", "spends": False})
|
|
184
197
|
if channel:
|
|
185
|
-
|
|
186
|
-
commands.append({"step": "ota-update", "command": f"npx eas-cli update --channel {channel}{environment} --platform {platform_arg} --message \"<what changed>\" --non-interactive", "spends": True, "note": "only JS/asset changes; a native change needs a new build (the fingerprint policy enforces this)"})
|
|
198
|
+
commands.append({"step": "fingerprint-gate", "command": "npx eas-cli fingerprint:compare --build-id <installed build id>", "spends": False, "note": "must report no difference; editing eas.json, native config or dependencies after the build changes the fingerprint and the update would reach no installed build"})
|
|
199
|
+
commands.append({"step": "ota-update", "command": f"npx eas-cli update --channel {channel} --environment {environment} --platform {platform_arg} --message \"<what changed>\" --non-interactive", "spends": True, "note": "only JS/asset changes; a native change needs a new build (the fingerprint policy enforces this)"})
|
|
187
200
|
return commands
|
|
188
201
|
|
|
189
202
|
|
|
@@ -198,6 +211,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
198
211
|
command.add_argument("--profile", default="production")
|
|
199
212
|
command.add_argument("--platform", choices=("ios", "android", "all"), default="all")
|
|
200
213
|
command.add_argument("--auto-submit", action="store_true", help="the build will run with --auto-submit --non-interactive")
|
|
214
|
+
command.add_argument("--environment", help="EAS environment for updates (default: the profile when it is development/preview/production)")
|
|
201
215
|
command.add_argument("--output", type=Path)
|
|
202
216
|
args = parser.parse_args(argv)
|
|
203
217
|
platforms = list(PLATFORMS) if args.platform == "all" else [args.platform]
|
|
@@ -207,7 +221,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
207
221
|
print(f"BLOCKED: maggie mobile {args.command}: {error}", file=sys.stderr)
|
|
208
222
|
return 2
|
|
209
223
|
if args.command == "plan":
|
|
210
|
-
report = {"schemaVersion": "maggie-mobile-release-plan.v1", "preflight": {key: report[key] for key in ("status", "errors", "warnings")}, "commands": plan_commands(args.profile, platforms, args.auto_submit, report["facts"].get("channel") if report["facts"].get("easUpdate") else None), "costNotice": report["costNotice"], "executed": False}
|
|
224
|
+
report = {"schemaVersion": "maggie-mobile-release-plan.v1", "preflight": {key: report[key] for key in ("status", "errors", "warnings")}, "commands": plan_commands(args.profile, platforms, args.auto_submit, report["facts"].get("channel") if report["facts"].get("easUpdate") else None, args.environment), "costNotice": report["costNotice"], "executed": False}
|
|
211
225
|
text = json.dumps(report, indent=2, ensure_ascii=False)
|
|
212
226
|
if args.output:
|
|
213
227
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|