@roku-sdk/ai 0.0.0-stage → 2.19.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/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +47 -3
- package/skills/ads-and-raf/SKILL.md +231 -0
- package/skills/ads-and-raf/references/integration-flows.md +332 -0
- package/skills/app-packaging/SKILL.md +107 -0
- package/skills/app-performance/SKILL.md +133 -0
- package/skills/cli-dev-loop/SKILL.md +217 -0
- package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
- package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
- package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
- package/skills/data-entry-keyboards/SKILL.md +160 -0
- package/skills/device-management/SKILL.md +158 -0
- package/skills/dialogs-and-modals/SKILL.md +210 -0
- package/skills/effects/SKILL.md +183 -0
- package/skills/fetch-and-data-loading/SKILL.md +164 -0
- package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
- package/skills/flex-grid/SKILL.md +236 -0
- package/skills/flex-grid/references/focus-integration.md +355 -0
- package/skills/flex-grid/references/plugins.md +201 -0
- package/skills/focus-management/SKILL.md +210 -0
- package/skills/focus-management/references/focus-recipes.md +290 -0
- package/skills/fonts-and-glyphs/SKILL.md +231 -0
- package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
- package/skills/logging-and-errors/SKILL.md +227 -0
- package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
- package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
- package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
- package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
- package/skills/mcp-setup/SKILL.md +131 -0
- package/skills/project-setup/SKILL.md +210 -0
- package/skills/project-setup/references/roku-config.md +120 -0
- package/skills/screen-navigation/SKILL.md +166 -0
- package/skills/sdk-components/SKILL.md +213 -0
- package/skills/sdk-components/references/dialogs.md +148 -0
- package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
- package/skills/sdk-installation/SKILL.md +129 -0
- package/skills/slideshow/SKILL.md +157 -0
- package/skills/slideshow/references/focus-and-handoff.md +187 -0
- package/skills/solid-on-roku/SKILL.md +181 -0
- package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
- package/skills/testing/SKILL.md +147 -0
- package/skills/testing/references/async-and-timing.md +97 -0
- package/skills/user-accounts-and-purchases/SKILL.md +164 -0
- package/skills-manifest.json +316 -0
- package/skills-manifest.schema.json +62 -0
- package/src/__tests__/manifest.test.ts +267 -0
- package/src/__tests__/snippets.test.ts +221 -0
- package/src/__tests__/trigger-routing.test.ts +77 -0
- package/src/index.ts +36 -0
- package/src/manifest.ts +215 -0
- package/src/router.ts +147 -0
- package/src/snippets.ts +212 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"skills": [
|
|
4
|
+
{
|
|
5
|
+
"name": "focus-management",
|
|
6
|
+
"description": "Use when D-pad focus in an RSG SDK TypeScript app has to be wired or is not behaving \u2014 reaching for FocusBoundary or useFocusable, deciding where an OK press on the focused item is handled, showing focus state on a row or list and dimming the items that are not focused, handing focus between regions built out of FocusBoundary, holding focus inside a dialog, or focus that lands on the wrong node. Not for a region a FlexGrid owns: that grid claims the movement keys itself, so its edges and its focus belong to the flex-grid skill.",
|
|
7
|
+
"triggers": [
|
|
8
|
+
"focus",
|
|
9
|
+
"FocusBoundary",
|
|
10
|
+
"useFocusable",
|
|
11
|
+
"D-pad navigation"
|
|
12
|
+
],
|
|
13
|
+
"path": "skills/focus-management/SKILL.md",
|
|
14
|
+
"references": [
|
|
15
|
+
"skills/focus-management/references/focus-recipes.md"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "flex-grid",
|
|
20
|
+
"description": "Use when the request names a FlexGrid, or asks for the virtualized shelf and poster-grid renderer @roku-sdk/flex-grid provides \u2014 creating one with defineFlexGrid or createFlexGrid, the config it requires, filling its rows in as they load, a FlexGrid that renders but receives no key press or whose initialFocus never resolves, a grid nested inside another grid's cell, moving focus from a grid edge to a nav bar or sibling region, and its focus-highlight, zoom and row-title plugins. Everything a grid owns is here, including its focus and its edges. Not for a list, menu or row built by hand out of FocusBoundary and useFocusable \u2014 including dimming the items of one that are not focused.",
|
|
21
|
+
"triggers": [
|
|
22
|
+
"FlexGrid",
|
|
23
|
+
"createFlexGrid",
|
|
24
|
+
"defineFlexGrid",
|
|
25
|
+
"shelf"
|
|
26
|
+
],
|
|
27
|
+
"path": "skills/flex-grid/SKILL.md",
|
|
28
|
+
"references": [
|
|
29
|
+
"skills/flex-grid/references/focus-integration.md",
|
|
30
|
+
"skills/flex-grid/references/plugins.md"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "testing",
|
|
35
|
+
"description": "Use when a test for an RSG SDK TypeScript component has to be written, read or fixed \u2014 what app authors usually call a unit test, though children render for real \u2014 rendering a component with render and TestAnchorNode, finding what it built by tag or by prop value, driving a key press through it, snapshotting the rendered tree, or a test whose assertion reads a value from before the update it is asserting on. Not for proving anything on real hardware, and not for a web component test: this renders into an in-process node tree, so its queries are over SceneGraph tags and props rather than a DOM.",
|
|
36
|
+
"triggers": [
|
|
37
|
+
"testing-library",
|
|
38
|
+
"TestAnchorNode",
|
|
39
|
+
"getByProp",
|
|
40
|
+
"fireEvent"
|
|
41
|
+
],
|
|
42
|
+
"path": "skills/testing/SKILL.md",
|
|
43
|
+
"references": [
|
|
44
|
+
"skills/testing/references/async-and-timing.md"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "sdk-components",
|
|
49
|
+
"description": "Use when the question is which @roku-sdk/components component draws a thing on screen in an RSG SDK TypeScript app, or how one is composed \u2014 a block of text, an image, a row of buttons, a busy indicator, a confirmation or alert dialog built from a StandardDialog and the named StdDlg area nodes that go inside it, layout with Group, LayoutGroup or MaskGroup, and which props a component needs before it renders anything at all. Not for a virtualized shelf or poster grid, not for focus, movement or the state that dims an unfocused item, and not for a Dynamic Component Library loaded at runtime, which the app-packaging skill routes.",
|
|
50
|
+
"triggers": [
|
|
51
|
+
"@roku-sdk/components",
|
|
52
|
+
"Poster",
|
|
53
|
+
"StandardDialog",
|
|
54
|
+
"LayoutGroup"
|
|
55
|
+
],
|
|
56
|
+
"path": "skills/sdk-components/SKILL.md",
|
|
57
|
+
"references": [
|
|
58
|
+
"skills/sdk-components/references/dialogs.md",
|
|
59
|
+
"skills/sdk-components/references/scenegraph-node-spelling.md"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "mcp-ai-dev-loop",
|
|
64
|
+
"description": "Use when an RSG SDK TypeScript app has to be built, deployed, driven or diagnosed on a real Roku through the SDK's MCP server \u2014 sideloading a build and knowing which directory it came from, a device still running older code than the source after a tool built and installed it, capturing what is on screen, reading the rendered node tree, sending remote keys, collecting device logs, turning a crash stack back into TypeScript source, asking from inside a working session whether the server those tools depend on is up, a tool call that answered from the wrong Roku because of the target it was given, or starting and stopping a billed Roku Cloud Emulator device with device_start and device_stop. Not for the Roku developer web interface, not for debugging a web or mobile app, and not for a shell script or CI job with no agent in it, which is the cli-dev-loop skill.",
|
|
65
|
+
"triggers": [
|
|
66
|
+
"MCP",
|
|
67
|
+
"capture_screen",
|
|
68
|
+
"get_server_health",
|
|
69
|
+
"decode_stack_trace"
|
|
70
|
+
],
|
|
71
|
+
"path": "skills/mcp-ai-dev-loop/SKILL.md",
|
|
72
|
+
"references": [
|
|
73
|
+
"skills/mcp-ai-dev-loop/references/crash-and-console.md",
|
|
74
|
+
"skills/mcp-ai-dev-loop/references/live-debugging.md"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "app-performance",
|
|
79
|
+
"description": "Use when an RSG SDK TypeScript app janks, drops frames, responds late, or its memory climbs the longer it runs on a real Roku, and the question is how to measure it \u2014 recording a Perfetto trace and the order the calls have to happen in, reading which thread and which interval actually blocks, attributing the inside of a slow window to the TypeScript that ran there, and reading how much JS heap the running app holds with cdp_heap_usage, including why the debug build that read needs makes its timings incomparable with a normal run. What retains that heap is cpu-and-heap-profiling. Not for the everyday deploy-and-inspect loop, not for a slow build, not for making a test reliable whether or not it runs on hardware, and not for a component's own API once a trace has named it.",
|
|
80
|
+
"triggers": [
|
|
81
|
+
"perfetto",
|
|
82
|
+
"perfetto_start",
|
|
83
|
+
"jank",
|
|
84
|
+
"cdp_heap_usage"
|
|
85
|
+
],
|
|
86
|
+
"path": "skills/app-performance/SKILL.md"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "cpu-and-heap-profiling",
|
|
90
|
+
"description": "Use when an RSG SDK TypeScript app's CPU time or heap has to be attributed \u2014 which JavaScript function is hot, or which objects and call sites hold the heap \u2014 recording a CPU profile, heap snapshot or allocation sample through the server's CDP session, making frames readable rather than minified, driving the app between start and stop, reading the .cpuprofile, .heapprofile or .heapsnapshot that lands on disk, and profiling while also evaluating expressions in the same session, which a debugger tool having run on that session makes impossible. Not the heap's size alone, which app-performance reads with cdp_heap_usage; not frame drops, jank or a render-thread question, which is app-performance; not a crash stack, which is mcp-ai-dev-loop; and not missing MCP tools, which is mcp-setup.",
|
|
91
|
+
"triggers": [
|
|
92
|
+
"cdp_profile_start",
|
|
93
|
+
"cdp_heap_snapshot",
|
|
94
|
+
"cpuprofile",
|
|
95
|
+
"heapsnapshot",
|
|
96
|
+
"cdp_heap_sampling_start",
|
|
97
|
+
"heapprofile"
|
|
98
|
+
],
|
|
99
|
+
"path": "skills/cpu-and-heap-profiling/SKILL.md",
|
|
100
|
+
"references": [
|
|
101
|
+
"skills/cpu-and-heap-profiling/references/reading-profiles.md"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "mcp-setup",
|
|
106
|
+
"description": "Use when the SDK's MCP tools are missing from an agent session, will not connect, or have to be configured for the first time \u2014 choosing between the editor spawning the server and running it persistently, the config entry that does either, starting a server and finding the port it chose, and reading the failure when no tools appear at all. Not for a session whose tools already answer: one call failing over targeting, a device or the app, asking from inside that session whether the backing server is up, and everything after that \u2014 deploying, capturing the screen, driving the app, reading logs \u2014 belongs to the mcp-ai-dev-loop skill.",
|
|
107
|
+
"triggers": [
|
|
108
|
+
"mcp.json",
|
|
109
|
+
"rk mcp",
|
|
110
|
+
"mcpServers",
|
|
111
|
+
"rk server"
|
|
112
|
+
],
|
|
113
|
+
"path": "skills/mcp-setup/SKILL.md"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "sdk-installation",
|
|
117
|
+
"description": "Use when @roku-sdk packages must be installed from npmjs.com or their versions have to move \u2014 public pnpm installs that need no token, an install that succeeds locally but fails in CI, a package publishing major lines under separate dist-tags, a peer dependency an SDK package requires the app to declare, and checking registry configuration and the lockfile to prove where packages resolve. Not for a package the app lists because its own code imports it, or how a scaffolded app is configured, which is the project-setup skill's subject.",
|
|
118
|
+
"triggers": [
|
|
119
|
+
"npmjs.com",
|
|
120
|
+
"registry",
|
|
121
|
+
"dist-tag",
|
|
122
|
+
"pnpm add"
|
|
123
|
+
],
|
|
124
|
+
"path": "skills/sdk-installation/SKILL.md"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "project-setup",
|
|
128
|
+
"description": "Use when an RSG SDK TypeScript app has to be created or its project-level configuration is wrong \u2014 scaffolding a new app, what roku-config.ts owns (title, version, splash, manifest entries, and dtsOptions such as includeDefaults when declaration generation does not recognise an XML base), the app's tsconfig.json, which @roku-sdk packages the app must list because its own code imports them, what the scaffold's .gitignore covers after rk create, or where a file such as an image has to live before it can be imported in code. Not for resolving SDK packages from npmjs.com, which version line installs, or a peer an SDK package requires the app to declare, which is the sdk-installation skill's subject.",
|
|
129
|
+
"triggers": [
|
|
130
|
+
"roku-config",
|
|
131
|
+
"scaffold",
|
|
132
|
+
"tsconfig",
|
|
133
|
+
"rk create"
|
|
134
|
+
],
|
|
135
|
+
"path": "skills/project-setup/SKILL.md",
|
|
136
|
+
"references": [
|
|
137
|
+
"skills/project-setup/references/roku-config.md"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "app-packaging",
|
|
142
|
+
"description": "Use when a built RSG SDK TypeScript app has to be signed, published, or cleared off a device \u2014 producing a signed .pkg and the password tiers a CI job with no terminal is left with, rekeying a box to a different signing key, getting a new version onto a channel for review or live (publishing to a channel is done in the Roku Developer Dashboard; the SDK has no command for it), listing or removing the dev channel and the Dynamic Component Libraries a device holds, and where setting a library up and referencing a node from it is documented once a partner ships that library separately and the app loads it from a URL at runtime. Not for installing @roku-sdk packages from npmjs.com, which is the sdk-installation skill's subject; not for producing the build that then gets signed, which is the cli-dev-loop skill; and not for what roku-config.ts declares about the app, which project-setup owns.",
|
|
143
|
+
"triggers": [
|
|
144
|
+
"rk package",
|
|
145
|
+
"rk dcl",
|
|
146
|
+
"squashfs"
|
|
147
|
+
],
|
|
148
|
+
"path": "skills/app-packaging/SKILL.md"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "device-management",
|
|
152
|
+
"description": "Use when the rk CLI's own list of Rokus is wrong, or a shell command has to be pointed at a different one \u2014 registering a device, storing its developer password and keeping the file that holds it out of a shared repository, choosing which registered device is active, overriding the target for a single command and why --device and --host cannot be combined, where roku-dev-config.json is looked up and which copy of it wins, rk device ls listing a device that no config file seems to mention, and registering, starting or stopping a Roku Cloud Emulator device named rce:<name>. Not for an agent session whose MCP tools already answer: a tool call that reached the wrong box carries its own target argument, and that is the mcp-ai-dev-loop skill. Not for what gets run against the device once it answers \u2014 building, sideloading, driving it or reading its logs is the cli-dev-loop skill \u2014 and not for leasing a device out of a shared pool, which needs internal tooling the partner CLI does not ship.",
|
|
153
|
+
"triggers": [
|
|
154
|
+
"rk device",
|
|
155
|
+
"roku-dev-config",
|
|
156
|
+
"set-active",
|
|
157
|
+
"--device"
|
|
158
|
+
],
|
|
159
|
+
"path": "skills/device-management/SKILL.md"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "cli-dev-loop",
|
|
163
|
+
"description": "Use when an RSG SDK TypeScript app has to be built, installed, driven and observed from a shell script or a CI job, with no agent session in it \u2014 rk build and rk dev writing to different directories while rk dev --no-build reads only one of them, rk start-dev and top-level rk sideload surviving as deprecated aliases, there being no rk deploy, a dev build with no Roku on hand, which commands refuse to run without rk server, a flag one build command accepts and the other rejects, flags rejected as the wrong half of --no-build or --no-sideload, sending remote keys or capturing the screen from a script, and collecting device logs or decoding a crash stack once the run is over. Not for an agent session whose MCP tools already answer \u2014 that is the mcp-ai-dev-loop skill, which reaches the same operations through tool calls; not for what the running app costs in frames, memory or startup time, which is app-performance; and not for registering a Roku or choosing which one commands reach, which is device-management.",
|
|
164
|
+
"triggers": [
|
|
165
|
+
"rk build",
|
|
166
|
+
"rk dev",
|
|
167
|
+
"rk start-dev",
|
|
168
|
+
"rk sideload",
|
|
169
|
+
"rk ecp"
|
|
170
|
+
],
|
|
171
|
+
"path": "skills/cli-dev-loop/SKILL.md",
|
|
172
|
+
"references": [
|
|
173
|
+
"skills/cli-dev-loop/references/logs-and-crashes.md"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "solid-on-roku",
|
|
178
|
+
"description": "Use when SolidJS code in an RSG SDK TypeScript app has to match what this platform ships rather than what the framework documents \u2014 an import of createDeferred, lazy or SuspenseList that @roku-sdk/solid deliberately does not re-export because the Roku runtime cannot support it, why reading the anchor node's inputs whole subscribes an effect to every field and what the untrack there is for, a let or const inside a loop behaving as if it were var, a generator function the build will not transform, a development build whose createSignal is not the one the release build runs, and whether Show needs a fallback. Not for handling something that threw, which is the logging-and-errors skill; not for a stale read in a test assertion, which is testing; and not for which component draws a thing or how focus and keys are wired, which are sdk-components and focus-management.",
|
|
179
|
+
"triggers": [
|
|
180
|
+
"@roku-sdk/solid",
|
|
181
|
+
"createSignal",
|
|
182
|
+
"createEffect",
|
|
183
|
+
"untrack"
|
|
184
|
+
],
|
|
185
|
+
"path": "skills/solid-on-roku/SKILL.md",
|
|
186
|
+
"references": [
|
|
187
|
+
"skills/solid-on-roku/references/hermes-and-exports.md"
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "logging-and-errors",
|
|
192
|
+
"description": "Use when an RSG SDK TypeScript app has to report what it is doing, or has to survive something throwing \u2014 the logger the SDK exports and the thunk its methods take, a level raised at runtime that recovers nothing because the build already deleted the call sites, log levels set through buildVariables that silently change nothing, console calls present in development and gone from a production build, a screen that comes up blank with no crash and no stack because a root component threw, containing that with ErrorBoundary and recovering with catchError or resetErrorBoundaries, registering an uncaught-exception handler and what it never sees, and calling enableExceptionTagging in main() so the running app marks its own traces.",
|
|
193
|
+
"triggers": [
|
|
194
|
+
"logger",
|
|
195
|
+
"baseLogLevel",
|
|
196
|
+
"enableExceptionTagging",
|
|
197
|
+
"setUncaughtExceptionHandler"
|
|
198
|
+
],
|
|
199
|
+
"path": "skills/logging-and-errors/SKILL.md",
|
|
200
|
+
"references": [
|
|
201
|
+
"skills/logging-and-errors/references/uncaught-exceptions.md"
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "fonts-and-glyphs",
|
|
206
|
+
"description": "Use when text in an RSG SDK TypeScript app needs a typeface or a symbol and the wrong thing renders \u2014 an arrow, chevron, star or check mark taken from the RokuFont01 constants instead of a Unicode character the firmware may not have, text coming out as empty boxes, symbols that disappeared once the app shipped a font of its own, whether fallbackGlyph does anything about that, importing a .ttf or .otf and the build refusing two files that share a name, a typeface that renders unreadably small because its size was left at the default, and a SimpleLabel whose fontSize is ignored because the built-in font name it was given is a fixed-size one.",
|
|
207
|
+
"triggers": [
|
|
208
|
+
"RokuFont01",
|
|
209
|
+
"glyphs",
|
|
210
|
+
"@roku-sdk/components/glyphs",
|
|
211
|
+
"fallbackGlyph"
|
|
212
|
+
],
|
|
213
|
+
"path": "skills/fonts-and-glyphs/SKILL.md",
|
|
214
|
+
"references": [
|
|
215
|
+
"skills/fonts-and-glyphs/references/glyph-tables.md"
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "dialogs-and-modals",
|
|
220
|
+
"description": "Use when a dialog in an RSG SDK TypeScript app has to be opened, closed and sequenced rather than assembled \u2014 an app-level provider that owns which dialog is showing, carrying a close reason back so the caller can branch on how it was dismissed, chaining a multi-step flow such as terms then entry then confirmation, a loading dialog that has to come down when the request it covers resolves, and a determinate progress dialog driven from app state. Not which component draws it, or which StdDlg area and item types go inside one, which is the sdk-components skill; and not holding D-pad focus inside a self-drawn overlay or deciding which keys close it, which is focus-management.",
|
|
221
|
+
"triggers": [
|
|
222
|
+
"DialogContext",
|
|
223
|
+
"StdDlgProgressItem",
|
|
224
|
+
"dialog lifecycle",
|
|
225
|
+
"StandardProgressDialog"
|
|
226
|
+
],
|
|
227
|
+
"path": "skills/dialogs-and-modals/SKILL.md"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "screen-navigation",
|
|
231
|
+
"description": "Use when an RSG SDK TypeScript app has to move between whole screens \u2014 mounting ScreenControllerProvider with the fallback it requires, pushing and popping a stack whose lower entries stay mounted, replacing the current screen instead of stacking it, releasing work when a screen is covered and restoring it when it is uncovered, passing data into a screen, and a pushed screen that shows the one underneath through it because the controller paints no background of its own. Not focus or D-pad movement once a screen is up, which is focus-management; and not the app's layout or which packages it declares, which is project-setup.",
|
|
232
|
+
"triggers": [
|
|
233
|
+
"pushScreen",
|
|
234
|
+
"popScreen",
|
|
235
|
+
"ScreenControllerProvider",
|
|
236
|
+
"onScreenCovered"
|
|
237
|
+
],
|
|
238
|
+
"path": "skills/screen-navigation/SKILL.md"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "user-accounts-and-purchases",
|
|
242
|
+
"description": "Use when an RSG SDK TypeScript app has to sign a viewer in, remember them across devices, or sell them something through ChannelStore \u2014 setting a data field, registering the result handler and only then setting command, reading the viewer's Roku account details, storing and retrieving channel credentials so a second device signs in without typing, fetching the product catalog, placing an order, listing what the viewer already owns, and requesting a device attestation token. Not signing a build, publishing it, or the beta channel, which is app-packaging; not SDK package installation, which is sdk-installation; and not a Roku's developer password, which is device-management.",
|
|
243
|
+
"triggers": [
|
|
244
|
+
"ChannelStore",
|
|
245
|
+
"getChannelCred",
|
|
246
|
+
"storeChannelCredData",
|
|
247
|
+
"getDeviceAttestationToken"
|
|
248
|
+
],
|
|
249
|
+
"path": "skills/user-accounts-and-purchases/SKILL.md"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "fetch-and-data-loading",
|
|
253
|
+
"description": "Use when an RSG SDK TypeScript app has to load data over the network \u2014 calling fetch with the single argument this runtime accepts rather than the two-argument form the web uses and which still compiles here, checking ok because a 4xx or 5xx resolves instead of rejecting, reading a typed body out of json, building a Request when a method or headers are needed, pointing HttpsAgent at a custom certificate file, and asking isFeatureSupported before relying on an optional capability. Not when the rows of a FlexGrid fill in, which that grid owns; and not scaffolding or which packages an app declares, which is project-setup.",
|
|
254
|
+
"triggers": [
|
|
255
|
+
"fetch",
|
|
256
|
+
"HttpsAgent",
|
|
257
|
+
"ResponseInstance",
|
|
258
|
+
"setGlobalAgent"
|
|
259
|
+
],
|
|
260
|
+
"path": "skills/fetch-and-data-loading/SKILL.md",
|
|
261
|
+
"references": [
|
|
262
|
+
"skills/fetch-and-data-loading/references/https-and-certificates.md"
|
|
263
|
+
]
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "data-entry-keyboards",
|
|
267
|
+
"description": "Use when a viewer has to type something into an RSG SDK TypeScript app \u2014 wiring CustomKeyboard, DynamicKeyboard, DynamicKeyGrid, DynamicMiniKeyboard or DynamicPinPad, reading what was entered as it changes, authoring a .keyboard.json definition with several modes for another language or a caps state, masking a PIN as it is typed, and a search field that filters while the viewer types. Not which pre-assembled dialog wraps one, or how a StandardDialog is composed from areas and items, which is sdk-components; and not remote key routing or where focus sits, which is focus-management.",
|
|
268
|
+
"triggers": [
|
|
269
|
+
"CustomKeyboard",
|
|
270
|
+
"DynamicPinPad",
|
|
271
|
+
"DynamicKeyGrid",
|
|
272
|
+
"keyboard.json"
|
|
273
|
+
],
|
|
274
|
+
"path": "skills/data-entry-keyboards/SKILL.md"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "slideshow",
|
|
278
|
+
"description": "Use when an RSG SDK TypeScript app shows one thing at a time and transitions between them \u2014 a hero banner, a full-screen image carousel, an onboarding or promo pager \u2014 built with @roku-sdk/slideshow: choosing among createSlideshow, createVirtualSlideshow and createPooledSlideshow, the JSX defineSlideshow wrapper that only wraps the eager one, slideFocusId and making a slide focusable, a pooled slideshow whose recycled templates can never take focus and how one entry in it is made reachable with the remote anyway, handing focus back to a sibling region at the first or last slide, and the DefaultChevrons and DefaultDotIndicator chrome. Not a row, shelf, poster grid or any screen that scrolls through many items at once, which is flex-grid; not which node draws the picture or the text inside a slide, which is sdk-components; and never video playback or the handoff from one video to the next, which this package has no part in.",
|
|
279
|
+
"triggers": [
|
|
280
|
+
"createSlideshow",
|
|
281
|
+
"createVirtualSlideshow",
|
|
282
|
+
"createPooledSlideshow",
|
|
283
|
+
"slideFocusId"
|
|
284
|
+
],
|
|
285
|
+
"path": "skills/slideshow/SKILL.md",
|
|
286
|
+
"references": [
|
|
287
|
+
"skills/slideshow/references/focus-and-handoff.md"
|
|
288
|
+
]
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "ads-and-raf",
|
|
292
|
+
"description": "Use when an RSG SDK TypeScript app has to play ads through the Roku Advertising Framework with @roku-sdk/ads \u2014 waiting for the Ads task to report ready before issuing anything, setting the data fields first and command last, and reading each result off its own callback; choosing between CSAI, SSAI and CSAS and the command sequence each needs; the roku_ads_lib manifest entry without which RAF never initialises; a command re-issued with the same value writing nothing because the field never changed; a result handler firing with a none sentinel before the real value; why almost every ad-pod field is optional and lowercase; and the audience-measurement fields that must precede getAds. Not which node plays the content video or anything else on screen, which is sdk-components; not making the network request yourself, which is fetch-and-data-loading, since RAF fetches the ads; and not ChannelStore's set-data-then-set-command sequence for sign-in and purchases, which is user-accounts-and-purchases.",
|
|
293
|
+
"triggers": [
|
|
294
|
+
"getAds",
|
|
295
|
+
"showAds",
|
|
296
|
+
"stitchedAdsInit",
|
|
297
|
+
"roku_ads_lib"
|
|
298
|
+
],
|
|
299
|
+
"path": "skills/ads-and-raf/SKILL.md",
|
|
300
|
+
"references": [
|
|
301
|
+
"skills/ads-and-raf/references/integration-flows.md"
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"name": "effects",
|
|
306
|
+
"description": "Use when a Rectangle or Poster in an RSG SDK TypeScript app needs rounded corners, a border or a gradient fill through the Effect node \u2014 where to create the Effect and when one is rebuilt instead of updated, one Effect shared by several consumers, animating its fields with an interpolator that otherwise does nothing, shaderCompatible on a Poster whose corners stay square for some images, a gradient that never draws, corners that do not appear on some devices and how to detect that at runtime, and a child that still paints over the rounded corners. Not clipping or fading a group of children through an image mask, which is MaskGroup in sdk-components; and not createEffect or any other reactive primitive, which is solid-on-roku.",
|
|
307
|
+
"triggers": [
|
|
308
|
+
"borderRadius",
|
|
309
|
+
"shaderCompatible",
|
|
310
|
+
"gradientStyle",
|
|
311
|
+
"IEffectProps"
|
|
312
|
+
],
|
|
313
|
+
"path": "skills/effects/SKILL.md"
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://roku.com/schemas/rsg-sdk/skills-manifest.schema.json",
|
|
4
|
+
"title": "RSG SDK Skills Manifest",
|
|
5
|
+
"description": "Standalone manifest mapping AI skill triggers to skill paths, shipped by @roku-sdk/ai. The zod schema in src/manifest.ts is the runtime authority; this file mirrors it for editors and CI. It is stricter about unknown keys on purpose: `additionalProperties: false` tells an author while editing that a key does nothing, whereas the loader ignores unknown keys so that a manifest from a newer @roku-sdk/* package still loads in an older one.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "skills"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": {
|
|
11
|
+
"description": "Schema/format version of the manifest itself. Stays 1 even though `description` became required: this is a strict const, so a bump would reject v1 documents rather than accept them, and manifests are only ever read from installed @roku-sdk/* packages that ship alongside the loader.",
|
|
12
|
+
"const": 1
|
|
13
|
+
},
|
|
14
|
+
"skills": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"minItems": 1,
|
|
17
|
+
"items": { "$ref": "#/definitions/skillEntry" }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"definitions": {
|
|
21
|
+
"skillEntry": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["name", "description", "triggers", "path"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"name": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "kebab-case; MUST equal the skill's directory name; unique within a manifest.",
|
|
29
|
+
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
30
|
+
},
|
|
31
|
+
"description": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Single-line activation prose, emitted verbatim as the generated skill's `description` frontmatter. Authored, not derived from triggers.",
|
|
34
|
+
"minLength": 1,
|
|
35
|
+
"maxLength": 1024,
|
|
36
|
+
"pattern": "^[^\\r\\n]+$"
|
|
37
|
+
},
|
|
38
|
+
"triggers": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"description": "Non-empty; each trigger should be unique across all discovered manifests.",
|
|
41
|
+
"minItems": 1,
|
|
42
|
+
"items": { "type": "string", "minLength": 1 }
|
|
43
|
+
},
|
|
44
|
+
"path": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "POSIX path relative to the package root; MUST resolve to an existing SKILL.md.",
|
|
47
|
+
"pattern": "^skills/[^/]+/SKILL\\.md$"
|
|
48
|
+
},
|
|
49
|
+
"references": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"description": "Documents delivered alongside the body and loaded on demand, for depth the body should not carry. Each MUST resolve on disk and sit under the same skills/<name>/ directory as `path` — the zod schema enforces that pairing, which this mirror cannot express.",
|
|
52
|
+
"minItems": 1,
|
|
53
|
+
"uniqueItems": true,
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^skills/[^/]+/references/[^/]+\\.md$"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "@jest/globals";
|
|
5
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
|
|
8
|
+
import { loadManifest, ManifestValidationError, type SkillManifest, zSkillEntry } from "../manifest";
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = resolve(__dirname, "..", "..");
|
|
11
|
+
|
|
12
|
+
const VALID_DESCRIPTION = "Use when wiring D-pad focus in an RSG SDK TypeScript app.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A schema-valid entry. Tests override only the field under test, so adding a
|
|
16
|
+
* required field does not have to be mirrored into every case.
|
|
17
|
+
*/
|
|
18
|
+
function skillEntry(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
19
|
+
return {
|
|
20
|
+
name: "focus-management",
|
|
21
|
+
description: VALID_DESCRIPTION,
|
|
22
|
+
triggers: ["focus"],
|
|
23
|
+
path: "skills/focus-management/SKILL.md",
|
|
24
|
+
...overrides
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readManifestJson(): unknown {
|
|
29
|
+
return JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "skills-manifest.json"), "utf8"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("manifest — loadManifest", () => {
|
|
33
|
+
it("parses a valid manifest into a typed object", () => {
|
|
34
|
+
const manifest = loadManifest({
|
|
35
|
+
version: 1,
|
|
36
|
+
skills: [skillEntry({ triggers: ["focus", "FocusBoundary"] })]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(manifest.version).toBe(1);
|
|
40
|
+
expect(manifest.skills).toHaveLength(1);
|
|
41
|
+
expect(manifest.skills[0]).toMatchObject({
|
|
42
|
+
name: "focus-management",
|
|
43
|
+
description: VALID_DESCRIPTION,
|
|
44
|
+
path: "skills/focus-management/SKILL.md"
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// `golden` was removed once it turned out to have no consumer. This zod
|
|
49
|
+
// schema is not strict, so a manifest written before the removal still
|
|
50
|
+
// loads. The shipped JSON Schema is stricter — `additionalProperties: false`
|
|
51
|
+
// rejects the same input — so the two disagree here, as they do for any
|
|
52
|
+
// unknown key. Loading is what runs, and it accepts the older shape.
|
|
53
|
+
it("ignores a field it no longer knows, such as the removed golden flag", () => {
|
|
54
|
+
const manifest = loadManifest({
|
|
55
|
+
version: 1,
|
|
56
|
+
skills: [skillEntry({ name: "project-setup", path: "skills/project-setup/SKILL.md", golden: true })]
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(manifest.skills[0]).not.toHaveProperty("golden");
|
|
60
|
+
expect(manifest.skills[0].name).toBe("project-setup");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("throws a descriptive ManifestValidationError for a bad skill name", () => {
|
|
64
|
+
const bad = { version: 1, skills: [skillEntry({ name: "Focus_Management" })] };
|
|
65
|
+
|
|
66
|
+
expect(() => loadManifest(bad)).toThrow(ManifestValidationError);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
loadManifest(bad);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
expect(error).toBeInstanceOf(ManifestValidationError);
|
|
72
|
+
const message = (error as ManifestValidationError).message;
|
|
73
|
+
expect(message).toContain("skills.0.name");
|
|
74
|
+
expect(message).toContain("kebab-case");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("rejects an empty triggers array", () => {
|
|
79
|
+
expect(() => loadManifest({ version: 1, skills: [skillEntry({ triggers: [] })] })).toThrow(
|
|
80
|
+
ManifestValidationError
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects a malformed path", () => {
|
|
85
|
+
expect(() => loadManifest({ version: 1, skills: [skillEntry({ path: "focus-management/SKILL.md" })] })).toThrow(
|
|
86
|
+
ManifestValidationError
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("rejects an unsupported top-level version", () => {
|
|
91
|
+
expect(() => loadManifest({ version: 2, skills: [skillEntry()] })).toThrow(ManifestValidationError);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects a manifest with no skills", () => {
|
|
95
|
+
expect(() => loadManifest({ version: 1, skills: [] })).toThrow(ManifestValidationError);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// The description is what an agent runtime matches on to load the skill at
|
|
99
|
+
// all, and the runtimes we generate for reject these three shapes outright —
|
|
100
|
+
// so they fail here, where the diagnostic names the offending entry.
|
|
101
|
+
it.each([
|
|
102
|
+
["absent", undefined],
|
|
103
|
+
["empty", ""],
|
|
104
|
+
["multi-line", "Use when wiring focus.\nSecond line."],
|
|
105
|
+
["over 1024 characters", "x".repeat(1025)]
|
|
106
|
+
])("rejects a description that is %s", (_label, description) => {
|
|
107
|
+
const entry = skillEntry({ description });
|
|
108
|
+
if (description === undefined) {
|
|
109
|
+
delete entry.description;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
expect(() => loadManifest({ version: 1, skills: [entry] })).toThrow(ManifestValidationError);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("accepts a description at the 1024-character ceiling", () => {
|
|
116
|
+
const description = "x".repeat(1024);
|
|
117
|
+
const manifest = loadManifest({ version: 1, skills: [skillEntry({ description })] });
|
|
118
|
+
|
|
119
|
+
expect(manifest.skills[0].description).toBe(description);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("manifest — references", () => {
|
|
124
|
+
it("loads an entry with no references", () => {
|
|
125
|
+
const manifest = loadManifest({ version: 1, skills: [skillEntry()] });
|
|
126
|
+
|
|
127
|
+
expect(manifest.skills[0].references).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("loads references in the order declared", () => {
|
|
131
|
+
const references = [
|
|
132
|
+
"skills/focus-management/references/focus-recipes.md",
|
|
133
|
+
"skills/focus-management/references/plugins.md"
|
|
134
|
+
];
|
|
135
|
+
const manifest = loadManifest({ version: 1, skills: [skillEntry({ references })] });
|
|
136
|
+
|
|
137
|
+
expect(manifest.skills[0].references).toEqual(references);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it.each([
|
|
141
|
+
["outside a references/ directory", "skills/focus-management/focus-recipes.md"],
|
|
142
|
+
["nested below references/", "skills/focus-management/references/deep/focus-recipes.md"],
|
|
143
|
+
["not a markdown file", "skills/focus-management/references/focus-recipes.txt"],
|
|
144
|
+
["absolute", "/skills/focus-management/references/focus-recipes.md"]
|
|
145
|
+
])("rejects a reference path that is %s", (_label, reference) => {
|
|
146
|
+
expect(() => loadManifest({ version: 1, skills: [skillEntry({ references: [reference] })] })).toThrow(
|
|
147
|
+
ManifestValidationError
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("rejects an empty references array, so the key always means something", () => {
|
|
152
|
+
expect(() => loadManifest({ version: 1, skills: [skillEntry({ references: [] })] })).toThrow(
|
|
153
|
+
ManifestValidationError
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// The pattern alone cannot catch this: the path is well-formed, it just
|
|
158
|
+
// belongs to another skill. The generator names its output from the owning
|
|
159
|
+
// entry, so the file would land in a directory whose own entry never
|
|
160
|
+
// declared it — and pruning works by directory.
|
|
161
|
+
it("rejects a reference under another skill's directory", () => {
|
|
162
|
+
const bad = {
|
|
163
|
+
version: 1,
|
|
164
|
+
skills: [skillEntry({ references: ["skills/project-setup/references/scaffold.md"] })]
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
expect(() => loadManifest(bad)).toThrow(ManifestValidationError);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
loadManifest(bad);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const message = (error as ManifestValidationError).message;
|
|
173
|
+
expect(message).toContain("skills.0.references.0");
|
|
174
|
+
expect(message).toContain("skills/focus-management/references/");
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("rejects a repeated reference, which would plan the same write twice", () => {
|
|
179
|
+
const reference = "skills/focus-management/references/focus-recipes.md";
|
|
180
|
+
const bad = { version: 1, skills: [skillEntry({ references: [reference, reference] })] };
|
|
181
|
+
|
|
182
|
+
expect(() => loadManifest(bad)).toThrow(ManifestValidationError);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
loadManifest(bad);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
expect((error as ManifestValidationError).message).toContain("duplicate reference");
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("manifest — shipped skills-manifest.json", () => {
|
|
193
|
+
it("validates against the schema", () => {
|
|
194
|
+
const manifest: SkillManifest = loadManifest(readManifestJson());
|
|
195
|
+
expect(manifest.skills.length).toBeGreaterThan(0);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// `skills-manifest.schema.json` is a hand-written mirror of `zSkillEntry`, read
|
|
199
|
+
// by editors and CI rather than by this loader, so nothing at runtime notices
|
|
200
|
+
// when a field lands on one side only. The two are deliberately not identical —
|
|
201
|
+
// the mirror is strict about unknown keys and cannot express the co-location
|
|
202
|
+
// rule — but a field the mirror has never heard of is drift, and the mirror
|
|
203
|
+
// rejects it under `additionalProperties: false` in the one place the author is
|
|
204
|
+
// still editing.
|
|
205
|
+
it("mirrors every zod entry field in the JSON Schema", () => {
|
|
206
|
+
const schema = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "skills-manifest.schema.json"), "utf8")) as {
|
|
207
|
+
definitions: { skillEntry: { properties: Record<string, unknown> } };
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
expect(Object.keys(schema.definitions.skillEntry.properties).sort()).toEqual(
|
|
211
|
+
Object.keys(zSkillEntry.shape).sort()
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("resolves every skill path on disk", () => {
|
|
216
|
+
const manifest = loadManifest(readManifestJson());
|
|
217
|
+
for (const skill of manifest.skills) {
|
|
218
|
+
expect(existsSync(resolve(PACKAGE_ROOT, skill.path))).toBe(true);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// The manifest is the only index of the library: discovery, the generated
|
|
223
|
+
// tree, the packaging check and the behavioral eval all enumerate skills
|
|
224
|
+
// from it, never from the directory. A skill directory with no entry is
|
|
225
|
+
// therefore invisible to every one of them while still being published by
|
|
226
|
+
// the `skills/**` files allow-list — so it ships to partners as prose no
|
|
227
|
+
// agent will ever load. Asserted as set equality rather than one-way
|
|
228
|
+
// containment, because the opposite direction (an entry whose directory is
|
|
229
|
+
// missing) is what the path test above covers.
|
|
230
|
+
it("resolves every declared reference on disk", () => {
|
|
231
|
+
const manifest = loadManifest(readManifestJson());
|
|
232
|
+
for (const skill of manifest.skills) {
|
|
233
|
+
for (const reference of skill.references ?? []) {
|
|
234
|
+
expect(existsSync(resolve(PACKAGE_ROOT, reference))).toBe(true);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// The mirror of the directory test below, one level down: `skills/**` publishes
|
|
240
|
+
// the whole tree, so a reference file nobody declared ships to partners while
|
|
241
|
+
// the generator never writes it and no body can point at it.
|
|
242
|
+
it("declares every file under a skill's references/ directory", () => {
|
|
243
|
+
const manifest = loadManifest(readManifestJson());
|
|
244
|
+
const declared = new Set(manifest.skills.flatMap(skill => skill.references ?? []));
|
|
245
|
+
|
|
246
|
+
for (const skill of manifest.skills) {
|
|
247
|
+
const dir = resolve(PACKAGE_ROOT, "skills", skill.name, "references");
|
|
248
|
+
if (!existsSync(dir)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
252
|
+
expect(declared).toContain(`skills/${skill.name}/references/${entry.name}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("has an entry for every skill directory, and no others", () => {
|
|
258
|
+
const manifest = loadManifest(readManifestJson());
|
|
259
|
+
const declared = manifest.skills.map(skill => skill.name).sort();
|
|
260
|
+
const onDisk = readdirSync(resolve(PACKAGE_ROOT, "skills"), { withFileTypes: true })
|
|
261
|
+
.filter(entry => entry.isDirectory())
|
|
262
|
+
.map(entry => entry.name)
|
|
263
|
+
.sort();
|
|
264
|
+
|
|
265
|
+
expect(onDisk).toEqual(declared);
|
|
266
|
+
});
|
|
267
|
+
});
|