openvibers/openvibe.examples Commits

examples/inventory-wear: show what people wear on OpenVibe next to names in your app (Inventory's public reads, no token) (#32)

OP OpenVibers committed 22 days ago · f26e3be91a · 9 files, +288 −5

Changes

ddiff --git a/README.md b/README.md
iindex 9c887c9..d0b261d 100644
---- a/README.md
++++ b/README.md
@@@ -3,7 +3,7 @@
> Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials
> and public APIs, nothing else.
--**Status:** alpha, 0.3.0 (roadmap Wave 20; the `create-openvibe-app` scaffolder, WS-N task 6). Thirteen examples, each with a smoke test that CI runs on
++**Status:** alpha, 0.3.0 (roadmap Wave 20; the `create-openvibe-app` scaffolder, WS-N task 6). Fourteen examples, each with a smoke test that CI runs on
Node 22.22.1 against `openvibe-sdk/testing`'s mock platform (no network). `npm run e2e` runs three
of them (Media, Events pull, a Tools job) against the real platform with your own sandbox app; it
is not part of CI or `npm test`, and it was not run against production as part of this release.
@@@ -36,6 +36,7 @@ Each example is small, runs on its own and proves one thing with a smoke test.
| [chat-bot](examples/chat-bot) | A bot on Chat's WebSocket protocol that stays in its one configured room and always identifies as a bot | Chat `/ws/chat` protocol (no SDK client: Chat is first-party) |
| [mod-manifest](examples/mod-manifest) | A `mods.mod-manifest@1` validated against the schema, the capability catalog and compatibility ranges | `openvibe-contracts`, `openvibe-sdk/registry` |
| [go-live](examples/go-live) | Go live on OpenVibe.Live from code: a Live API token finds or creates a WHIP slot and reads its WHIP URL, FFmpeg 8+ publishes (test pattern, a file or a camera), the session is named once media flows and ended when FFmpeg stops; the stream key is never printed | `openvibe-sdk/live` (`slots`, `streams`, `channels.url`), `/core`, FFmpeg's WHIP muxer |
++| [inventory-wear](examples/inventory-wear) | Show what people wear on OpenVibe (hats, chat tags, badges, name effects) next to names in your own app: the public catalog and who wears what, 100 people per call, with no token; text and escaped HTML | `openvibe-sdk/inventory` (`kinds`, `definitions`, `equipped`) |
| [space-deploy](examples/space-deploy) | Deploy a static site to OpenVibe.Space from CI with an app token (space.deploy.create, deployer role): the built folder as one deploy, live on main or a preview for a pull request, a refusal that changes nothing, rollback; a ready GitHub Actions workflow | `openvibe-sdk/space` (`deploys.uploadFiles`, `rollback`), `/auth` |
| [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` |
| [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` |
@@@ -293,7 +294,7 @@ test/public-surface.test.js, test/structure.test.js
## Owns
- the nine charter examples: vanilla browser app, Node server app, webhook consumer, event
-- subscriber, Media uploader, Chat bot, tool/job example, mod manifest example, OAuth app example; plus the AI chat example (the openai client on AI's OpenAI-compatible endpoint) the go-live example (Live over WHIP from code), the zone-s3 example (Zone with the AWS SDK v3) and the space-deploy example (Space deploys from CI)
++ subscriber, Media uploader, Chat bot, tool/job example, mod manifest example, OAuth app example; plus the AI chat example (the openai client on AI's OpenAI-compatible endpoint) the go-live example (Live over WHIP from code), the zone-s3 example (Zone with the AWS SDK v3), the space-deploy example (Space deploys from CI) and the inventory-wear example (what people wear, rendered in your app)
- the on-demand end-to-end run (`npm run e2e`) and the sandbox-app setup script
- the developer path check (`npm run developer-path`, and `developer-path:mock` in CI)
ddiff --git a/STATUS.json b/STATUS.json
iindex 3f79c60..9799007 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -9,7 +9,7 @@
"deployed": false,
"deployment": "none: a repository with CI, not a runtime and not a published package",
"features": [
-- "thirteen copyable examples: browser app, Node server app, Media uploader, event subscriber, webhook consumer, tool job, OAuth app, chat bot, mod manifest, AI chat (the official openai client on ai.openvibe.services/v1, plain, streamed and the project's own provider), go live (a Live API token sets up a WHIP slot; FFmpeg 8+ publishes; the session is named and ended), Zone over S3 (the official AWS SDK v3: put, get, paged list, presigned share link, delete), Space deploys from CI (an app token, live or preview, rollback, a GitHub Actions workflow)",
++ "fourteen copyable examples: browser app, Node server app, Media uploader, event subscriber, webhook consumer, tool job, OAuth app, chat bot, mod manifest, AI chat (the official openai client on ai.openvibe.services/v1, plain, streamed and the project's own provider), go live (a Live API token sets up a WHIP slot; FFmpeg 8+ publishes; the session is named and ended), Zone over S3 (the official AWS SDK v3: put, get, paged list, presigned share link, delete), Space deploys from CI (an app token, live or preview, rollback, a GitHub Actions workflow), what people wear (Inventory's public catalog and equipped reads, no token)",
"smoke tests against openvibe-sdk/testing's mock platform, offline",
"create-openvibe-app scaffolder",
"npm run e2e against the real platform (on demand)",
ddiff --git a/examples/inventory-wear/.env.example b/examples/inventory-wear/.env.example
nnew file mode 100644
iindex 0000000..07fd820
---- /dev/null
++++ b/examples/inventory-wear/.env.example
@@@ -0,0 +1,3 @@
++# Nothing is required: Inventory's catalog and who wears what are public reads, with no token.
++# Optional:
++# OV_INVENTORY_URL=https://inventory.openvibe.network
ddiff --git a/examples/inventory-wear/README.md b/examples/inventory-wear/README.md
nnew file mode 100644
iindex 0000000..789be98
---- /dev/null
++++ b/examples/inventory-wear/README.md
@@@ -0,0 +1,68 @@
++# What people wear, in your app
++
++On OpenVibe, a person's hat, chat tag, badge and name effect follow them from site to site through
++[OpenVibe.Inventory](https://inventory.openvibe.network), the network-wide inventory. This example
++shows them next to names in **your** app, as OpenVibe.Live's chat does. Both reads are public, so
++you need no token and no app.
++
++```bash
++node wear.js usr_01J… usr_01J…
++# 🤠 [VIP] alex ⭐ (Cowboy Hat · VIP tag · Divine Name · Early Supporter)
++# sam
++```
++
++## What it proves
++
++- **The catalog is public and changes rarely.** It's two reads:
++ - `kinds.list()`: each kind's slots and how surfaces render it;
++ - `definitions.list({ kind })`: names, art, attributes.
++
++ Read it once and keep it. Production had 6 kinds and 70 published items on 2026-10-22.
++- **Who wears what** is one public read for up to 100 people (`inventory.equipped(subjects)`), the
++ same call chat renders use. `equippedOf()` splits larger lists into calls of 100.
++- **A look:**
++ - the hat's emoji goes before the name;
++ - a chat tag goes in brackets;
++ - a badge's emoji goes after;
++ - a name effect is the CSS class OpenVibe's pages use (`name-fx-divine`, …).
++
++ An item whose definition is retired or unpublished shows nothing rather than a guess.
++- **`renderHtml()` escapes the name** and adds a class only when it is a plain token, so a person's
++ name can't inject markup.
++- **Reading is for everyone; issuing is not.** Defining and granting items is for each kind's
++ issuer (OpenVibe's own services today, `inventory.definition.manage` and `inventory.item.grant`
++ are first-party), and nothing is sold or traded.
++
++## Files
++
++| File | What |
++|---|---|
++| `wear.js` | `loadConfig()`, `createInventory()`, `loadCatalog()`, `equippedOf()`, `lookOf()`, `renderLine()`, `renderHtml()`, CLI |
++| `test/smoke.test.js` | the catalog, batched equipped reads and both renderings against a stand-in for Inventory |
++
++## Run the smoke test
++
++```bash
++npm test
++```
++
++The test hands the SDK a `fetch` that answers like Inventory's public routes (`server/http/api.js`
++on OpenVibe.Inventory's main, shapes sampled from production on 2026-10-22). It checks:
++- that no token is sent;
++- one definitions read per kind;
++- 152 people fetched in calls of 100 and 52;
++- the look, including a retired item that shows nothing;
++- the text line;
++- the escaped HTML, with an unsafe class refused.
++
++Nothing reaches the network.
++
++## Run it against the real platform
++
++```bash
++npm install
++node wear.js usr_01J… # anyone's subject id (usr_…), as your app learns it from sign-in
++```
++
++An app that signs people in with OpenVibe (see [oauth-app](../oauth-app)) learns each person's
++`usr_…` id from their token, and that id is all these reads need.
ddiff --git a/examples/inventory-wear/package.json b/examples/inventory-wear/package.json
nnew file mode 100644
iindex 0000000..2b0edf5
---- /dev/null
++++ b/examples/inventory-wear/package.json
@@@ -0,0 +1,18 @@
++{
++ "name": "openvibe-example-inventory-wear",
++ "version": "0.1.0",
++ "private": true,
++ "description": "Show what people wear on OpenVibe (hats, chat tags, badges, name effects) next to their names in your own app, from Inventory's public reads.",
++ "license": "MIT",
++ "main": "wear.js",
++ "scripts": {
++ "start": "node --env-file-if-exists=.env wear.js",
++ "test": "node test/smoke.test.js"
++ },
++ "engines": {
++ "node": ">=22"
++ },
++ "dependencies": {
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.60.0"
++ }
++}
ddiff --git a/examples/inventory-wear/test/smoke.test.js b/examples/inventory-wear/test/smoke.test.js
nnew file mode 100644
iindex 0000000..1998272
---- /dev/null
++++ b/examples/inventory-wear/test/smoke.test.js
@@@ -0,0 +1,80 @@
++'use strict';
++/**
++ * Smoke test: wear.js against a stand-in for OpenVibe.Inventory's public reads (the shapes server/http/api.js answers,
++ * sampled from production on 2026-10-22): no token sent, the catalog read once per kind, equipped sets fetched 100
++ * people per call, a look built from the slots (a retired definition shows nothing), and the line and HTML rendered
++ * with names escaped. No network.
++ */
++const assert = require('node:assert/strict');
++const { loadConfig, createInventory, loadCatalog, equippedOf, lookOf, renderLine, renderHtml } = require('../wear');
++
++globalThis.fetch = () => { throw new Error('network access in a smoke test'); };
++
++const A = 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0';
++const B = 'usr_01JABCDEFGHJKMNPQRSTVWXYZ1';
++const KINDS = [
++ { id: 'live.chat_tag', slots: ['chat_tag'] }, { id: 'live.hat', slots: ['hat'] },
++ { id: 'live.name_effect', slots: ['name_effect'] }, { id: 'network.badge', slots: ['badge'] },
++];
++const DEFS = {
++ 'live.hat': [{ id: 'itd_01M4GGQ9Y4DWMP43ERNQ3BQW1V', kind: 'live.hat', name: 'Cowboy Hat', art: { emoji: '🤠' }, attributes: { tier: 2 } }],
++ 'live.chat_tag': [{ id: 'itd_01M4GGQ9Z0000000000000000A', kind: 'live.chat_tag', name: 'VIP tag', art: {}, attributes: { text: 'VIP' } }],
++ 'live.name_effect': [{ id: 'itd_01M4GGQ9VT32FM9CFD8J0FVX3H', kind: 'live.name_effect', name: 'Divine Name', art: { emoji: '✝️', token: 'name-fx-divine' }, attributes: { css_class: 'name-fx-divine' } }],
++ 'network.badge': [{ id: 'itd_01M4GGQ9Z0000000000000000B', kind: 'network.badge', name: 'Early Supporter', art: { emoji: '⭐' }, attributes: {} }],
++};
++const calls = [];
++const json = (o) => new Response(JSON.stringify(o), { status: 200, headers: { 'content-type': 'application/json' } });
++async function fakeFetch(url, init = {}) {
++ const u = new URL(String(url));
++ calls.push({ path: u.pathname, query: Object.fromEntries(u.searchParams), auth: new Headers(init.headers).get('authorization') });
++ assert.equal(u.origin, 'https://inventory.openvibe.network');
++ if (u.pathname === '/api/v1/kinds') return json({ kinds: KINDS });
++ if (u.pathname === '/api/v1/definitions') return json({ definitions: DEFS[u.searchParams.get('kind')] || [] });
++ if (u.pathname === '/api/v1/equipped') {
++ const subjects = u.searchParams.get('subjects').split(',');
++ return json({ equipped: subjects.map((s) => ({
++ subject: s, updated_at: '2026-10-22T19:00:00.000Z',
++ slots: s === A ? {
++ 'live.hat:hat': { instance_id: 'inv_1', definition_id: 'itd_01M4GGQ9Y4DWMP43ERNQ3BQW1V' },
++ 'live.chat_tag:chat_tag': { instance_id: 'inv_2', definition_id: 'itd_01M4GGQ9Z0000000000000000A' },
++ 'live.name_effect:name_effect': { instance_id: 'inv_3', definition_id: 'itd_01M4GGQ9VT32FM9CFD8J0FVX3H', token: 'name-fx-divine' },
++ 'network.badge:badge': { instance_id: 'inv_4', definition_id: 'itd_01M4GGQ9Z0000000000000000B' },
++ 'live.particle:particle': { instance_id: 'inv_5', definition_id: 'itd_RETIRED' },
++ } : {},
++ })) });
++ }
++ throw new Error(`unexpected ${u.pathname}`);
++}
++
++(async () => {
++ assert.equal(loadConfig({}).inventoryUrl, 'https://inventory.openvibe.network');
++ const inv = createInventory(loadConfig({}), { fetch: fakeFetch });
++
++ // The catalog: kinds once, then each kind's published definitions.
++ const catalog = await loadCatalog(inv);
++ assert.equal(catalog.kinds.size, 4);
++ assert.equal(catalog.definitions.size, 4);
++ assert.deepEqual(calls.filter((c) => c.path === '/api/v1/definitions').map((c) => c.query.kind).sort(), ['live.chat_tag', 'live.hat', 'live.name_effect', 'network.badge']);
++ assert.ok(calls.every((c) => c.auth === null), 'public reads: no token is sent');
++
++ // Who wears what: 100 people per call.
++ calls.length = 0;
++ const many = [A, B, ...Array.from({ length: 150 }, (_, i) => `usr_01JABCDEFGHJKMNPQRST${String(i).padStart(6, '0').replace(/[ILOU]/g, '0')}`)];
++ const eq = await equippedOf(inv, many);
++ assert.equal(eq.size, 152);
++ assert.deepEqual(calls.map((c) => c.query.subjects.split(',').length), [100, 52]);
++ await assert.rejects(equippedOf(inv, ['alex']), /not a person's id/);
++
++ // A's look: hat before, tag in brackets, badge after, the name effect as a class; the retired item shows nothing.
++ const look = lookOf(eq.get(A), catalog);
++ assert.deepEqual([look.before, look.tag, look.after, look.nameClass], [['🤠'], 'VIP', ['⭐'], 'name-fx-divine']);
++ assert.equal(renderLine('alex', look), '🤠 [VIP] alex ⭐ (Cowboy Hat · VIP tag · Divine Name · Early Supporter)');
++ assert.equal(renderHtml('<b>alex</b>', look), '🤠 <span class="ov-tag">VIP</span> <span class="name-fx-divine">&lt;b&gt;alex&lt;/b&gt;</span> ⭐', 'names are escaped');
++ // B wears nothing: just the name.
++ assert.equal(renderLine('sam', lookOf(eq.get(B), catalog)), 'sam');
++ assert.equal(renderHtml('sam', lookOf(eq.get(B), catalog)), '<span>sam</span>');
++ // A class that is not a plain token is never put into the page.
++ assert.equal(renderHtml('x', { ...look, nameClass: 'a" onmouseover="x' }).includes('onmouseover'), false);
++
++ console.log('inventory-wear smoke: public catalog and equipped reads without a token, 100 per call, the look rendered as text and escaped HTML');
++})().catch((err) => { console.error(err); process.exit(1); });
ddiff --git a/examples/inventory-wear/wear.js b/examples/inventory-wear/wear.js
nnew file mode 100644
iindex 0000000..9abcb55
---- /dev/null
++++ b/examples/inventory-wear/wear.js
@@@ -0,0 +1,98 @@
++'use strict';
++/**
++ * Show what people wear on OpenVibe next to their names in your own app: the network-wide inventory
++ * (OpenVibe.Inventory) is public to read, with no token. A person's hat, badge, name effect and chat tag follow them
++ * from site to site, and any app can render them the way OpenVibe.Live's chat does.
++ *
++ * node wear.js usr_01J… usr_01J…
++ * # 🤠 [VIP] alex (Cowboy Hat · name effect: name-fx-divine)
++ *
++ * Two reads, both public and cacheable:
++ * - the catalog: every kind (its slots and how surfaces render it) and the published definitions (names, art,
++ * attributes), which change rarely, so read them once and keep them;
++ * - who wears what: GET /api/v1/equipped for up to 100 people per call, as chat renders do.
++ * Defining and granting items is for each kind's issuer (OpenVibe's own services today); an app reads.
++ */
++const { createClient } = require('openvibe-sdk/core');
++const { createInventoryClient } = require('openvibe-sdk/inventory');
++
++const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
++const BATCH = 100; // Inventory's limit per /equipped call
++
++function loadConfig(env = process.env) {
++ return { inventoryUrl: String(env.OV_INVENTORY_URL || 'https://inventory.openvibe.network').replace(/\/+$/, '') };
++}
++
++/** Inventory's client with no token: these reads are public. */
++function createInventory(config, { fetch } = {}) {
++ return createInventoryClient(createClient({ baseUrls: { inventory: config.inventoryUrl }, ...(fetch ? { fetch } : {}) }));
++}
++
++/** The catalog: kinds by id and published definitions by id (page through a kind's definitions as needed). */
++async function loadCatalog(inv) {
++ const { kinds = [] } = await inv.kinds.list();
++ const definitions = new Map();
++ for (const k of kinds) {
++ const { definitions: list = [] } = await inv.definitions.list({ kind: k.id, limit: 200 });
++ for (const d of list) definitions.set(d.id, d);
++ }
++ return { kinds: new Map(kinds.map((k) => [k.id, k])), definitions };
++}
++
++/** Everyone's equipped slots, 100 people per call → Map(subject → slots). */
++async function equippedOf(inv, subjects) {
++ const want = [...new Set(subjects)];
++ for (const s of want) if (!SUBJECT_RE.test(s)) throw new TypeError(`${s} is not a person's id (usr_…)`);
++ const out = new Map();
++ for (let i = 0; i < want.length; i += BATCH) {
++ const { equipped = [] } = await inv.equipped(want.slice(i, i + BATCH));
++ for (const e of equipped) out.set(e.subject, e.slots || {});
++ }
++ return out;
++}
++
++/**
++ * One person's look as plain parts: the hat's emoji before the name, a chat tag in brackets, a badge's emoji after,
++ * and a name effect as the CSS class OpenVibe's pages use (with its emoji when there is one).
++ */
++function lookOf(slots, catalog) {
++ const look = { before: [], after: [], tag: null, nameClass: null, items: [] };
++ for (const [slotKey, slot] of Object.entries(slots || {})) {
++ const kindId = slotKey.slice(0, slotKey.lastIndexOf(':'));
++ const def = catalog.definitions.get(slot.definition_id);
++ if (!def) continue; // retired or not published: show nothing rather than a guess
++ const emoji = def.art && def.art.emoji;
++ look.items.push({ kind: kindId, name: def.name });
++ if (kindId === 'live.hat' && emoji) look.before.push(emoji);
++ else if (kindId === 'live.chat_tag') look.tag = (def.attributes && def.attributes.text) || def.name;
++ else if (kindId === 'network.badge' && emoji) look.after.push(emoji);
++ else if (kindId === 'live.name_effect') look.nameClass = (def.art && def.art.token) || (def.attributes && def.attributes.css_class) || null;
++ }
++ return look;
++}
++
++/** The text line a terminal or a plain list shows. */
++function renderLine(name, look) {
++ return [...look.before, look.tag ? `[${look.tag}]` : null, name, ...look.after].filter(Boolean).join(' ')
++ + (look.items.length ? ` (${look.items.map((i) => i.name).join(' · ')})` : '');
++}
++
++/** HTML for a web page, escaped; style the name-fx-* classes as you like. */
++function renderHtml(name, look) {
++ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
++ const tag = look.tag ? `<span class="ov-tag">${esc(look.tag)}</span> ` : '';
++ const cls = look.nameClass && /^[a-z0-9-]{1,40}$/.test(look.nameClass) ? ` class="${look.nameClass}"` : '';
++ return `${look.before.map(esc).join('')}${look.before.length ? ' ' : ''}${tag}<span${cls}>${esc(name)}</span>${look.after.length ? ' ' : ''}${look.after.map(esc).join('')}`;
++}
++
++module.exports = { loadConfig, createInventory, loadCatalog, equippedOf, lookOf, renderLine, renderHtml };
++
++if (require.main === module) {
++ (async () => {
++ const subjects = process.argv.slice(2);
++ if (!subjects.length) throw new Error('usage: wear.js <usr_…> [usr_…]');
++ const inv = createInventory(loadConfig());
++ const [catalog, equipped] = await Promise.all([loadCatalog(inv), equippedOf(inv, subjects)]);
++ for (const s of subjects) console.log(renderLine(s, lookOf(equipped.get(s), catalog)));
++ })().catch((err) => { console.error(err.code ? `${err.code}: ${err.message}` : err.message); process.exit(1); });
++}
ddiff --git a/package-lock.json b/package-lock.json
iindex 642bf5f..7fe0ae3 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -77,6 +77,17 @@
"node": ">=22"
}
},
++ "examples/inventory-wear": {
++ "name": "openvibe-example-inventory-wear",
++ "version": "0.1.0",
++ "license": "MIT",
++ "dependencies": {
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.60.0"
++ },
++ "engines": {
++ "node": ">=22"
++ }
++ },
"examples/media-uploader": {
"name": "openvibe-example-media-uploader",
"version": "0.2.0",
@@@ -725,6 +736,10 @@
"resolved": "examples/go-live",
"link": true
},
++ "node_modules/openvibe-example-inventory-wear": {
++ "resolved": "examples/inventory-wear",
++ "link": true
++ },
"node_modules/openvibe-example-media-uploader": {
"resolved": "examples/media-uploader",
"link": true
ddiff --git a/test/structure.test.js b/test/structure.test.js
iindex 814bc30..ab284b6 100644
---- a/test/structure.test.js
++++ b/test/structure.test.js
@@@ -11,13 +11,13 @@ const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
--const EXPECTED = ['ai-chat', 'browser-app', 'chat-bot', 'event-subscriber', 'go-live', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];
++const EXPECTED = ['ai-chat', 'browser-app', 'chat-bot', 'event-subscriber', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];
// The pin every example must share: the repository's own (the pin bot moves them all at once).
const ROOT_PKG = require(path.join(ROOT, 'package.json'));
const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];
const dirs = fs.readdirSync(path.join(ROOT, 'examples')).filter((d) => fs.statSync(path.join(ROOT, 'examples', d)).isDirectory()).sort();
--assert.deepEqual(dirs, EXPECTED, 'the nine charter examples, ai-chat, go-live, space-deploy and zone-s3');
++assert.deepEqual(dirs, EXPECTED, 'the nine charter examples, ai-chat, go-live, inventory-wear, space-deploy and zone-s3');
function sources(dir) {
const out = [];