mosaic-headless 1.29.2 → 1.30.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/README.md +17 -5
- package/SKILL.md +41 -2
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/package.json +9 -2
- package/references/responsive.md +1 -0
- package/scripts/check-release.mjs +5 -1
- package/scripts/sync-version.mjs +3 -2
- package/sites/_components.py +560 -0
- package/sites/_content.py +341 -0
- package/sites/_conversion.py +225 -0
- package/sites/_dialogs.py +579 -0
- package/sites/_dynamic.py +311 -0
- package/sites/_forms.py +487 -0
- package/sites/_sliders.py +437 -0
- package/sites/_tokens.py +277 -0
- package/tools/build_page.py +4 -2
- package/tools/build_site.py +4 -2
- package/tools/copy_styles.py +4 -2
- package/tools/mosaic_config.py +71 -0
- package/tools/probe.py +4 -2
- package/tools/probe_accordion.py +4 -2
- package/tools/sweep_components.py +4 -2
- package/tools/sweep_interactions.py +4 -2
- package/tools/sweep_node_properties.py +4 -2
- package/tools/sweep_node_types.py +4 -2
- package/tools/sweep_properties.py +4 -2
- package/tools/sweep_style_properties.py +4 -2
- package/tools/sweep_style_states.py +4 -2
- package/tools/theme_zip.py +6 -3
- package/tools/verify_browser.py +4 -2
- package/tools/verify_intro.py +4 -2
- package/tools/verify_rwd.py +4 -2
- package/sites/moksa.json +0 -63681
package/sites/_tokens.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""The design-system lab: what a token is, and why this install does not use variants.
|
|
2
|
+
|
|
3
|
+
Mosaic has three styling layers and most of a site belongs in the top two. The
|
|
4
|
+
bottom one - a style on one node - is what every page builder gives you and is the
|
|
5
|
+
wrong place to build a site. Above it:
|
|
6
|
+
|
|
7
|
+
- **collection variables**, which compile to real CSS custom properties on `:root`
|
|
8
|
+
and can be pointed at from 39 of the style properties;
|
|
9
|
+
- **variants**, which style every element of a KIND once, site-wide, from a catalog
|
|
10
|
+
of 156 entries you do not create but commit styles for.
|
|
11
|
+
|
|
12
|
+
This page shows the first layer working on itself: every swatch's colour IS the
|
|
13
|
+
token it names, read through `{"token": "--mk-…"}`, so the page cannot describe a
|
|
14
|
+
value it is not also displaying. Change the token and the page changes with it.
|
|
15
|
+
|
|
16
|
+
The second layer gets the honest treatment instead of a demonstration, because
|
|
17
|
+
**variants are theme-global and this install carries two brands on one theme**.
|
|
18
|
+
Committing a Heading 2 style here would restyle the other brand's pages too. That
|
|
19
|
+
constraint is worth more to a reader than a staged example would be, so the page
|
|
20
|
+
states it, and shows the part that IS observable: every element already carries its
|
|
21
|
+
variant's class name (`m-heading-2`, `m-button`), which is what a variant rule
|
|
22
|
+
would hook onto.
|
|
23
|
+
|
|
24
|
+
The token-referencable list is read from `data/style-properties.csv` at build time
|
|
25
|
+
rather than typed, so the page cannot drift from the table that was extracted from
|
|
26
|
+
the plugin.
|
|
27
|
+
"""
|
|
28
|
+
import csv
|
|
29
|
+
import io
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
from _moksa import (CJK, DISPLAY, MONO, RULE, T, TOKENS, bp, box, grid, ml, mono,
|
|
33
|
+
section, wrap)
|
|
34
|
+
|
|
35
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def tokenable():
|
|
39
|
+
path = os.path.join(_HERE, "..", "data", "style-properties.csv")
|
|
40
|
+
with io.open(path, encoding="utf-8") as fh:
|
|
41
|
+
return [r["property"] for r in csv.DictReader(fh) if r["tokenable"]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
TOKENABLE = tokenable()
|
|
45
|
+
|
|
46
|
+
# What each token is actually for. The catalog is the point of a token system:
|
|
47
|
+
# a name that says WHERE it is used outlives a name that says what colour it is.
|
|
48
|
+
ROLES = {
|
|
49
|
+
"--mk-ink": "正文和標題的字色",
|
|
50
|
+
"--mk-paper": "頁面底色",
|
|
51
|
+
"--mk-panel": "次要區塊的底色",
|
|
52
|
+
"--mk-accent": "強調:按鈕、重點標記",
|
|
53
|
+
"--mk-accent-ink": "強調色的深版,用在小字",
|
|
54
|
+
"--mk-ai": "藍摺:入口動畫的色塊",
|
|
55
|
+
"--mk-muted": "次要文字",
|
|
56
|
+
"--mk-rule": "分隔線",
|
|
57
|
+
"--mk-faint": "標籤、頁碁、最淺的那層字",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def swatch(name, meta):
|
|
62
|
+
value = meta.get("value", "")
|
|
63
|
+
dark = name in ("--mk-ink", "--mk-ai", "--mk-accent-ink", "--mk-muted",
|
|
64
|
+
"--mk-faint")
|
|
65
|
+
return box("tk-sw-" + name.strip("-"), {
|
|
66
|
+
"display": "flex", "flexDirection": "column", "rowGap": "0px",
|
|
67
|
+
"customDeclarations": "border:1px solid " + RULE + ";"}, [
|
|
68
|
+
# the colour field IS the token - nothing here restates the hex by hand
|
|
69
|
+
box("tk-chip-" + name.strip("-"), {
|
|
70
|
+
"height": "88px", "backgroundColor": {"token": name}}, []),
|
|
71
|
+
box("tk-meta-" + name.strip("-"), {
|
|
72
|
+
"paddingTop": "14px", "paddingBottom": "16px",
|
|
73
|
+
"paddingLeft": "14px", "paddingRight": "14px"}, [
|
|
74
|
+
mono(name, size="11px", color="--mk-ink", track="0.08em"),
|
|
75
|
+
mono(value, size="10px", color="--mk-faint", track="0.04em",
|
|
76
|
+
marginTop="6px"),
|
|
77
|
+
T("p", ROLES.get(name, ""), fontFamily=CJK, fontSize="12px",
|
|
78
|
+
lineHeight="1.7", color={"token": "--mk-muted"}, marginTop="8px"),
|
|
79
|
+
]),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
HEAD = section("tk-head", [wrap("tk-head-w", [
|
|
84
|
+
box("tk-head-pad", {"paddingTop": "116px"}, [], _m={"paddingTop": "26px"}),
|
|
85
|
+
mono("DESIGN SYSTEM / 設計系統", size="11px",
|
|
86
|
+
color="--mk-faint", track="0.26em"),
|
|
87
|
+
ml("h1", "一個值,\n改一次。",
|
|
88
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="62px", lineHeight="1.06",
|
|
89
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="18px",
|
|
90
|
+
_t={"fontSize": "48px"}, _m={"fontSize": "34px"}),
|
|
91
|
+
T("p",
|
|
92
|
+
"頁面編輯器通常只給你最底層:"
|
|
93
|
+
"把樣式寫在單一節點上。"
|
|
94
|
+
"那是起點,不是蓋網站的地方。"
|
|
95
|
+
"Mosaic 在它之上還有兩層:"
|
|
96
|
+
"集合變數(設計代幣)跟 variant(元件類別)。",
|
|
97
|
+
fontFamily=CJK, fontSize="16px", lineHeight="1.95",
|
|
98
|
+
color={"token": "--mk-ink"}, marginTop="22px", maxWidth="660px"),
|
|
99
|
+
T("p",
|
|
100
|
+
"下面每一塊色的背景都是它自己那個代幣——"
|
|
101
|
+
"不是手寫的色碼。所以這頁沒辦法描述一個"
|
|
102
|
+
"它沒有同時顯示出來的值。",
|
|
103
|
+
fontFamily=CJK, fontSize="14px", lineHeight="1.9",
|
|
104
|
+
color={"token": "--mk-muted"}, marginTop="12px", maxWidth="660px"),
|
|
105
|
+
box("tk-head-gap", {"paddingTop": "46px"}, []),
|
|
106
|
+
])], pad_y="0px")
|
|
107
|
+
|
|
108
|
+
SWATCHES = section("tk-sw", [wrap("tk-sw-w", [
|
|
109
|
+
grid("tk-sw-grid", 3, "18px",
|
|
110
|
+
[swatch(n, m) for n, m in TOKENS.items()], tcols=2, mcols=1),
|
|
111
|
+
box("tk-sw-foot", {"paddingTop": "64px"}, []),
|
|
112
|
+
])], bg="--mk-panel", pad_y="0px")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def step(num, title, body, codes):
|
|
116
|
+
return box("tk-step-%d" % num, {
|
|
117
|
+
"minWidth": "0px",
|
|
118
|
+
"display": "flex", "flexDirection": "column", "rowGap": "0px"}, [
|
|
119
|
+
mono("STEP %d" % num, size="10px", color="--mk-accent-ink", track="0.24em"),
|
|
120
|
+
ml("h3", title, fontFamily=DISPLAY, fontWeight="600", fontSize="18px",
|
|
121
|
+
lineHeight="1.3", color={"token": "--mk-ink"}, marginTop="10px"),
|
|
122
|
+
T("p", body, fontFamily=CJK, fontSize="13px", lineHeight="1.85",
|
|
123
|
+
color={"token": "--mk-muted"}, marginTop="8px"),
|
|
124
|
+
box("tk-step-code-%d" % num, {
|
|
125
|
+
"minWidth": "0px", "marginTop": "14px", "paddingTop": "12px", "paddingBottom": "12px",
|
|
126
|
+
"paddingLeft": "14px", "paddingRight": "14px",
|
|
127
|
+
"backgroundColor": {"token": "--mk-ink"},
|
|
128
|
+
"overflow": "auto"},
|
|
129
|
+
[mono(c, size="10px", color="rgb(250,250,247)", track="0.02em",
|
|
130
|
+
whiteSpace="pre", marginTop="4px" if i else "0px")
|
|
131
|
+
for i, c in enumerate(codes)]),
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
PIPELINE = section("tk-pipe", [wrap("tk-pipe-w", [
|
|
136
|
+
box("tk-pipe-pad", {"paddingTop": "62px"}, []),
|
|
137
|
+
mono("HOW IT COMPILES", size="10px", color="--mk-faint", track="0.26em"),
|
|
138
|
+
ml("h2", "從資料到 CSS,三步。",
|
|
139
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="32px", lineHeight="1.15",
|
|
140
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="14px",
|
|
141
|
+
_m={"fontSize": "24px"}),
|
|
142
|
+
box("tk-pipe-gap", {"paddingTop": "26px"}, []),
|
|
143
|
+
grid("tk-pipe-grid", 3, "20px", [
|
|
144
|
+
step(1, "宣告一個代幣",
|
|
145
|
+
"一個 collection variable。customProperty 是逐字輸出的,"
|
|
146
|
+
"所以前面那兩個減號必須自己寫。",
|
|
147
|
+
['{"name": "Ink",',
|
|
148
|
+
' "type": "color",',
|
|
149
|
+
' "customProperty": "--mk-ink",',
|
|
150
|
+
' "skinsData": {skin: {mode:',
|
|
151
|
+
' {"value": "rgb(22,24,28)"}}}}']),
|
|
152
|
+
step(2, "在節點上指向它",
|
|
153
|
+
"39 個樣式屬性接得住代幣引用,"
|
|
154
|
+
"寫法是一把鑰匙的物件。",
|
|
155
|
+
['"color": {"token": "--mk-ink"}',
|
|
156
|
+
'',
|
|
157
|
+
'# 實際傳的是變數 uuid:',
|
|
158
|
+
'"color": {"var": "<uuid>"}']),
|
|
159
|
+
step(3, "輸出的 CSS",
|
|
160
|
+
"代幣落在 :root,節點的規則用 var() 指過去。"
|
|
161
|
+
"改一個值,所有指過來的地方跟著變。",
|
|
162
|
+
[':root{',
|
|
163
|
+
' --mk-ink: rgb(22, 24, 28);',
|
|
164
|
+
'}',
|
|
165
|
+
'._d{ color: var(--mk-ink) }']),
|
|
166
|
+
], tcols=1, mcols=1),
|
|
167
|
+
box("tk-pipe-foot", {"paddingTop": "60px"}, []),
|
|
168
|
+
])], pad_y="0px")
|
|
169
|
+
|
|
170
|
+
PROPS = section("tk-props", [wrap("tk-props-w", [
|
|
171
|
+
box("tk-props-pad", {"paddingTop": "60px"}, []),
|
|
172
|
+
mono("TOKENABLE", size="10px", color="--mk-faint", track="0.26em"),
|
|
173
|
+
ml("h2", "哪些屬性接得住代幣?%d 個。" % len(TOKENABLE),
|
|
174
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="32px", lineHeight="1.15",
|
|
175
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="14px",
|
|
176
|
+
_m={"fontSize": "23px"}),
|
|
177
|
+
T("p",
|
|
178
|
+
"不是每個樣式屬性都行。"
|
|
179
|
+
"這份清單是從外掛原始碼萃取出來的表"
|
|
180
|
+
"(data/style-properties.csv)在建置時讀出來的,"
|
|
181
|
+
"不是手打的——所以它不會跟表脱節。",
|
|
182
|
+
fontFamily=CJK, fontSize="14px", lineHeight="1.9",
|
|
183
|
+
color={"token": "--mk-muted"}, marginTop="10px", maxWidth="620px"),
|
|
184
|
+
box("tk-props-list", {
|
|
185
|
+
"marginTop": "22px", "display": "flex", "flexWrap": "wrap",
|
|
186
|
+
"columnGap": "8px", "rowGap": "8px"},
|
|
187
|
+
[box("tk-prop-%d" % i, {
|
|
188
|
+
"paddingTop": "6px", "paddingBottom": "6px",
|
|
189
|
+
"paddingLeft": "10px", "paddingRight": "10px",
|
|
190
|
+
"customDeclarations": "border:1px solid " + RULE + ";"},
|
|
191
|
+
[mono(p, size="10px", color="--mk-ink", track="0.04em")])
|
|
192
|
+
for i, p in enumerate(TOKENABLE)]),
|
|
193
|
+
box("tk-props-foot", {"paddingTop": "64px"}, []),
|
|
194
|
+
])], bg="--mk-panel", pad_y="0px")
|
|
195
|
+
|
|
196
|
+
VARIANTS = section("tk-var", [wrap("tk-var-w", [
|
|
197
|
+
box("tk-var-pad", {"paddingTop": "60px"}, []),
|
|
198
|
+
mono("VARIANTS", size="10px", color="--mk-faint", track="0.26em"),
|
|
199
|
+
ml("h2", "第二層:把所有 h2 一次改完。",
|
|
200
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="32px", lineHeight="1.15",
|
|
201
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="14px",
|
|
202
|
+
_m={"fontSize": "24px"}),
|
|
203
|
+
T("p",
|
|
204
|
+
"Mosaic 內建一份 156 筆的 variant 目錄(Heading 2、"
|
|
205
|
+
"Button、Section、Body……)。你不創建 variant,"
|
|
206
|
+
"你是用目錄裡現成的 ID 提交一組樣式,"
|
|
207
|
+
"輸出就是一條全站規則:"
|
|
208
|
+
"h2,.m-heading-2{…}。排版節奏和元件預設屬於這一層。",
|
|
209
|
+
fontFamily=CJK, fontSize="15px", lineHeight="1.95",
|
|
210
|
+
color={"token": "--mk-ink"}, marginTop="14px", maxWidth="680px"),
|
|
211
|
+
box("tk-var-note", {
|
|
212
|
+
"marginTop": "22px", "paddingTop": "20px", "paddingBottom": "20px",
|
|
213
|
+
"paddingLeft": "22px", "paddingRight": "22px",
|
|
214
|
+
"backgroundColor": {"token": "--mk-paper"},
|
|
215
|
+
"customDeclarations": "border-left:3px solid rgb(255,90,54);"}, [
|
|
216
|
+
mono("為什麼這個站沒用這一層",
|
|
217
|
+
size="10px", color="--mk-accent-ink", track="0.2em"),
|
|
218
|
+
T("p",
|
|
219
|
+
"因為 variant 是主題全域的,"
|
|
220
|
+
"而這個安裝同時挂著兩個品牌。"
|
|
221
|
+
"在這裡提交一條 Heading 2 的規則,"
|
|
222
|
+
"另一個品牌的頁面也會跟著變——"
|
|
223
|
+
"所以這個站把排版烘進節點裡。"
|
|
224
|
+
"一個主題一個品牌的時候,"
|
|
225
|
+
"排版應該全部放在 variant 層,"
|
|
226
|
+
"而不是像這裡一樣每個節點都寫一遍。",
|
|
227
|
+
fontFamily=CJK, fontSize="13px", lineHeight="1.9",
|
|
228
|
+
color={"token": "--mk-ink"}, marginTop="10px", maxWidth="680px"),
|
|
229
|
+
]),
|
|
230
|
+
T("p",
|
|
231
|
+
"不過有一半是看得到的:"
|
|
232
|
+
"每個元素本來就帶著自己 variant 的類別名稱。"
|
|
233
|
+
"這頁的標題都有 m-heading-2 / m-heading-3,"
|
|
234
|
+
"按鈕都有 m-button——那就是 variant 規則會挖上去的地方。",
|
|
235
|
+
fontFamily=CJK, fontSize="14px", lineHeight="1.9",
|
|
236
|
+
color={"token": "--mk-muted"}, marginTop="18px", maxWidth="680px"),
|
|
237
|
+
box("tk-var-foot", {"paddingTop": "60px"}, []),
|
|
238
|
+
])], pad_y="0px")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def trap(num, title, body):
|
|
242
|
+
return box("tk-trap-%d" % num, {}, [
|
|
243
|
+
mono("TRAP %02d" % num, size="10px", color="--mk-accent-ink", track="0.22em"),
|
|
244
|
+
ml("h3", title, fontFamily=DISPLAY, fontWeight="600", fontSize="17px",
|
|
245
|
+
lineHeight="1.3", color={"token": "--mk-ink"}, marginTop="10px"),
|
|
246
|
+
T("p", body, fontFamily=CJK, fontSize="13px", lineHeight="1.85",
|
|
247
|
+
color={"token": "--mk-muted"}, marginTop="8px"),
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
TRAPS = section("tk-traps", [wrap("tk-traps-w", [
|
|
252
|
+
box("tk-traps-inner", {
|
|
253
|
+
"paddingTop": "54px", "paddingBottom": "84px",
|
|
254
|
+
"customDeclarations": "border-top:1px solid " + RULE + ";"}, [
|
|
255
|
+
mono("踩過的坑", size="10px", color="--mk-faint", track="0.26em"),
|
|
256
|
+
box("tk-traps-gap", {"paddingTop": "20px"}, []),
|
|
257
|
+
grid("tk-traps-grid", 3, "22px", [
|
|
258
|
+
trap(1, "customProperty 要自帶兩個減號",
|
|
259
|
+
"它是逐字輸出的。寫 brand 會得到 "
|
|
260
|
+
":root{brand: …} 和 background-color:var(brand),"
|
|
261
|
+
"兩個都不是合法 CSS,而且沒有任何錯誤訊息。"),
|
|
262
|
+
trap(2, "skinsData 形狀寫錯不會失敗",
|
|
263
|
+
"變數照樣建立,:root 照樣有宣告,"
|
|
264
|
+
"但值是子工廂的預設——顏色是 #FFF。"
|
|
265
|
+
"所以代幣變白色的時候,"
|
|
266
|
+
"意思是值根本沒傳到。"),
|
|
267
|
+
trap(3, "改值會留下舊的",
|
|
268
|
+
"改一個代幣的值,舊的宣告不會自己消失:"
|
|
269
|
+
":root 裡會同時出現兩行。"
|
|
270
|
+
"要用 status:\"delete\" 明確刪掉,"
|
|
271
|
+
"清空節點是不會動到它的。"),
|
|
272
|
+
], tcols=1, mcols=1),
|
|
273
|
+
]),
|
|
274
|
+
])], pad_y="0px")
|
|
275
|
+
|
|
276
|
+
TREE = {"type": "div", "data": {"attrID": "tk-page"},
|
|
277
|
+
"children": [HEAD, SWATCHES, PIPELINE, PROPS, VARIANTS, TRAPS]}
|
package/tools/build_page.py
CHANGED
|
@@ -39,6 +39,7 @@ refused outright unless --force, because committing one is how you get a 200-res
|
|
|
39
39
|
page that serves a 54-byte error string. After committing, the page is fetched and its
|
|
40
40
|
size checked - the only reliable signal that the write actually worked.
|
|
41
41
|
"""
|
|
42
|
+
import mosaic_config
|
|
42
43
|
import argparse
|
|
43
44
|
import csv
|
|
44
45
|
import json
|
|
@@ -559,11 +560,12 @@ def build(client, cfg, spec, force):
|
|
|
559
560
|
|
|
560
561
|
if __name__ == "__main__":
|
|
561
562
|
ap = argparse.ArgumentParser()
|
|
562
|
-
ap.add_argument("--config",
|
|
563
|
+
ap.add_argument("--config",
|
|
564
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
563
565
|
ap.add_argument("--spec", required=True)
|
|
564
566
|
ap.add_argument("--force", action="store_true", help="skip the placement/outcome checks")
|
|
565
567
|
a = ap.parse_args()
|
|
566
|
-
cfg =
|
|
568
|
+
cfg = mosaic_config.load(a.config)
|
|
567
569
|
spec = json.load(open(a.spec, encoding="utf-8"))
|
|
568
570
|
client = Client(cfg)
|
|
569
571
|
build(client, cfg, spec, a.force)
|
package/tools/build_site.py
CHANGED
|
@@ -28,6 +28,7 @@ a theme.
|
|
|
28
28
|
Pages are bound to WordPress posts by slug, so the posts must already exist. The theme
|
|
29
29
|
is committed once, before anything references a token.
|
|
30
30
|
"""
|
|
31
|
+
import mosaic_config
|
|
31
32
|
import argparse
|
|
32
33
|
import json
|
|
33
34
|
import os
|
|
@@ -374,10 +375,11 @@ def cache_check(url, _fresh_bytes=None):
|
|
|
374
375
|
|
|
375
376
|
def main():
|
|
376
377
|
ap = argparse.ArgumentParser()
|
|
377
|
-
ap.add_argument("--config",
|
|
378
|
+
ap.add_argument("--config",
|
|
379
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
378
380
|
ap.add_argument("--site", required=True)
|
|
379
381
|
a = ap.parse_args()
|
|
380
|
-
cfg =
|
|
382
|
+
cfg = mosaic_config.load(a.config)
|
|
381
383
|
site = json.load(open(a.site, encoding="utf-8"))
|
|
382
384
|
client = Client(cfg)
|
|
383
385
|
surface = Surface()
|
package/tools/copy_styles.py
CHANGED
|
@@ -26,6 +26,7 @@ Always shows the diff and asks, unless --yes. A style copy is not reversible fro
|
|
|
26
26
|
this tool - the previous value is gone once the commit lands - so it prints the
|
|
27
27
|
before and after of every target first.
|
|
28
28
|
"""
|
|
29
|
+
import mosaic_config
|
|
29
30
|
import argparse
|
|
30
31
|
import json
|
|
31
32
|
import os
|
|
@@ -96,7 +97,8 @@ def merge(dst, src):
|
|
|
96
97
|
|
|
97
98
|
def main():
|
|
98
99
|
ap = argparse.ArgumentParser()
|
|
99
|
-
ap.add_argument("--config",
|
|
100
|
+
ap.add_argument("--config",
|
|
101
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
100
102
|
ap.add_argument("--from", dest="src", required=True, help="source attrID")
|
|
101
103
|
ap.add_argument("--to", help="comma-separated target attrIDs")
|
|
102
104
|
ap.add_argument("--to-prefix", help="every attrID starting with this, except the source")
|
|
@@ -107,7 +109,7 @@ def main():
|
|
|
107
109
|
ap.add_argument("--yes", action="store_true")
|
|
108
110
|
a = ap.parse_args()
|
|
109
111
|
|
|
110
|
-
cfg =
|
|
112
|
+
cfg = mosaic_config.load(a.config)
|
|
111
113
|
client = Client(cfg)
|
|
112
114
|
|
|
113
115
|
if a.to_prefix:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Where a tool gets the site URL and the REST session, in one place.
|
|
3
|
+
|
|
4
|
+
Every tool here talks to ONE WordPress site over its REST API, and needs three
|
|
5
|
+
things to do it: the site's base URL, a logged-in cookie, and a matching
|
|
6
|
+
`wp_rest` nonce. `tools/mint_session.php` prints all three from WP-CLI on the
|
|
7
|
+
server; nothing here ever goes looking for them.
|
|
8
|
+
|
|
9
|
+
Two ways to supply them, and the environment wins:
|
|
10
|
+
|
|
11
|
+
MOSAIC_SITE_URL https://example.com
|
|
12
|
+
MOSAIC_REST_COOKIE wordpress_logged_in_…=…
|
|
13
|
+
MOSAIC_REST_NONCE 0a0326da96
|
|
14
|
+
MOSAIC_THEME_ID the theme uuid, when a tool needs one
|
|
15
|
+
|
|
16
|
+
or a JSON file passed with `--config`:
|
|
17
|
+
|
|
18
|
+
{"base": "https://example.com", "cookie": "…", "nonce": "…",
|
|
19
|
+
"themeID": "…", "version": "1.0.9"}
|
|
20
|
+
|
|
21
|
+
The environment path exists so the credential never has to be written to disk at
|
|
22
|
+
all. When this skill is installed as a plugin, its `userConfig` prompts for these
|
|
23
|
+
values - the cookie and the nonce declared `sensitive: true`, so Claude Code masks
|
|
24
|
+
them and keeps them in the platform's credential store rather than in
|
|
25
|
+
`settings.json` - and the user exports them for the tool run. A value is asked
|
|
26
|
+
FOR, never read from wherever the user happened to leave it.
|
|
27
|
+
|
|
28
|
+
The file is still supported because a sweep over a scratch site is easier to
|
|
29
|
+
repeat from one, and because `--config` is what every example in this skill shows.
|
|
30
|
+
Nothing in either path is sent anywhere but the site named in `base`.
|
|
31
|
+
"""
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
|
|
35
|
+
ENV = {
|
|
36
|
+
"base": "MOSAIC_SITE_URL",
|
|
37
|
+
"cookie": "MOSAIC_REST_COOKIE",
|
|
38
|
+
"nonce": "MOSAIC_REST_NONCE",
|
|
39
|
+
"themeID": "MOSAIC_THEME_ID",
|
|
40
|
+
"version": "MOSAIC_VERSION",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
REQUIRED = ("base", "cookie", "nonce")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load(path=None, require=REQUIRED):
|
|
47
|
+
"""Return the config dict, environment over file.
|
|
48
|
+
|
|
49
|
+
`path` may be None, in which case the environment has to carry everything.
|
|
50
|
+
A missing required key is a hard error naming both ways to supply it, rather
|
|
51
|
+
than a KeyError five frames deep in a request.
|
|
52
|
+
"""
|
|
53
|
+
cfg = {}
|
|
54
|
+
if path:
|
|
55
|
+
with open(path, encoding="utf-8") as fh:
|
|
56
|
+
cfg = json.load(fh)
|
|
57
|
+
for key, var in ENV.items():
|
|
58
|
+
value = os.environ.get(var)
|
|
59
|
+
if value:
|
|
60
|
+
cfg[key] = value
|
|
61
|
+
|
|
62
|
+
missing = [k for k in (require or ()) if not cfg.get(k)]
|
|
63
|
+
if missing:
|
|
64
|
+
raise SystemExit(
|
|
65
|
+
"missing %s.\n"
|
|
66
|
+
"Supply them in the environment (%s) or in a JSON file passed with "
|
|
67
|
+
"--config.\n"
|
|
68
|
+
"`php tools/mint_session.php` run through WP-CLI on the site prints a "
|
|
69
|
+
"cookie and a matching nonce."
|
|
70
|
+
% (", ".join(missing), ", ".join(ENV[k] for k in missing)))
|
|
71
|
+
return cfg
|
package/tools/probe.py
CHANGED
|
@@ -31,6 +31,7 @@ that "worked" can be checked against what it actually emitted rather than truste
|
|
|
31
31
|
COMMIT_5xx PHP fatal during commit
|
|
32
32
|
BROKE_PAGE committed, and the page stopped rendering
|
|
33
33
|
"""
|
|
34
|
+
import mosaic_config
|
|
34
35
|
import argparse
|
|
35
36
|
import csv
|
|
36
37
|
import json
|
|
@@ -135,10 +136,11 @@ def run(client, cfg, cases, out_path):
|
|
|
135
136
|
|
|
136
137
|
if __name__ == "__main__":
|
|
137
138
|
ap = argparse.ArgumentParser()
|
|
138
|
-
ap.add_argument("--config",
|
|
139
|
+
ap.add_argument("--config",
|
|
140
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
139
141
|
ap.add_argument("--cases", required=True)
|
|
140
142
|
ap.add_argument("--out")
|
|
141
143
|
a = ap.parse_args()
|
|
142
|
-
cfg =
|
|
144
|
+
cfg = mosaic_config.load(a.config)
|
|
143
145
|
cases = json.load(open(a.cases, encoding="utf-8"))
|
|
144
146
|
run(Client(cfg), cfg, cases, a.out)
|
package/tools/probe_accordion.py
CHANGED
|
@@ -16,6 +16,7 @@ is only worth writing down next to "and here is what happens when it is not".
|
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import mosaic_config
|
|
19
20
|
import argparse
|
|
20
21
|
import csv
|
|
21
22
|
import json
|
|
@@ -90,13 +91,14 @@ def place(client, cfg, surface, slug, post, node, label):
|
|
|
90
91
|
|
|
91
92
|
def main():
|
|
92
93
|
ap = argparse.ArgumentParser()
|
|
93
|
-
ap.add_argument("--config",
|
|
94
|
+
ap.add_argument("--config",
|
|
95
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
94
96
|
ap.add_argument("--slug", required=True)
|
|
95
97
|
ap.add_argument("--post", type=int, required=True)
|
|
96
98
|
ap.add_argument("--csv")
|
|
97
99
|
a = ap.parse_args()
|
|
98
100
|
|
|
99
|
-
cfg =
|
|
101
|
+
cfg = mosaic_config.load(a.config)
|
|
100
102
|
client = Client(cfg)
|
|
101
103
|
rows, failed = [], 0
|
|
102
104
|
|
|
@@ -41,6 +41,7 @@ text in the delivered page - because one instance rendering proves less than two
|
|
|
41
41
|
"""
|
|
42
42
|
from __future__ import annotations
|
|
43
43
|
|
|
44
|
+
import mosaic_config
|
|
44
45
|
import argparse
|
|
45
46
|
import csv
|
|
46
47
|
import json
|
|
@@ -89,13 +90,14 @@ def fetch(url):
|
|
|
89
90
|
|
|
90
91
|
def main():
|
|
91
92
|
ap = argparse.ArgumentParser()
|
|
92
|
-
ap.add_argument("--config",
|
|
93
|
+
ap.add_argument("--config",
|
|
94
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
93
95
|
ap.add_argument("--post", type=int, required=True)
|
|
94
96
|
ap.add_argument("--slug", required=True)
|
|
95
97
|
ap.add_argument("--csv")
|
|
96
98
|
a = ap.parse_args()
|
|
97
99
|
|
|
98
|
-
cfg =
|
|
100
|
+
cfg = mosaic_config.load(a.config)
|
|
99
101
|
client, surface = Client(cfg), Surface()
|
|
100
102
|
rows, failed = [], 0
|
|
101
103
|
|
|
@@ -47,6 +47,7 @@ Statuses
|
|
|
47
47
|
"""
|
|
48
48
|
from __future__ import annotations
|
|
49
49
|
|
|
50
|
+
import mosaic_config
|
|
50
51
|
import argparse
|
|
51
52
|
import csv
|
|
52
53
|
import json
|
|
@@ -279,7 +280,8 @@ def second_pass(client, master, template, cases):
|
|
|
279
280
|
|
|
280
281
|
def main():
|
|
281
282
|
ap = argparse.ArgumentParser()
|
|
282
|
-
ap.add_argument("--config",
|
|
283
|
+
ap.add_argument("--config",
|
|
284
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
283
285
|
ap.add_argument("--post", type=int, required=True)
|
|
284
286
|
ap.add_argument("--slug", required=True)
|
|
285
287
|
ap.add_argument("--csv")
|
|
@@ -287,7 +289,7 @@ def main():
|
|
|
287
289
|
help="re-commit the same nodes once more before judging")
|
|
288
290
|
a = ap.parse_args()
|
|
289
291
|
|
|
290
|
-
cfg =
|
|
292
|
+
cfg = mosaic_config.load(a.config)
|
|
291
293
|
client, surface = Client(cfg), Surface()
|
|
292
294
|
cases = candidates()
|
|
293
295
|
|
|
@@ -42,6 +42,7 @@ as passes would be the blind-spot-scored-as-success failure this file was writte
|
|
|
42
42
|
avoid; leaving them as SKIPPED understates the evidence by nearly seven hundred rows.
|
|
43
43
|
So they get their own status, and it carries the count and the file that holds it.
|
|
44
44
|
"""
|
|
45
|
+
import mosaic_config
|
|
45
46
|
import argparse
|
|
46
47
|
import csv
|
|
47
48
|
import json
|
|
@@ -158,12 +159,13 @@ def judge(prop, value, tag):
|
|
|
158
159
|
|
|
159
160
|
def main():
|
|
160
161
|
ap = argparse.ArgumentParser()
|
|
161
|
-
ap.add_argument("--config",
|
|
162
|
+
ap.add_argument("--config",
|
|
163
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
162
164
|
ap.add_argument("--page", required=True)
|
|
163
165
|
ap.add_argument("--csv")
|
|
164
166
|
a = ap.parse_args()
|
|
165
167
|
|
|
166
|
-
cfg =
|
|
168
|
+
cfg = mosaic_config.load(a.config)
|
|
167
169
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
168
170
|
props = list(csv.DictReader(
|
|
169
171
|
open(os.path.join(here, "..", "data", "node-properties.csv"), encoding="utf-8")))
|
|
@@ -27,6 +27,7 @@ tree. This costs one page load per type and is the only way the numbers mean any
|
|
|
27
27
|
BROKE_PAGE committed, and the page stopped rendering - the silent-failure case
|
|
28
28
|
REJECTED the commit came back with an `exceptions` body (HTTP 200)
|
|
29
29
|
"""
|
|
30
|
+
import mosaic_config
|
|
30
31
|
import argparse
|
|
31
32
|
import csv
|
|
32
33
|
import json
|
|
@@ -406,7 +407,8 @@ def sweep(client, cfg, types, out_path):
|
|
|
406
407
|
|
|
407
408
|
if __name__ == "__main__":
|
|
408
409
|
ap = argparse.ArgumentParser()
|
|
409
|
-
ap.add_argument("--config",
|
|
410
|
+
ap.add_argument("--config",
|
|
411
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
410
412
|
ap.add_argument("--setup", action="store_true")
|
|
411
413
|
ap.add_argument("--sweep", action="store_true")
|
|
412
414
|
ap.add_argument("--types", help="comma-separated subset; overrides --edition")
|
|
@@ -416,7 +418,7 @@ if __name__ == "__main__":
|
|
|
416
418
|
ap.add_argument("--out", default="../data/node-verification.csv")
|
|
417
419
|
a = ap.parse_args()
|
|
418
420
|
|
|
419
|
-
cfg =
|
|
421
|
+
cfg = mosaic_config.load(a.config)
|
|
420
422
|
client = Client(cfg)
|
|
421
423
|
if a.setup:
|
|
422
424
|
setup(client, cfg, a.config)
|
|
@@ -36,6 +36,7 @@ NO_EFFECT is not a failure. It is the honest answer for a property whose effect
|
|
|
36
36
|
probe page cannot show, and it is reported as itself instead of being quietly counted
|
|
37
37
|
as a pass.
|
|
38
38
|
"""
|
|
39
|
+
import mosaic_config
|
|
39
40
|
import argparse
|
|
40
41
|
import csv
|
|
41
42
|
import json
|
|
@@ -206,10 +207,11 @@ def run(client, cfg, jobs, out_path):
|
|
|
206
207
|
|
|
207
208
|
if __name__ == "__main__":
|
|
208
209
|
ap = argparse.ArgumentParser()
|
|
209
|
-
ap.add_argument("--config",
|
|
210
|
+
ap.add_argument("--config",
|
|
211
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
210
212
|
ap.add_argument("--carriers", default="text,button,div,section,image,icon",
|
|
211
213
|
help="types used to probe the inherited property set")
|
|
212
214
|
ap.add_argument("--out", default="../data/property-verification.csv")
|
|
213
215
|
a = ap.parse_args()
|
|
214
|
-
cfg =
|
|
216
|
+
cfg = mosaic_config.load(a.config)
|
|
215
217
|
run(Client(cfg), cfg, load_surface(a.carriers.split(",")), a.out)
|
|
@@ -46,6 +46,7 @@ Statuses
|
|
|
46
46
|
`SKIPPED` and `DIFFERENT` are never folded into a pass rate. A sweep that scores its
|
|
47
47
|
own blind spots as successes is the thing this skill exists to argue against.
|
|
48
48
|
"""
|
|
49
|
+
import mosaic_config
|
|
49
50
|
import argparse
|
|
50
51
|
import csv
|
|
51
52
|
import json
|
|
@@ -133,12 +134,13 @@ def test_value(row):
|
|
|
133
134
|
|
|
134
135
|
def main():
|
|
135
136
|
ap = argparse.ArgumentParser()
|
|
136
|
-
ap.add_argument("--config",
|
|
137
|
+
ap.add_argument("--config",
|
|
138
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
137
139
|
ap.add_argument("--page", required=True, help="slug of a page that renders")
|
|
138
140
|
ap.add_argument("--csv")
|
|
139
141
|
a = ap.parse_args()
|
|
140
142
|
|
|
141
|
-
cfg =
|
|
143
|
+
cfg = mosaic_config.load(a.config)
|
|
142
144
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
143
145
|
props = list(csv.DictReader(
|
|
144
146
|
open(os.path.join(here, "..", "data", "style-properties.csv"),
|
|
@@ -32,6 +32,7 @@ Statuses
|
|
|
32
32
|
"""
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
|
+
import mosaic_config
|
|
35
36
|
import argparse
|
|
36
37
|
import csv
|
|
37
38
|
import json
|
|
@@ -127,13 +128,14 @@ def norm(text):
|
|
|
127
128
|
|
|
128
129
|
def main():
|
|
129
130
|
ap = argparse.ArgumentParser()
|
|
130
|
-
ap.add_argument("--config",
|
|
131
|
+
ap.add_argument("--config",
|
|
132
|
+
help="JSON with base/cookie/nonce; omit it and the MOSAIC_SITE_URL / MOSAIC_REST_COOKIE / MOSAIC_REST_NONCE environment variables supply them instead")
|
|
131
133
|
ap.add_argument("--post", type=int, required=True)
|
|
132
134
|
ap.add_argument("--slug", required=True)
|
|
133
135
|
ap.add_argument("--csv")
|
|
134
136
|
a = ap.parse_args()
|
|
135
137
|
|
|
136
|
-
cfg =
|
|
138
|
+
cfg = mosaic_config.load(a.config)
|
|
137
139
|
client, surface = Client(cfg), Surface()
|
|
138
140
|
cases = plan()
|
|
139
141
|
testable = [c for c in cases if not c["blocked"] and c["host"]]
|