@olegkoval/agent-skills 1.19.0 → 1.20.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 (102) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.cursor-plugin/index.json +5 -0
  3. package/.grok-plugin/index.json +5 -0
  4. package/.kiro/steering/garmin-watchface.md +201 -0
  5. package/.kiro/steering/qodoloop.md +13 -1
  6. package/.windsurf/rules/garmin-watchface.md +200 -0
  7. package/.windsurf/rules/qodoloop.md +13 -1
  8. package/README.md +7 -1
  9. package/catalog/skills.json +28 -4
  10. package/collections/wearables.json +7 -0
  11. package/package.json +1 -1
  12. package/packages/marketing/viral-launch/adapters/claude/skills/viral-launch/references/external-skills.md +12 -0
  13. package/packages/marketing/viral-launch/adapters/claude/skills/viral-launch/references/launch-checklist.md +42 -0
  14. package/packages/marketing/viral-launch/adapters/cursor/skills/viral-launch/references/external-skills.md +12 -0
  15. package/packages/marketing/viral-launch/adapters/cursor/skills/viral-launch/references/launch-checklist.md +42 -0
  16. package/packages/marketing/viral-launch/adapters/grok/skills/viral-launch/references/external-skills.md +12 -0
  17. package/packages/marketing/viral-launch/adapters/grok/skills/viral-launch/references/launch-checklist.md +42 -0
  18. package/packages/software-development/qodoloop/SKILL.md +13 -1
  19. package/packages/software-development/qodoloop/adapters/claude/skills/qodoloop/SKILL.md +13 -1
  20. package/packages/software-development/qodoloop/adapters/claude/skills/qodoloop/references/graphql-queries.md +151 -0
  21. package/packages/software-development/qodoloop/adapters/cursor/skills/qodoloop/SKILL.md +13 -1
  22. package/packages/software-development/qodoloop/adapters/cursor/skills/qodoloop/references/graphql-queries.md +151 -0
  23. package/packages/software-development/qodoloop/adapters/grok/skills/qodoloop/SKILL.md +13 -1
  24. package/packages/software-development/qodoloop/adapters/grok/skills/qodoloop/references/graphql-queries.md +151 -0
  25. package/packages/software-development/qodoloop/adapters/kiro/steering/qodoloop.md +13 -1
  26. package/packages/software-development/qodoloop/adapters/windsurf/rules/qodoloop.md +13 -1
  27. package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/github-actions.md +16 -0
  28. package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/node-semantic-release.md +13 -0
  29. package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/preset.md +47 -0
  30. package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/validation.md +17 -0
  31. package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/github-actions.md +16 -0
  32. package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/node-semantic-release.md +13 -0
  33. package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/preset.md +47 -0
  34. package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/validation.md +17 -0
  35. package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/github-actions.md +16 -0
  36. package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/node-semantic-release.md +13 -0
  37. package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/preset.md +47 -0
  38. package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/validation.md +17 -0
  39. package/packages/wearables/garmin-watchface/SKILL.md +204 -0
  40. package/packages/wearables/garmin-watchface/adapters/claude/plugin.json +5 -0
  41. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/SKILL.md +205 -0
  42. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/devices.md +98 -0
  43. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/display.md +120 -0
  44. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/layout.md +224 -0
  45. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/simulator.md +146 -0
  46. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/store.md +151 -0
  47. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/testing.md +154 -0
  48. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-calibrate +184 -0
  49. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-capture +215 -0
  50. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-devices +177 -0
  51. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-doctor +85 -0
  52. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/templates/Makefile +72 -0
  53. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/templates/monkey.jungle +13 -0
  54. package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/templates/monkey.test.jungle +7 -0
  55. package/packages/wearables/garmin-watchface/adapters/codex/README.md +3 -0
  56. package/packages/wearables/garmin-watchface/adapters/cursor/plugin.json +6 -0
  57. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/SKILL.md +205 -0
  58. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/devices.md +98 -0
  59. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/display.md +120 -0
  60. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/layout.md +224 -0
  61. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/simulator.md +146 -0
  62. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/store.md +151 -0
  63. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/testing.md +154 -0
  64. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-calibrate +184 -0
  65. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-capture +215 -0
  66. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-devices +177 -0
  67. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-doctor +85 -0
  68. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/templates/Makefile +72 -0
  69. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/templates/monkey.jungle +13 -0
  70. package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/templates/monkey.test.jungle +7 -0
  71. package/packages/wearables/garmin-watchface/adapters/grok/plugin.json +6 -0
  72. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/SKILL.md +205 -0
  73. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/devices.md +98 -0
  74. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/display.md +120 -0
  75. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/layout.md +224 -0
  76. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/simulator.md +146 -0
  77. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/store.md +151 -0
  78. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/testing.md +154 -0
  79. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-calibrate +184 -0
  80. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-capture +215 -0
  81. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-devices +177 -0
  82. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-doctor +85 -0
  83. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/templates/Makefile +72 -0
  84. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/templates/monkey.jungle +13 -0
  85. package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/templates/monkey.test.jungle +7 -0
  86. package/packages/wearables/garmin-watchface/adapters/kiro/steering/garmin-watchface.md +201 -0
  87. package/packages/wearables/garmin-watchface/adapters/windsurf/rules/garmin-watchface.md +200 -0
  88. package/packages/wearables/garmin-watchface/references/devices.md +98 -0
  89. package/packages/wearables/garmin-watchface/references/display.md +120 -0
  90. package/packages/wearables/garmin-watchface/references/layout.md +224 -0
  91. package/packages/wearables/garmin-watchface/references/simulator.md +146 -0
  92. package/packages/wearables/garmin-watchface/references/store.md +151 -0
  93. package/packages/wearables/garmin-watchface/references/testing.md +154 -0
  94. package/packages/wearables/garmin-watchface/scripts/ciq-calibrate +184 -0
  95. package/packages/wearables/garmin-watchface/scripts/ciq-capture +215 -0
  96. package/packages/wearables/garmin-watchface/scripts/ciq-devices +177 -0
  97. package/packages/wearables/garmin-watchface/scripts/ciq-doctor +85 -0
  98. package/packages/wearables/garmin-watchface/templates/Makefile +72 -0
  99. package/packages/wearables/garmin-watchface/templates/monkey.jungle +13 -0
  100. package/packages/wearables/garmin-watchface/templates/monkey.test.jungle +7 -0
  101. package/scripts/build-adapters.sh +1 -1
  102. package/scripts/validate-catalog.sh +2 -1
@@ -0,0 +1,146 @@
1
+ # The simulator
2
+
3
+ ## It persists app settings and ignores your defaults
4
+
5
+ Changing a default in `properties.xml` does **not** change what the simulator
6
+ shows. It keeps the value it persisted the first time the app ran, so a property
7
+ reads `false` while the XML plainly says `true`. This looks exactly like broken
8
+ settings code and will send you debugging the wrong thing.
9
+
10
+ Clear it: **File → Reset All App Data**. That also drops the loaded device, so
11
+ relaunch the simulator and re-push afterwards.
12
+
13
+ Diagnose before assuming a bug — print what the app actually sees:
14
+
15
+ ```monkeyc
16
+ System.println("SETTINGS pro=" + _proMode + " ghost=" + _ghost);
17
+ ```
18
+
19
+ If one property matches its XML default and another does not, you are looking at
20
+ stale persistence, not a bug.
21
+
22
+ ## monkeydo wedges
23
+
24
+ Regularly. Symptoms: exit 124, no output, or a hang with no push.
25
+
26
+ ```bash
27
+ pkill -f monkeydo
28
+ pkill -x simulator # NOT `pkill -f simulator` -- see below
29
+ "$SDK/bin/connectiq" &
30
+ sleep 12
31
+ "$SDK/bin/monkeydo" bin/app.prg fenix6pro
32
+ ```
33
+
34
+ **Use `pkill -x simulator`, not `pkill -f simulator`.** The `-f` form matches the
35
+ full command line of every process, and unrelated applications carry the word
36
+ "simulator" in their arguments -- Claude Desktop's helper processes register a
37
+ `claude-simulator` URL scheme, for instance. `pkill -f simulator` kills them too.
38
+ `-x` matches the process name exactly.
39
+
40
+ The simulator needs about 10-12 seconds before it will accept a push, and a
41
+ further beat before its window is scriptable. Pushing too early gives a window
42
+ titled "CIQ Simulator" with no device name and no display.
43
+
44
+ For a watch face `monkeydo` does not exit on its own — it stays attached. That is
45
+ normal; Ctrl-C once the face renders.
46
+
47
+ ## Screenshots
48
+
49
+ Use `bin/ciq-capture`. If you must do it by hand, know the three failure modes:
50
+
51
+ **1. `screencapture -R` grabs a screen region, not a window.** If the simulator is
52
+ not in front you get a photograph of your terminal, saved successfully, with no
53
+ error. Always raise the window and verify it came forward:
54
+
55
+ ```bash
56
+ osascript -e 'tell application "System Events" to tell process "simulator" to set frontmost to true'
57
+ sleep 2
58
+ osascript -e 'tell application "System Events" to get name of first process whose frontmost is true'
59
+ ```
60
+
61
+ **2. The window moves between restarts.** Read its bounds every time:
62
+
63
+ ```bash
64
+ osascript -e 'tell application "System Events" to tell process "simulator" to get {position, size} of (first window whose name contains "CIQ Simulator")'
65
+ ```
66
+
67
+ **3. The display is a sub-rectangle of the window.** Do not guess it. Render a
68
+ full-screen marker colour, capture, and take its bounding box — that is the
69
+ display rect exactly. `bin/ciq-calibrate` automates this. On a 2x Retina Mac with
70
+ the fenix 6 Pro skin the answer is 518x518 at (155,345), a clean 2:1 for the
71
+ 260x260 display.
72
+
73
+ Sanity check the result before believing it. A watch face is neither a blank
74
+ white page nor solid black; if the dark fraction is outside roughly 25-90%, you
75
+ captured the wrong window.
76
+
77
+ **Mask round displays.** The square capture picks up a few pixels of the
78
+ simulator's bezel skin in its corners, which do not exist on the device. Mask at
79
+ 4x and downsample or the edge stair-steps.
80
+
81
+ The simulator's own **File → Save Screen Capture** works too, but its save dialog
82
+ is a Java panel that does not respond to `Cmd+Shift+G` or normal AppleScript UI
83
+ scripting. Fine by hand, not automatable.
84
+
85
+ ## Menus worth knowing (AppleScript-drivable)
86
+
87
+ ```bash
88
+ osascript -e 'tell application "System Events" to tell process "simulator" to get name of every menu item of menu 1 of menu bar item "File" of menu bar 1'
89
+ ```
90
+
91
+ | Menu | Item | Use |
92
+ | --- | --- | --- |
93
+ | File | Save Screen Capture | manual screenshot |
94
+ | File | Reset All App Data | clear persisted settings |
95
+ | File | Kill App | stop without closing |
96
+ | Settings | Set Battery Status | test battery thresholds |
97
+ | Settings | Set User Profile | resting HR and similar |
98
+ | Settings | Trigger App Settings | fire `onSettingsChanged` |
99
+ | Simulation | Activity Data | non-zero steps/calories |
100
+ | Simulation | Time Simulation | check other times of day |
101
+
102
+ The simulator reports zero steps and calories by default, because it has no
103
+ activity data — not because your render path is broken. Use Simulation →
104
+ Activity Data before concluding anything.
105
+
106
+ ## Sensor data
107
+
108
+ `Toybox.SensorHistory` returns iterators whose most recent buckets are often
109
+ `null`. Take the newest **non-null** sample rather than the newest sample:
110
+
111
+ ```monkeyc
112
+ const SCAN_DEPTH = 24;
113
+
114
+ function bodyBattery() as Number? {
115
+ if (!(Toybox has :SensorHistory)
116
+ || !(Toybox.SensorHistory has :getBodyBatteryHistory)) {
117
+ return null;
118
+ }
119
+ var it = Toybox.SensorHistory.getBodyBatteryHistory(
120
+ {:period => SCAN_DEPTH, :order => SensorHistory.ORDER_NEWEST_FIRST});
121
+ if (it == null) { return null; }
122
+ var sample = it.next();
123
+ var seen = 0;
124
+ while (sample != null && seen < SCAN_DEPTH) {
125
+ if (sample.data != null) { return sample.data.toNumber(); }
126
+ sample = it.next();
127
+ seen += 1;
128
+ }
129
+ return null;
130
+ }
131
+ ```
132
+
133
+ Always guard with `Toybox has :SensorHistory` — not every device has it, and the
134
+ face must degrade to `--` rather than crash.
135
+
136
+ ## Verifying on real hardware
137
+
138
+ The simulator renders on a backlit LCD and flatters everything. Colour, contrast
139
+ and legibility decisions are not verified until seen on the device in daylight.
140
+ Deploy over USB:
141
+
142
+ ```bash
143
+ make build
144
+ cp bin/app.prg /Volumes/GARMIN/GARMIN/APPS/
145
+ # eject, then pick the face on-device
146
+ ```
@@ -0,0 +1,151 @@
1
+ # Publishing
2
+
3
+ **Portal:** <https://apps.garmin.com/developer/dashboard>
4
+ **Guidelines:** <https://developer.garmin.com/connect-iq/app-review-guidelines/>
5
+
6
+ Upload is a single file: the `.iq` from `make package`. It is a multi-device
7
+ bundle — every declared product is inside it. Nothing else to attach.
8
+
9
+ ## App settings live on the phone, not the watch
10
+
11
+ Connect IQ watch face settings are **not reachable from the watch**. The
12
+ on-device face menu offers only Apply. Users configure via:
13
+
14
+ Garmin Connect app → device → Connect IQ Apps → Watch Faces → *your face* → gear.
15
+
16
+ Say this in the listing or it reads as a missing feature.
17
+
18
+ Wiring, three files plus a handler:
19
+
20
+ ```xml
21
+ <!-- resources/settings/properties.xml -->
22
+ <properties>
23
+ <property id="proMode" type="boolean">false</property>
24
+ </properties>
25
+
26
+ <!-- resources/settings/settings.xml -->
27
+ <settings>
28
+ <setting propertyKey="@Properties.proMode" title="@Strings.SettingProMode">
29
+ <settingConfig type="boolean" />
30
+ </setting>
31
+ </settings>
32
+ ```
33
+
34
+ ```monkeyc
35
+ // getValue THROWS when a key is absent -- which happens on a fresh install
36
+ // before the Connect app has ever written settings. Default every read.
37
+ private function boolSetting(key as String, fallback as Boolean) as Boolean {
38
+ try {
39
+ var value = Application.Properties.getValue(key);
40
+ if (value instanceof Lang.Boolean) { return value as Boolean; }
41
+ } catch (e) {
42
+ return fallback;
43
+ }
44
+ return fallback;
45
+ }
46
+ ```
47
+
48
+ ```monkeyc
49
+ // In the AppBase subclass -- without this the face keeps its old settings
50
+ // until it is reselected.
51
+ function onSettingsChanged() as Void {
52
+ WatchUi.requestUpdate();
53
+ }
54
+ ```
55
+
56
+ Re-read settings in both `onLayout` and `onShow`.
57
+
58
+ See `reference/simulator.md` for why the simulator will lie to you about this.
59
+
60
+ ## Intellectual property
61
+
62
+ Section 3(a) is stricter than people expect and explicitly covers your **listing
63
+ text and images**, not just code:
64
+
65
+ > You may not infringe any copyright, trademark, patent, trade secret, or any
66
+ > other form of intellectual property. You must own or have a license to use all
67
+ > IP included in or used by your app. This includes your app's software and
68
+ > content, your developer account name, as well as the logos, images, and content
69
+ > that you use to promote your app in the Connect IQ store.
70
+
71
+ > Be careful using brand names and logos ... Be careful before making any mention
72
+ > of a brand or using any logo or name that is not yours.
73
+
74
+ And Garmin will not adjudicate:
75
+
76
+ > It is not for us to decide whether your use of a third party's IP may be
77
+ > infringing, properly used under a license, or fair use.
78
+
79
+ So there is no approval to win. They accept it, and §5(b) lets them suspend with
80
+ **no notice** if a rights holder complains — against the account, which carries
81
+ your other apps too.
82
+
83
+ Practical tiers for an homage:
84
+
85
+ - **Highest risk:** a film title or real brand in the app name, launcher icon, or
86
+ screenshots. That is trademark use for product identity.
87
+ - **Lower risk:** one factual, referential sentence in the description body.
88
+ - **No risk, and usually better copy:** describe the *object* and the *era*. The
89
+ people who will recognise it recognise the shape, not the name.
90
+
91
+ "The brand is dead so I can use it" does not hold. Abandonment is a legal finding
92
+ requiring genuine non-use with no intent to resume, and it is moot if anyone holds
93
+ a live registration — renewal is cheap and dormant heritage marks are routinely
94
+ warehoused and revived. Check the registers before relying on it: USPTO Trademark
95
+ Search, EUIPO eSearch, WIPO Global Brand Database, and the national register of
96
+ the owner's country. A lapsed mark you do not own is still not a mark you *own*,
97
+ which is what §3(a) asks for.
98
+
99
+ Not legal advice. When it matters, ask a lawyer, not the store.
100
+
101
+ ## Donation links
102
+
103
+ Permitted, with care. §1(a) prohibits listings that are advertising-focused or
104
+ "serve primarily to drive affiliate traffic to a website", and §4(d) requires
105
+ stating whether payment is needed. A single support line **after** a complete
106
+ description, prefixed with *free*, clears both. Leading with it, or repeating it
107
+ in the short description, does not.
108
+
109
+ ## Screenshots
110
+
111
+ Regenerate from the build you are uploading, not an older one. Stale screenshots
112
+ are the most common thing to forget after a redesign, and they misrepresent the
113
+ version under review.
114
+
115
+ ```bash
116
+ make build && make sim
117
+ bin/ciq-capture docs/store/screenshot.png --face --size 260 --mask
118
+ ```
119
+
120
+ Generate the launcher icon and any hero image from the same geometry the face
121
+ uses, in a checked-in script, so they cannot drift from what the device renders.
122
+ Two things that bite when reusing face-drawing code for artwork:
123
+
124
+ - **Polarity must invert with the surface.** Face colours drawn on a hero's dark
125
+ background paint dark-on-dark and vanish. Take colours as parameters.
126
+ - **Detail that sells at full size destroys legibility at icon size.** Ghost
127
+ segments turn every digit into an `8` at 40px. Drop them there.
128
+
129
+ ## Checklist
130
+
131
+ - [ ] `make test` passes — and the tests actually compile (`reference/testing.md`)
132
+ - [ ] `make lint` clean at level 3
133
+ - [ ] `make package` builds every declared device
134
+ - [ ] Developer key not committed
135
+ - [ ] Screenshots regenerated from this build
136
+ - [ ] Launcher icon is yours, at the size each device wants
137
+ - [ ] Listing states that settings are in the Garmin Connect app
138
+ - [ ] No third-party marks in name, icon, screenshots, or copy
139
+ - [ ] Verified on real hardware in daylight, not just the simulator
140
+
141
+ ## Key hygiene
142
+
143
+ The developer key signs your app id. Lose it and you cannot update the listing;
144
+ commit it and anyone can impersonate your app.
145
+
146
+ ```bash
147
+ make key # generates ~/.Garmin/ConnectIQ/developer_key.der
148
+ ```
149
+
150
+ Back it up somewhere private. Keep `bin/` and the key out of git, and run a
151
+ pre-commit check that greps for `.der`/`.pem` in the index.
@@ -0,0 +1,154 @@
1
+ # Testing
2
+
3
+ ## The trap: a green run with zero tests compiled
4
+
5
+ `monkey.jungle` almost always contains:
6
+
7
+ ```
8
+ base.excludeAnnotations = test
9
+ ```
10
+
11
+ That is correct — test code must not ship in the store build. But it strips
12
+ `(:test)` from **every** target, including `--unit-test`. The result:
13
+
14
+ ```
15
+ BUILD SUCCESSFUL
16
+ Ran 0 tests
17
+ ```
18
+
19
+ or, worse, a green run reported against stale test files that reference deleted
20
+ symbols. Tests written months ago may never have executed once.
21
+
22
+ **Fix** — a second jungle, passed as an additional `-f`:
23
+
24
+ ```
25
+ # monkey.test.jungle
26
+ # Loaded only for `make test`, after monkey.jungle, so (:test) code actually
27
+ # compiles. The grammar requires a value here, so this is a placeholder that
28
+ # matches no annotation.
29
+ base.excludeAnnotations = none
30
+ ```
31
+
32
+ ```makefile
33
+ TEST_JUNGLE := monkey.jungle;monkey.test.jungle
34
+
35
+ test:
36
+ $(MONKEYC) -f "$(TEST_JUNGLE)" -d $(DEVICE) -o $(APP) -y $(KEY) -w -l 2 --unit-test
37
+ $(MONKEYDO) $(APP) $(DEVICE) -t
38
+ ```
39
+
40
+ Note `base.excludeAnnotations =` with an empty value is a **syntax error**. Use a
41
+ placeholder like `none`.
42
+
43
+ **Prove your tests compile.** Do not take a green run on trust:
44
+
45
+ ```monkeyc
46
+ (:test)
47
+ function someExistingTest(logger as Logger) as Boolean {
48
+ return ThisDoesNotExist.definitelyNotAFunction(); // must FAIL the build
49
+ }
50
+ ```
51
+
52
+ If the build still succeeds, nothing is being compiled. Run this check whenever
53
+ you inherit a project or change the jungle.
54
+
55
+ ## What is worth testing
56
+
57
+ Monkey C unit tests run on-device or in the simulator and can only assert on pure
58
+ logic — you cannot inspect rendered pixels. So test the two things that are pure:
59
+ **geometry** and **formatting**. Keep `Dc` out of `Layout` and `Fields` and they
60
+ stay testable.
61
+
62
+ ### Geometry invariants
63
+
64
+ ```monkeyc
65
+ (:test)
66
+ function goalBarClearsBothTextColumns(logger as Logger) as Boolean {
67
+ var barEnd = Layout.BAR_X + Layout.BAR_W;
68
+ return (Layout.BAR_X - Layout.LABEL_X) >= 40
69
+ && (Layout.VALUE_X - barEnd) >= 40;
70
+ }
71
+ ```
72
+
73
+ ### Sweeps, not single values
74
+
75
+ The single most valuable pattern here. A point test passes with the font height
76
+ you assumed; the sweep catches the device you have not tried:
77
+
78
+ ```monkeyc
79
+ (:test)
80
+ function footerStaysOnScreenForAnyPlausibleFont(logger as Logger) as Boolean {
81
+ for (var labelH = 14; labelH <= 40; labelH += 1) {
82
+ for (var timeH = 40; timeH <= 120; timeH += 4) {
83
+ if (footerBottom(labelH, timeH / 2, timeH) >= Layout.SCREEN) {
84
+ return false;
85
+ }
86
+ }
87
+ }
88
+ return true;
89
+ }
90
+ ```
91
+
92
+ ### Lookup tables where a typo is plausible
93
+
94
+ A mistyped segment mask produces a *plausible* wrong digit — a `6` missing its
95
+ top bar still reads as a `6` to a careless eye. Pin by count plus the specific
96
+ features that distinguish neighbours:
97
+
98
+ ```monkeyc
99
+ (:test)
100
+ function everyDigitLightsTheRightNumberOfSegments(logger as Logger) as Boolean {
101
+ var expected = [6, 2, 5, 5, 4, 5, 6, 3, 7, 6];
102
+ for (var d = 0; d <= 9; d += 1) {
103
+ var count = 0;
104
+ for (var bit = 0; bit < 7; bit += 1) {
105
+ if ((Seg7.maskFor(d) & (1 << bit)) != 0) { count += 1; }
106
+ }
107
+ if (count != expected[d]) { return false; }
108
+ }
109
+ return true;
110
+ }
111
+
112
+ (:test)
113
+ function zeroHasNoMiddleBarButEightDoes(logger as Logger) as Boolean {
114
+ return !Seg7.isLit(Seg7.maskFor(0), Seg7.SEG_G)
115
+ && Seg7.isLit(Seg7.maskFor(8), Seg7.SEG_G);
116
+ }
117
+ ```
118
+
119
+ ### Thresholds, with the reasoning in the test name
120
+
121
+ ```monkeyc
122
+ // A 14-day watch at 50% has a week left. Treating that as a warning was the bug.
123
+ (:test)
124
+ function halfChargeIsHealthyNotCaution(logger as Logger) as Boolean {
125
+ return Fields.batteryColor(50.0) == Palette.LABEL;
126
+ }
127
+ ```
128
+
129
+ ### Dead code
130
+
131
+ If a function is tested but never called, the tests pass and the feature does not
132
+ exist. Grep for call sites of anything you have just written a test for.
133
+
134
+ ## What tests cannot catch
135
+
136
+ Everything visual. Overlapping rows, a bar through a label, a face that is too
137
+ dim, content drawn off-screen — all of it passes a green suite. **Capture a
138
+ screenshot and look at it before committing.** Every visual bug in this skill's
139
+ history was found by looking.
140
+
141
+ ```bash
142
+ make build && make sim
143
+ bin/ciq-capture /tmp/face.png --face --size 260
144
+ # then Read the PNG
145
+ ```
146
+
147
+ ## Running
148
+
149
+ ```bash
150
+ make test # compile with the test jungle and run
151
+ ```
152
+
153
+ `monkeydo ... -t` prints per-test PASS/FAIL and a summary. It also wedges often;
154
+ see `reference/simulator.md`.
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """Find where the device display sits inside the simulator window.
3
+
4
+ Guessing this crops in bezel skin or clips the face. Instead: build a throwaway
5
+ app that floods the whole screen with one unmistakable colour, push it, capture,
6
+ and take the bounding box of that colour. That box IS the display rect, to the
7
+ pixel, for whatever device skin is loaded.
8
+
9
+ ciq-calibrate # uses the fenix6pro device by default
10
+ ciq-calibrate --device fr955
11
+
12
+ Writes ~/.cache/ciq-capture/calibration.json, which ciq-capture then uses.
13
+ Re-run when switching to a device whose skin is a different size.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ import tempfile
24
+ import time
25
+ from pathlib import Path
26
+
27
+ CACHE = Path.home() / ".cache" / "ciq-capture"
28
+ CIQ = Path.home() / "Library/Application Support/Garmin/ConnectIQ"
29
+ HERE = Path(__file__).resolve().parent
30
+
31
+ MARKER_RGB = (0xFF, 0x00, 0xFF)
32
+
33
+ APP = """import Toybox.Application;
34
+ import Toybox.Graphics;
35
+ import Toybox.Lang;
36
+ import Toybox.WatchUi;
37
+
38
+ class CalApp extends Application.AppBase {
39
+ function initialize() { AppBase.initialize(); }
40
+ function getInitialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] {
41
+ return [new CalView()];
42
+ }
43
+ }
44
+
45
+ class CalView extends WatchUi.WatchFace {
46
+ function initialize() { WatchFace.initialize(); }
47
+ function onUpdate(dc as Dc) as Void {
48
+ dc.setColor(0xFF00FF, 0xFF00FF);
49
+ dc.clear();
50
+ }
51
+ }
52
+ """
53
+
54
+ MANIFEST = """<?xml version="1.0" encoding="UTF-8"?>
55
+ <iq:manifest xmlns:iq="http://www.garmin.com/xml/connectiq" version="3">
56
+ <iq:application entry="CalApp" id="{appid}" launcherIcon="@Drawables.LauncherIcon"
57
+ minApiLevel="3.1.0" name="@Strings.AppName" type="watchface">
58
+ <iq:products><iq:product id="{device}"/></iq:products>
59
+ <iq:permissions/>
60
+ <iq:languages><iq:language>eng</iq:language></iq:languages>
61
+ <iq:barrels/>
62
+ </iq:application>
63
+ </iq:manifest>
64
+ """
65
+
66
+ JUNGLE = "project.manifest = manifest.xml\nbase.sourcePath = source\nbase.resourcePath = resources\n"
67
+ STRINGS = '<?xml version="1.0" encoding="UTF-8"?>\n<strings><string id="AppName">cal</string></strings>\n'
68
+ DRAWABLES = ('<?xml version="1.0" encoding="UTF-8"?>\n<drawables>'
69
+ '<bitmap id="LauncherIcon" filename="icon.png"/></drawables>\n')
70
+
71
+
72
+ def sdk_bin():
73
+ cfg = CIQ / "current-sdk.cfg"
74
+ if not cfg.is_file():
75
+ sys.exit("No SDK selected. Open SdkManager.app and install/select an SDK.")
76
+ return Path(cfg.read_text().strip()) / "bin"
77
+
78
+
79
+ def osa(script):
80
+ r = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
81
+ return r.stdout.strip() if r.returncode == 0 else ""
82
+
83
+
84
+ def main():
85
+ ap = argparse.ArgumentParser(description=__doc__,
86
+ formatter_class=argparse.RawDescriptionHelpFormatter)
87
+ ap.add_argument("--device", default="fenix6pro")
88
+ ap.add_argument("--key", default=str(Path.home() / ".Garmin/ConnectIQ/developer_key.der"))
89
+ args = ap.parse_args()
90
+
91
+ try:
92
+ from PIL import Image
93
+ except ImportError:
94
+ sys.exit("Needs Pillow: pip install pillow")
95
+
96
+ key = Path(args.key)
97
+ if not key.is_file():
98
+ sys.exit(f"No developer key at {key}. Generate one with `make key`.")
99
+
100
+ sdk = sdk_bin()
101
+ import uuid
102
+
103
+ with tempfile.TemporaryDirectory() as tmp:
104
+ root = Path(tmp)
105
+ (root / "source").mkdir()
106
+ (root / "resources/drawables").mkdir(parents=True)
107
+ (root / "resources/strings").mkdir(parents=True)
108
+
109
+ (root / "source/CalApp.mc").write_text(APP)
110
+ (root / "manifest.xml").write_text(
111
+ MANIFEST.format(appid=uuid.uuid4().hex, device=args.device))
112
+ (root / "monkey.jungle").write_text(JUNGLE)
113
+ (root / "resources/strings/strings.xml").write_text(STRINGS)
114
+ (root / "resources/drawables/drawables.xml").write_text(DRAWABLES)
115
+ Image.new("RGB", (40, 40), (0, 0, 0)).save(root / "resources/drawables/icon.png")
116
+
117
+ prg = root / "cal.prg"
118
+ print(f"building marker app for {args.device} ...")
119
+ r = subprocess.run(
120
+ [str(sdk / "monkeyc"), "-f", str(root / "monkey.jungle"),
121
+ "-d", args.device, "-o", str(prg), "-y", str(key), "-w"],
122
+ capture_output=True, text=True, cwd=root)
123
+ if "BUILD SUCCESSFUL" not in (r.stdout + r.stderr):
124
+ sys.exit("marker build failed:\n" + r.stdout + r.stderr)
125
+
126
+ subprocess.run(["pkill", "-f", "monkeydo"], capture_output=True)
127
+ subprocess.Popen([str(sdk / "connectiq")],
128
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
129
+ time.sleep(10)
130
+ subprocess.Popen([str(sdk / "monkeydo"), str(prg), args.device],
131
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
132
+ print("pushing to simulator ...")
133
+ time.sleep(20)
134
+
135
+ osa('tell application "System Events" to tell process "simulator" '
136
+ "to set frontmost to true")
137
+ time.sleep(2)
138
+
139
+ out = osa('tell application "System Events" to tell process "simulator" to get '
140
+ '{position, size} of (first window whose name contains "CIQ Simulator")')
141
+ parts = [p.strip() for p in out.split(",") if p.strip()]
142
+ if len(parts) != 4:
143
+ sys.exit("could not read the simulator window bounds")
144
+ x, y, w, h = (int(p) for p in parts)
145
+
146
+ shot = root / "cal.png"
147
+ subprocess.run(["screencapture", "-x", f"-R{x},{y},{w},{h}", str(shot)], check=True)
148
+
149
+ im = Image.open(shot).convert("RGB")
150
+ px = im.load()
151
+ xs, ys = [], []
152
+ for yy in range(im.height):
153
+ for xx in range(im.width):
154
+ r_, g_, b_ = px[xx, yy]
155
+ if r_ > 200 and b_ > 200 and g_ < 60:
156
+ xs.append(xx)
157
+ ys.append(yy)
158
+
159
+ if not xs:
160
+ sys.exit(
161
+ "marker not found in the capture.\n"
162
+ "The simulator was probably not in front, or the app did not load."
163
+ )
164
+
165
+ left, top = min(xs), min(ys)
166
+ width, height = max(xs) - left + 1, max(ys) - top + 1
167
+ if abs(width - height) > 2:
168
+ print(f"warning: marker is {width}x{height}, not square -- "
169
+ "non-square display or a partial capture", file=sys.stderr)
170
+
171
+ scale = round(width / max(im.width and 1, 1)) or 1
172
+ rect = {"left": left, "top": top, "size": max(width, height),
173
+ "scale": 2 if width > 300 else 1, "device": args.device}
174
+
175
+ CACHE.mkdir(parents=True, exist_ok=True)
176
+ CALIB = CACHE / "calibration.json"
177
+ CALIB.write_text(json.dumps(rect, indent=2) + "\n")
178
+ print(f"\ndisplay rect: {width}x{height} at ({left},{top}) in a "
179
+ f"{im.width}x{im.height} capture")
180
+ print(f"written to {CALIB}")
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()