@olegkoval/agent-skills 1.28.0 → 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.
Files changed (98) hide show
  1. package/.claude-plugin/plugin.json +7 -2
  2. package/.cursor-plugin/index.json +26 -1
  3. package/.grok-plugin/index.json +26 -1
  4. package/.kiro/steering/garmin-watchface.md +83 -31
  5. package/.windsurf/rules/garmin-watchface.md +83 -31
  6. package/README.md +8 -3
  7. package/catalog/skills.json +117 -1
  8. package/collections/software-development.json +2 -1
  9. package/package.json +1 -1
  10. package/packages/software-development/branch-cleanup/SKILL.md +204 -0
  11. package/packages/software-development/branch-cleanup/adapters/claude/plugin.json +5 -0
  12. package/packages/software-development/branch-cleanup/adapters/claude/skills/branch-cleanup/SKILL.md +205 -0
  13. package/packages/software-development/branch-cleanup/adapters/codex/README.md +19 -0
  14. package/packages/software-development/branch-cleanup/adapters/cursor/plugin.json +6 -0
  15. package/packages/software-development/branch-cleanup/adapters/cursor/skills/branch-cleanup/SKILL.md +205 -0
  16. package/packages/software-development/branch-cleanup/adapters/grok/plugin.json +6 -0
  17. package/packages/software-development/branch-cleanup/adapters/grok/skills/branch-cleanup/SKILL.md +205 -0
  18. package/packages/software-development/morning-routine/SKILL.md +149 -0
  19. package/packages/software-development/morning-routine/adapters/claude/plugin.json +5 -0
  20. package/packages/software-development/morning-routine/adapters/claude/skills/morning-routine/SKILL.md +150 -0
  21. package/packages/software-development/morning-routine/adapters/codex/README.md +19 -0
  22. package/packages/software-development/morning-routine/adapters/cursor/plugin.json +6 -0
  23. package/packages/software-development/morning-routine/adapters/cursor/skills/morning-routine/SKILL.md +150 -0
  24. package/packages/software-development/morning-routine/adapters/grok/plugin.json +6 -0
  25. package/packages/software-development/morning-routine/adapters/grok/skills/morning-routine/SKILL.md +150 -0
  26. package/packages/software-development/pr-finalize-complete/adapters/claude/plugin.json +1 -1
  27. package/packages/software-development/pr-finalize-complete/adapters/codex/README.md +7 -11
  28. package/packages/software-development/pr-finalize-complete/adapters/cursor/plugin.json +1 -1
  29. package/packages/software-development/pr-finalize-complete/adapters/grok/plugin.json +1 -1
  30. package/packages/software-development/release-day/SKILL.md +286 -0
  31. package/packages/software-development/release-day/adapters/claude/plugin.json +5 -0
  32. package/packages/software-development/release-day/adapters/claude/skills/release-day/SKILL.md +287 -0
  33. package/packages/software-development/release-day/adapters/codex/README.md +19 -0
  34. package/packages/software-development/release-day/adapters/cursor/plugin.json +6 -0
  35. package/packages/software-development/release-day/adapters/cursor/skills/release-day/SKILL.md +287 -0
  36. package/packages/software-development/release-day/adapters/grok/plugin.json +6 -0
  37. package/packages/software-development/release-day/adapters/grok/skills/release-day/SKILL.md +287 -0
  38. package/packages/software-development/vinted-listing/SKILL.md +58 -0
  39. package/packages/software-development/vinted-listing/adapters/claude/plugin.json +5 -0
  40. package/packages/software-development/vinted-listing/adapters/claude/skills/vinted-listing/SKILL.md +59 -0
  41. package/packages/software-development/vinted-listing/adapters/claude/skills/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
  42. package/packages/software-development/vinted-listing/adapters/codex/README.md +3 -0
  43. package/packages/software-development/vinted-listing/adapters/cursor/plugin.json +6 -0
  44. package/packages/software-development/vinted-listing/adapters/cursor/skills/vinted-listing/SKILL.md +59 -0
  45. package/packages/software-development/vinted-listing/adapters/cursor/skills/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
  46. package/packages/software-development/vinted-listing/adapters/grok/plugin.json +6 -0
  47. package/packages/software-development/vinted-listing/adapters/grok/skills/vinted-listing/SKILL.md +59 -0
  48. package/packages/software-development/vinted-listing/adapters/grok/skills/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
  49. package/packages/software-development/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
  50. package/packages/wearables/garmin-watchface/SKILL.md +82 -35
  51. package/packages/wearables/garmin-watchface/adapters/claude/plugin.json +1 -1
  52. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/SKILL.md +82 -35
  53. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/design-proposals.md +99 -0
  54. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/devices.md +112 -0
  55. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/display.md +78 -3
  56. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/inspiration-wizard.md +140 -0
  57. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/layout.md +28 -0
  58. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/publishing.md +134 -5
  59. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/simulator.md +86 -2
  60. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-inspire +329 -0
  61. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-mock +502 -0
  62. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-release +12 -6
  63. package/packages/wearables/garmin-watchface/adapters/cursor/plugin.json +1 -1
  64. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/SKILL.md +82 -35
  65. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/design-proposals.md +99 -0
  66. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/devices.md +112 -0
  67. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/display.md +78 -3
  68. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/inspiration-wizard.md +140 -0
  69. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/layout.md +28 -0
  70. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/publishing.md +134 -5
  71. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/simulator.md +86 -2
  72. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-inspire +329 -0
  73. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-mock +502 -0
  74. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-release +12 -6
  75. package/packages/wearables/garmin-watchface/adapters/grok/plugin.json +1 -1
  76. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/SKILL.md +82 -35
  77. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/design-proposals.md +99 -0
  78. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/devices.md +112 -0
  79. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/display.md +78 -3
  80. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/inspiration-wizard.md +140 -0
  81. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/layout.md +28 -0
  82. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/publishing.md +134 -5
  83. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/simulator.md +86 -2
  84. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-inspire +329 -0
  85. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-mock +502 -0
  86. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-release +12 -6
  87. package/packages/wearables/garmin-watchface/adapters/kiro/steering/garmin-watchface.md +83 -31
  88. package/packages/wearables/garmin-watchface/adapters/windsurf/rules/garmin-watchface.md +83 -31
  89. package/packages/wearables/garmin-watchface/references/design-proposals.md +99 -0
  90. package/packages/wearables/garmin-watchface/references/devices.md +112 -0
  91. package/packages/wearables/garmin-watchface/references/display.md +78 -3
  92. package/packages/wearables/garmin-watchface/references/inspiration-wizard.md +140 -0
  93. package/packages/wearables/garmin-watchface/references/layout.md +28 -0
  94. package/packages/wearables/garmin-watchface/references/publishing.md +134 -5
  95. package/packages/wearables/garmin-watchface/references/simulator.md +86 -2
  96. package/packages/wearables/garmin-watchface/scripts/ciq-inspire +329 -0
  97. package/packages/wearables/garmin-watchface/scripts/ciq-mock +502 -0
  98. package/packages/wearables/garmin-watchface/scripts/ciq-release +12 -6
@@ -222,3 +222,31 @@ digit nought" — they are different pictures.
222
222
  checked against a boundary: a stroke straddles its path, putting half its width
223
223
  outside what you verified.
224
224
  - Order matters when compositing. Pasting a square image over a ring erases it.
225
+
226
+ ## Fonts are tiers, not pixel heights
227
+
228
+ `FONT_XTINY` is not a size — it is a request, and the device answers with a
229
+ glyph proportionate to its own screen. A 454px watch supplies a materially
230
+ taller `FONT_XTINY` than a 260px one, unasked.
231
+
232
+ So when text looks wrong at a new resolution, **the bug is a length that failed
233
+ to scale, never the font**. Do not branch on screen size to pick a bigger tier:
234
+ that double-scales, and labels end up wider than the containers naming them.
235
+ Tried on a real face, `FONT_SMALL` on a 1.75x screen made a four-character
236
+ subdial label wider than the arc it sat inside; plain `FONT_XTINY` was correct
237
+ on sight.
238
+
239
+ The cheap check before theorising: does a sibling face that never touched its
240
+ fonts render correctly at the new size? If yes, fonts are not your problem.
241
+
242
+ **Where a font tier IS a real constraint, the limit is the container.** A label
243
+ inside a 30-unit arc has to fit that arc at every resolution, which is a
244
+ different question from how it scales. Derive placement from the MEASURED glyph:
245
+
246
+ ```monkeyc
247
+ var h = dc.getFontHeight(Fonts.label());
248
+ var y = cy + DIGIT_TOP_DY - Layout.grid(2) - h; // seat it on its own height
249
+ ```
250
+
251
+ A fixed offset scales with the screen while the glyph does not, which puts the
252
+ label on top of its own digits at one size and adrift at another.
@@ -1,9 +1,9 @@
1
1
  # Publishing through the browser
2
2
 
3
- `references/store.md` covers *what* the listing needs. This covers *driving the
3
+ `reference/store.md` covers *what* the listing needs. This covers *driving the
4
4
  portal*, which is the only way in — Garmin publishes no submission API.
5
5
 
6
- Run `scripts/ciq-release` before opening a browser. Everything it checks is
6
+ Run `bin/ciq-release` before opening a browser. Everything it checks is
7
7
  something you would otherwise discover halfway through the form.
8
8
 
9
9
  ## The portal is automatable
@@ -44,6 +44,113 @@ Three separate traps in that block:
44
44
  Verify by checking the URL after `goto`: an unauthenticated session is bounced
45
45
  to `sso.garmin.com/portal/sso/...`, an authenticated one stays put.
46
46
 
47
+ ### When cookie import fails, `browse connect` is the answer
48
+
49
+ Cookie import is the fast path and it fails in two common ways, both of which
50
+ look identical from the outside (`Imported 0 cookies`):
51
+
52
+ - **The browser encrypts its cookie store** and browse cannot decrypt it. Comet
53
+ is one. Reading the key out of the macOS keychain yourself is credential
54
+ extraction, an agent permission-classifier will block it, and it should.
55
+ - **You are signed into a different browser than you think.** Check the cookie
56
+ DB's mtime rather than guessing — the store of the browser you just used is
57
+ written seconds ago:
58
+
59
+ ```bash
60
+ for n in "Google/Chrome/Default" "Comet/Default" "BraveSoftware/Brave-Browser/Default"; do
61
+ f="$HOME/Library/Application Support/$n/Cookies"
62
+ [ -f "$f" ] && echo "$n $(stat -f '%Sm' -t '%H:%M:%S' "$f")"
63
+ done
64
+ ```
65
+
66
+ The path that works regardless is to put the login **inside the browser browse
67
+ drives**:
68
+
69
+ ```bash
70
+ $B connect # headed Chromium under browse's control
71
+ $B goto https://apps.garmin.com/en-US/developer/dashboard
72
+ $B focus # then the human signs in, in that window
73
+ ```
74
+
75
+ No import, no keychain, no profile mismatch. `handoff` is NOT equivalent — it
76
+ opens a *separate* browser whose session `resume` does not inherit.
77
+
78
+ **Do not trust window counts from System Events.** `count of windows` returns 0
79
+ for Chromium browsers without macOS automation permission, even while the window
80
+ is plainly on screen — Finder returns a real number, so the API looks like it is
81
+ working. Diagnosing "the browser never opened" from that reading sends you off
82
+ fixing the wrong thing.
83
+
84
+ ### Protect the daemon session
85
+
86
+ Every browse daemon restart destroys the authenticated context and you start
87
+ over. Restarts are triggered by crashes and by some error paths, and the giveaway
88
+ is `[browse] Starting server...` in otherwise-unrelated output. Check
89
+ `$B url` after any surprise: `about:blank` means the context is gone.
90
+
91
+ Practical consequence: between the human signing in and the work being finished,
92
+ run as few commands as possible, and none that can throw.
93
+
94
+ ### Uploads are path-restricted
95
+
96
+ `$B upload` refuses any file outside `/private/tmp` or the current repo:
97
+
98
+ ```
99
+ Path must be within: /private/tmp, /Users/.../some-other-repo
100
+ ```
101
+
102
+ Note it validates against the shell's *current* directory, which may be a
103
+ different project than the one you are publishing. Copy the package first:
104
+
105
+ ```bash
106
+ cp bin/<name>.iq /private/tmp/ && $B upload "input[type=file]" /private/tmp/<name>.iq
107
+ ```
108
+
109
+ ### Developer URLs, and the two forms
110
+
111
+ The dashboard's app links go to the PUBLIC page (`/apps/<appId>`), not the
112
+ editor. The developer routes are:
113
+
114
+ ```
115
+ https://apps.garmin.com/developer/<devId>/apps/<appId>/update # new version
116
+ https://apps.garmin.com/developer/<devId>/apps/<appId>/edit # listing text
117
+ ```
118
+
119
+ `<devId>` is stable across your apps — read it off any editor URL once. Guessing
120
+ `/developer/<appId>/edit` returns 404.
121
+
122
+ Field handles, current as of mid-2026:
123
+
124
+ | What | Selector |
125
+ | --- | --- |
126
+ | Package | `input[type=file]` |
127
+ | Version string | third `input[type=text]` on `/update` |
128
+ | Description | `textarea[name="app-desc-en"]` |
129
+ | What's New | `textarea[name="app-whats-new-en"]` |
130
+
131
+ React controls these, so assigning `.value` alone does not register. Use the
132
+ native setter and dispatch both events:
133
+
134
+ ```js
135
+ var set = Object.getOwnPropertyDescriptor(
136
+ window.HTMLTextAreaElement.prototype, 'value').set;
137
+ set.call(el, text);
138
+ el.dispatchEvent(new Event('input', { bubbles: true }));
139
+ el.dispatchEvent(new Event('change', { bubbles: true }));
140
+ ```
141
+
142
+ Pass long copy through a staged `<script type="application/json">` node rather
143
+ than interpolating it into a `js` one-liner — multi-line store copy will not
144
+ survive shell and JS quoting intact.
145
+
146
+ ### The public page lags the portal
147
+
148
+ After a successful publish the portal reports `Status: Verified, Signature:
149
+ Verified` and expands the product list immediately. The **public** app page can
150
+ still show the previous device list minutes later. The portal is authoritative;
151
+ do not re-upload because the public page has not caught up, and do not report
152
+ the new devices as live until it has.
153
+
47
154
  ## "The Connect IQ store is currently in maintenance mode"
48
155
 
49
156
  This string is in the DOM of the dashboard and of every app page, next to
@@ -68,8 +175,8 @@ the next one.
68
175
 
69
176
  ```bash
70
177
  $B upload "input[type=file]" "$(pwd)/bin/<name>.iq"
71
- $B fill "input[id*='version'],input[name*='version']" "2.0"
72
- $B click "button:has-text('Upload and publish')"
178
+ $B fill @e43 "2.0"
179
+ $B click @e46 # "Upload and publish"
73
180
  ```
74
181
 
75
182
  On success the page reports **Status: Verified, Signature: Verified** and
@@ -84,6 +191,28 @@ The description arrives pre-filled with the copy from the PREVIOUS version.
84
191
  After a redesign it describes a face that no longer exists, and nothing prompts
85
192
  you about it. Re-paste from `docs/store/listing.md` every time.
86
193
 
194
+ ### Step 2 can fail silently — verify the live page
195
+
196
+ Observed on a real submission: the package uploaded, the version went live, and
197
+ *Submit* on step 2 **saved nothing**. No error, no validation message; the page
198
+ simply stayed on `/update` instead of redirecting. The listing kept the previous
199
+ description for as long as nobody looked.
200
+
201
+ Treat uploading a version and updating its listing as two independent
202
+ operations, whatever the form implies:
203
+
204
+ 1. After *Submit*, check that the URL actually changed to `/apps/<uuid>`.
205
+ Sitting on `/update` is the failure signature.
206
+ 2. Re-fetch the **public** page and string-match something distinctive from the
207
+ new copy. This is the only real confirmation.
208
+ 3. If it did not save, go back and use the separate **Edit Details** flow, which
209
+ does work.
210
+
211
+ A long description may also be truncated behind a read-more on the public page,
212
+ so a missing string near the *end* of the copy is not automatically a failed
213
+ save — check a distinctive phrase from early in the text too before concluding
214
+ either way.
215
+
87
216
  ## Image slots, in DOM order
88
217
 
89
218
  There are seven `input[type=file]` elements and none of them carry a name, id
@@ -115,7 +244,7 @@ Submit:
115
244
  same symbol space.
116
245
 
117
246
  Write settings paths in words: *"Garmin Connect app, then: your device, Connect
118
- IQ Apps, Watch Faces, the app name, settings"*. `scripts/ciq-release` checks the fenced
247
+ IQ Apps, Watch Faces, <app>, settings"*. `bin/ciq-release` checks the fenced
119
248
  blocks of `listing.md` for both classes before you open a browser.
120
249
 
121
250
  The error text renders as a leaf node with no `aria-invalid` anywhere, so find
@@ -26,11 +26,30 @@ Regularly. Symptoms: exit 124, no output, or a hang with no push.
26
26
  ```bash
27
27
  pkill -f monkeydo
28
28
  pkill -x simulator # NOT `pkill -f simulator` -- see below
29
- "$SDK/bin/connectiq" &
30
- sleep 12
29
+ open -a "$SDK/bin/ConnectIQ.app" # NOT "$SDK/bin/connectiq" -- see below
30
+ sleep 15
31
31
  "$SDK/bin/monkeydo" bin/app.prg fenix6pro
32
32
  ```
33
33
 
34
+ **Launch the `.app` bundle, not the `bin/connectiq` shell script.** Started via
35
+ the script from a non-interactive context, the simulator runs as a GUI process
36
+ that never creates a window:
37
+
38
+ ```
39
+ count of windows -> 0
40
+ ```
41
+
42
+ Every capture then fails with "simulator did not come forward", which reads
43
+ exactly like the frontmost-check being broken and sends you off calibrating
44
+ something that was never wrong. `open -a "$SDK/bin/ConnectIQ.app"` opens a real
45
+ window and everything downstream works. Confirm before pushing:
46
+
47
+ ```bash
48
+ osascript -e 'tell application "System Events" to tell process "simulator" \
49
+ to get {count of windows, name of window 1}'
50
+ # -> 1, CIQ Simulator - Forerunner® 970 (6.0.2)
51
+ ```
52
+
34
53
  **Use `pkill -x simulator`, not `pkill -f simulator`.** The `-f` form matches the
35
54
  full command line of every process, and unrelated applications carry the word
36
55
  "simulator" in their arguments -- Claude Desktop's helper processes register a
@@ -96,6 +115,7 @@ osascript -e 'tell application "System Events" to tell process "simulator" to ge
96
115
  | Settings | Set Battery Status | test battery thresholds |
97
116
  | Settings | Set User Profile | resting HR and similar |
98
117
  | Settings | Trigger App Settings | fire `onSettingsChanged` |
118
+ | Settings | Display Mode → Always-Active | **the AMOLED always-on frame** |
99
119
  | Simulation | Activity Data | non-zero steps/calories |
100
120
  | Simulation | Time Simulation | check other times of day |
101
121
 
@@ -103,6 +123,70 @@ The simulator reports zero steps and calories by default, because it has no
103
123
  activity data — not because your render path is broken. Use Simulation →
104
124
  Activity Data before concluding anything.
105
125
 
126
+ **Settings → Display Mode → Always-Active is how you see the always-on frame**,
127
+ and it is a submenu, not a checkbox. The two items are *High Power* and
128
+ *Always-Active* — NOT "Normal" and "Always-On", which is what the naming
129
+ everywhere else in the SDK leads you to type. Enumerate rather than guess:
130
+
131
+ ```bash
132
+ osascript -e 'tell application "System Events" to tell process "simulator" \
133
+ to get name of menu items of menu 1 of menu item "Display Mode" \
134
+ of menu 1 of menu bar item "Settings" of menu bar 1'
135
+ ```
136
+
137
+ Settings → *Sleep Mode* is a different thing
138
+ entirely — the user's sleep window, not the watch-face power state — and
139
+ clicking it leaves the face fully lit, which looks like `onEnterSleep` never
140
+ firing:
141
+
142
+ ```bash
143
+ osascript -e 'tell application "System Events" to tell process "simulator" \
144
+ to click menu item "Always-Active" of menu 1 of menu item "Display Mode" \
145
+ of menu 1 of menu bar item "Settings" of menu bar 1'
146
+ ```
147
+
148
+ Check both directions. Switching an AMOLED product to Always-On should give the
149
+ restricted frame; switching a MIP product to Always-On should change **nothing**,
150
+ because it carries no `requiresBurnInProtection` and has no reason to dim.
151
+
152
+ Measure rather than eyeball the result — the budget is a number:
153
+
154
+ ```python
155
+ # lit fraction and centroid inside the round glass, from a raw capture
156
+ lit = sum(1 for p in glass_pixels if max(p) > 40)
157
+ print(100 * lit / len(glass_pixels)) # keep well under 10%
158
+ ```
159
+
160
+ The centroid is the more useful of the two. It caught a blanked leading digit
161
+ cell leaving the time 106px right of centre — invisible in a passing test suite,
162
+ and easy to look straight past in a screenshot.
163
+
164
+ ### Stale monkeydo processes wedge the next run
165
+
166
+ `make test` hanging forever, or `Unable to connect to simulator`, is usually an
167
+ earlier `monkeydo` still holding the port rather than anything wrong with the
168
+ build. They accumulate silently across a session — several were found alive
169
+ here from runs an hour old.
170
+
171
+ ```bash
172
+ pgrep -lf "monkeydo|bin/shell" # look before blaming the test
173
+ pkill -f monkeydo; pkill -f "bin/shell"
174
+ ```
175
+
176
+ Kill the simulator too if it will not come forward, then relaunch with
177
+ `open -a "$SDK/bin/ConnectIQ.app"` and re-push.
178
+
179
+ ### A locked Mac silently defeats every capture
180
+
181
+ `screencapture` on a locked machine photographs the login screen, and the
182
+ AppleScript menu clicks report success while going nowhere. The result is a
183
+ plausible PNG containing a desktop wallpaper, and analysis of it produces
184
+ confident nonsense — a lit-pixel fraction of 0.0005 rather than an error.
185
+
186
+ Assert the capture contains a display before measuring it: the round glass is
187
+ a large dark disc, so a near-zero dark fraction over any 454-square window
188
+ means there is no watch in the frame.
189
+
106
190
  ## Sensor data
107
191
 
108
192
  `Toybox.SensorHistory` returns iterators whose most recent buckets are often
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env python3
2
+ """Pull design inspiration from open APIs and turn it into device-truthful
3
+ design tokens.
4
+
5
+ ciq-inspire --brand garmin.com # Brandfetch: colours, fonts
6
+ ciq-inspire --image ~/crest.png # dominant colours from a file
7
+ ciq-inspire --prior-art "chronograph" # GitHub: Monkey C faces to read
8
+ ciq-inspire --brand acme.com --json tokens.json
9
+
10
+ Every colour that comes back is quantized to the 4x4x4 lattice and ranked by
11
+ reflectance BEFORE you see it. A palette approved in its source form is a
12
+ palette that changes the moment it reaches the watch, and the change is never
13
+ small: an off-lattice mid-grey and an off-lattice pale grey routinely collapse
14
+ onto the same value.
15
+
16
+ Sources, and what each is actually good for
17
+ -------------------------------------------
18
+
19
+ Brandfetch https://brandfetch.com -- brand colours and typeface names for a
20
+ domain. Set BRANDFETCH_API_KEY. Free tier is enough.
21
+ GitHub prior art: real Monkey C sources you can read for how a thing was
22
+ drawn. Uses `gh` when installed (higher rate limit), else
23
+ unauthenticated HTTP.
24
+ Local image dominant colours from a logo, crest or reference photo. Needs
25
+ Pillow, which the screenshot tooling already requires.
26
+
27
+ There is NO public Connect IQ Store API. Store research is manual.
28
+
29
+ IP, and this is not optional
30
+ ----------------------------
31
+
32
+ Brandfetch will happily hand you another company's colours, wordmark and logo.
33
+ Colours and proportions are fine to be inspired by. Marks are not: the Connect
34
+ IQ store rejects, and Garmin removes, faces carrying a third party's name or
35
+ logo in the face, the icon, the screenshots or the copy. Use --brand for a
36
+ domain you own, or for palette study only. See references/store.md.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import argparse
42
+ import json
43
+ import os
44
+ import shutil
45
+ import subprocess
46
+ import sys
47
+ import urllib.error
48
+ import urllib.parse
49
+ import urllib.request
50
+ from pathlib import Path
51
+
52
+ LEVELS = (0x00, 0x55, 0xAA, 0xFF)
53
+ UA = "ciq-inspire (Connect IQ design tooling)"
54
+
55
+
56
+ def quantize(rgb):
57
+ return tuple(min(LEVELS, key=lambda level: abs(level - c)) for c in rgb)
58
+
59
+
60
+ def hexof(rgb):
61
+ return "#%02X%02X%02X" % rgb
62
+
63
+
64
+ def luminance(rgb):
65
+ r, g, b = rgb
66
+ return (r * 299 + g * 587 + b * 114) // 1000
67
+
68
+
69
+ def parse_hex(text):
70
+ t = (text or "").strip().lstrip("#")
71
+ if len(t) == 3:
72
+ t = "".join(c * 2 for c in t)
73
+ if len(t) != 6:
74
+ return None
75
+ try:
76
+ return tuple(int(t[i:i + 2], 16) for i in (0, 2, 4))
77
+ except ValueError:
78
+ return None
79
+
80
+
81
+ def get(url, headers=None):
82
+ req = urllib.request.Request(url, headers={"User-Agent": UA, **(headers or {})})
83
+ try:
84
+ with urllib.request.urlopen(req, timeout=20) as resp:
85
+ data = json.loads(resp.read().decode("utf-8"))
86
+ if not isinstance(data, dict):
87
+ print(f"API returned non-dict response", file=sys.stderr)
88
+ return None
89
+ return data
90
+ except json.JSONDecodeError as exc:
91
+ print(f"API returned invalid JSON: {exc}", file=sys.stderr)
92
+ return None
93
+
94
+
95
+ # ------------------------------------------------------------- brandfetch ----
96
+
97
+ def from_brandfetch(domain):
98
+ key = os.environ.get("BRANDFETCH_API_KEY")
99
+ if not key:
100
+ print("BRANDFETCH_API_KEY is not set. Get a free key at "
101
+ "https://developers.brandfetch.com and export it.", file=sys.stderr)
102
+ return None
103
+ url = f"https://api.brandfetch.io/v2/brands/{urllib.parse.quote(domain)}"
104
+ try:
105
+ data = get(url, {"Authorization": f"Bearer {key}"})
106
+ except urllib.error.HTTPError as exc:
107
+ print(f"Brandfetch {exc.code} for {domain}: {exc.reason}", file=sys.stderr)
108
+ return None
109
+ except urllib.error.URLError as exc:
110
+ print(f"Brandfetch unreachable: {exc.reason}", file=sys.stderr)
111
+ return None
112
+
113
+ if data is None:
114
+ return None
115
+
116
+ colours = []
117
+ for c in data.get("colors") or []:
118
+ rgb = parse_hex(c.get("hex"))
119
+ if rgb:
120
+ colours.append({"rgb": rgb, "role": c.get("type") or "unknown"})
121
+
122
+ fonts = [f.get("name") for f in (data.get("fonts") or []) if f.get("name")]
123
+ return {
124
+ "source": f"brandfetch:{domain}",
125
+ "name": data.get("name") or domain,
126
+ "colours": colours,
127
+ # Recorded for tone only. No system font on the watch is any of these,
128
+ # and a segment display has to be drawn rather than typeset.
129
+ "typefaces": fonts,
130
+ }
131
+
132
+
133
+ # ------------------------------------------------------------------ image ----
134
+
135
+ def from_image(path, count):
136
+ try:
137
+ from PIL import Image
138
+ except ImportError:
139
+ print("Pillow is required for --image: python3 -m pip install Pillow",
140
+ file=sys.stderr)
141
+ return None
142
+ if not Path(path).is_file():
143
+ print(f"No such file: {path}", file=sys.stderr)
144
+ return None
145
+
146
+ img = Image.open(path).convert("RGB")
147
+ img.thumbnail((160, 160))
148
+ # Quantize FIRST, then count. Counting true colours and quantizing after
149
+ # returns near-duplicates that collapse into one value on the panel and
150
+ # makes a two-colour logo look like a six-colour palette.
151
+ # Raw bytes rather than getdata(): getdata() is deprecated in Pillow 14 and
152
+ # tobytes() has meant the same thing in every version.
153
+ raw = img.tobytes()
154
+ tally = {}
155
+ for i in range(0, len(raw) - 2, 3):
156
+ q = quantize(raw[i:i + 3])
157
+ tally[q] = tally.get(q, 0) + 1
158
+ total = sum(tally.values())
159
+ if not tally or total == 0:
160
+ return {
161
+ "source": f"image:{Path(path).name}",
162
+ "name": Path(path).stem,
163
+ "colours": [],
164
+ "typefaces": [],
165
+ }
166
+ ranked = sorted(tally.items(), key=lambda kv: -kv[1])[:count]
167
+ return {
168
+ "source": f"image:{Path(path).name}",
169
+ "name": Path(path).stem,
170
+ "colours": [{"rgb": rgb, "role": f"{(n * 100) // total}% of pixels"}
171
+ for rgb, n in ranked],
172
+ "typefaces": [],
173
+ }
174
+
175
+
176
+ # ---------------------------------------------------------------- github ----
177
+
178
+ def gh_search(query, limit):
179
+ """Repository search. `gh` first: unauthenticated search is rate-limited to
180
+ 10/minute per IP and fails opaquely once you cross it."""
181
+ q = urllib.parse.quote(query)
182
+ path = f"search/repositories?q={q}&sort=stars&per_page={limit}"
183
+ if shutil.which("gh"):
184
+ try:
185
+ out = subprocess.run(["gh", "api", path], capture_output=True,
186
+ text=True, timeout=25)
187
+ if out.returncode == 0:
188
+ return json.loads(out.stdout)
189
+ except (subprocess.SubprocessError, json.JSONDecodeError):
190
+ pass
191
+ try:
192
+ return get(f"https://api.github.com/{path}")
193
+ except (urllib.error.HTTPError, urllib.error.URLError) as exc:
194
+ print(f"GitHub search failed: {exc}", file=sys.stderr)
195
+ return None
196
+
197
+
198
+ def from_prior_art(theme, limit):
199
+ # GitHub's linguist knows Monkey C, and `language:"Monkey C"` is by far the
200
+ # highest-signal filter available -- ~2600 repositories, essentially all of
201
+ # them Connect IQ. Free-text terms are ANDed against name/description/readme
202
+ # on top of it, so anything specific ("chronograph") routinely returns zero.
203
+ # Fall back to the whole corpus rather than reporting nothing.
204
+ data = gh_search(f'{theme} language:"Monkey C"', limit)
205
+ if data and not data.get("items"):
206
+ print(f'No Monkey C repository mentions "{theme}"; showing the most-starred '
207
+ "Connect IQ projects instead.", file=sys.stderr)
208
+ data = gh_search('language:"Monkey C"', limit)
209
+ if not data:
210
+ return []
211
+ out = []
212
+ for repo in data.get("items", []):
213
+ out.append({
214
+ "repo": repo.get("full_name"),
215
+ "stars": repo.get("stargazers_count"),
216
+ "url": repo.get("html_url"),
217
+ "license": ((repo.get("license") or {}).get("spdx_id")) or "none",
218
+ "description": (repo.get("description") or "")[:110],
219
+ })
220
+ return out
221
+
222
+
223
+ # ----------------------------------------------------------------- report ----
224
+
225
+ def roles(colours):
226
+ """Suggest palette roles by reflectance, not by hue.
227
+
228
+ On a transflective panel brightness IS reflectance, so glance order is a
229
+ luminance ladder. Anything that assigns roles by hue produces a face where
230
+ the fastest-moving element is the dimmest thing on the dial."""
231
+ uniq = sorted({quantize(c["rgb"]) for c in colours}, key=luminance)
232
+ if not uniq:
233
+ return {}
234
+ darkest, brightest = uniq[0], uniq[-1]
235
+ mid = uniq[len(uniq) // 2]
236
+ # An accent must be bright enough to read AND distinguishable from the ink.
237
+ accent = max((c for c in uniq if c not in (darkest, brightest)),
238
+ key=luminance, default=brightest)
239
+ return {
240
+ "DIAL": darkest,
241
+ "RULE": mid if mid not in (darkest, brightest) else darkest,
242
+ "INDEX": brightest,
243
+ "HAND": brightest,
244
+ "ACCENT": accent,
245
+ }
246
+
247
+
248
+ def report(bundles, art):
249
+ for b in bundles:
250
+ print(f"\n{b['name']} ({b['source']})")
251
+ print("-" * 62)
252
+ if not b["colours"]:
253
+ print(" no colours returned")
254
+ for c in sorted(b["colours"], key=lambda c: -luminance(quantize(c["rgb"]))):
255
+ src, q = tuple(c["rgb"]), quantize(c["rgb"])
256
+ shift = "" if src == q else f" <- {hexof(src)} shifted"
257
+ print(f" {hexof(q)} lum {luminance(q):>3} {c['role']}{shift}")
258
+ if b["typefaces"]:
259
+ print(f" typefaces (tone only, none exist on device): "
260
+ f"{', '.join(b['typefaces'])}")
261
+
262
+ merged = [c for b in bundles for c in b["colours"]]
263
+ if merged:
264
+ print("\nSuggested roles, ordered by reflectance")
265
+ print("-" * 62)
266
+ for role, rgb in roles(merged).items():
267
+ print(f" {role:<8} {hexof(rgb)} lum {luminance(rgb)}")
268
+ print("\n Sanity-check before adopting: DIAL to INDEX should be the "
269
+ "biggest\n luminance gap you have, and ACCENT must not be the "
270
+ "dimmest colour on\n the face -- see references/display.md.")
271
+
272
+ if art:
273
+ print("\nPrior art on GitHub -- read the source, do not copy it")
274
+ print("-" * 62)
275
+ for r in art:
276
+ print(f" {r['stars']:>5}* {r['repo']} [{r['license']}]")
277
+ if r["description"]:
278
+ print(f" {r['description']}")
279
+ print("\n Licences vary and most of these carry none, which means all "
280
+ "rights\n reserved. Read for technique; write your own code.")
281
+
282
+
283
+ def main():
284
+ ap = argparse.ArgumentParser(
285
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
286
+ ap.add_argument("--brand", metavar="DOMAIN", help="Brandfetch lookup")
287
+ ap.add_argument("--image", metavar="PATH", help="dominant colours from a file")
288
+ ap.add_argument("--colours", type=int, default=6, help="how many from --image")
289
+ ap.add_argument("--prior-art", metavar="THEME", help="search GitHub for faces")
290
+ ap.add_argument("--limit", type=int, default=8, help="prior-art results")
291
+ ap.add_argument("--json", metavar="PATH", help="also write tokens as JSON")
292
+ args = ap.parse_args()
293
+
294
+ if not (args.brand or args.image or args.prior_art):
295
+ ap.error("nothing to do: pass --brand, --image and/or --prior-art")
296
+
297
+ bundles = []
298
+ if args.brand:
299
+ b = from_brandfetch(args.brand)
300
+ if b:
301
+ bundles.append(b)
302
+ if args.image:
303
+ b = from_image(args.image, args.colours)
304
+ if b:
305
+ bundles.append(b)
306
+ art = from_prior_art(args.prior_art, args.limit) if args.prior_art else []
307
+
308
+ if not bundles and not art:
309
+ print("No sources returned anything usable.", file=sys.stderr)
310
+ return 1
311
+
312
+ report(bundles, art)
313
+
314
+ if args.json:
315
+ merged = [c for b in bundles for c in b["colours"]]
316
+ Path(args.json).write_text(json.dumps({
317
+ "sources": [b["source"] for b in bundles],
318
+ "palette": [hexof(quantize(c["rgb"])) for c in merged],
319
+ "roles": {k: hexof(v) for k, v in roles(merged).items()},
320
+ "priorArt": art,
321
+ }, indent=2) + "\n", encoding="utf-8")
322
+ print(f"\nWrote {args.json}")
323
+
324
+ print("\nNext: draw exactly three variants as SVG and run ciq-mock on them.")
325
+ return 0
326
+
327
+
328
+ if __name__ == "__main__":
329
+ sys.exit(main())