openvibers/openvibe.examples Commits
v0.3.0: create-openvibe-app (WS-N task 6): start a new app from a tested example (web, server, bot, mod) with its smoke test, a .gitignore, the package renamed and the SDK pinned; no network, refuses a non-empty directory; runs from the tag with npx --package=<tarball> create-openvibe-app; test scaffolds every template and runs its smoke test where it lands
Changes
@@@ -3,7 +3,7 @@> Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials> and public APIs, nothing else.--**Status:** alpha, 0.2.0 (roadmap Wave 20). Nine 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). Nine examples, each with a smoke test that CI runs onNode 22.22.1 against `openvibe-sdk/testing`'s mock platform (no network). `npm run e2e` runs threeof them (Media, Events pull, a Tools job) against the real platform with your own sandbox app; itis not part of CI or `npm test`, and it was not run against production as part of this release.@@@ -34,6 +34,27 @@ Each example folder has a README, a `.env.example`, its own `package.json` and a`test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`,`OV_CLIENT_SECRET`, `OV_PROJECT_ID`, …); secrets are never logged or sent to a browser.++## Start a new app++++`create-openvibe-app` copies one of these examples into a new folder as your starting point, with++its smoke test, a `.gitignore`, the package renamed and the SDK pinned to the same tag. It needs no++network beyond fetching this package.++++```bash++npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web++cd my-app && npm install && cp .env.example .env && npm test++```++++| Template | Starts from | For |++|---|---|---|++| `web` | [browser-app](examples/browser-app) | a plain web page that signs in with OpenVibe (PKCE) and calls a public API |++| `server` | [node-server-app](examples/node-server-app) | a Node server with a client-credentials app token and registry discovery |++| `bot` | [chat-bot](examples/chat-bot) | a chat bot that stays in its one room and always identifies as a bot |++| `mod` | [mod-manifest](examples/mod-manifest) | a mod manifest validated against the schema, capabilities and compatibility ranges |++++Without `--template` it asks on a terminal (and picks `web` otherwise). `npm create openvibe-app`++will work the same way once the package is on the npm registry; until then use the `npx` line.++## End-to-end walkthroughFrom a new account to the examples running against the platform, with public surfaces only.@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W20",-- "version": "0.2.0",++ "version": "0.3.0","sdk": "openvibe-sdk v0.4.0","contracts": "openvibe-contracts v0.28.0 (mod-manifest example)","ci": "every example's smoke test runs on Node 22.22.1 against openvibe-sdk/testing's mock platform (developer-app rules for Events, Media and Tools as in production) plus a local Chat WebSocket mock; none reaches the network; a second job runs the developer path check (scripts/developer-path.js) against the same mock platform",@@@ -1,16 +1,19 @@{"name": "openvibe-examples",-- "version": "0.2.0",++ "version": "0.3.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-examples",-- "version": "0.2.0",++ "version": "0.3.0","license": "MIT","workspaces": ["examples/*"],++ "bin": {++ "create-openvibe-app": "scripts/create-openvibe-app.js"++ },"devDependencies": {"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0"},@@@ -1,10 +1,13 @@{"name": "openvibe-examples",-- "version": "0.2.0",++ "version": "0.3.0","private": true,"description": "Executable public integration examples for the OpenVibe platform: SDK, scoped credentials and public APIs only.","license": "MIT","repository": "github:OpenVibers/OpenVibe.Examples",++ "bin": {++ "create-openvibe-app": "scripts/create-openvibe-app.js"++ },"workspaces": ["examples/*"],nnew file mode 100755@@@ -0,0 +1,116 @@++#!/usr/bin/env node++'use strict';++/**++ * create-openvibe-app (roadmap WS-N task 6): start a new OpenVibe app from one of the tested examples.++ *++ * npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 \++ * create-openvibe-app my-app --template web++ * node scripts/create-openvibe-app.js my-app --template bot (from a checkout)++ *++ * Templates are the examples in this repository, copied as they are tested in CI (smoke test++ * included), with the package renamed, a .gitignore added and the SDK and contracts pinned to the++ * tags the examples use. Nothing is downloaded and nothing is sent anywhere: the templates travel++ * inside this package. The target directory must not exist or must be empty.++ *++ * --template <web|server|bot|mod> default: asked on a terminal, else web++ * --list the templates, then exit++ */++const fs = require('fs');++const path = require('path');++const readline = require('readline');++++const ROOT = path.join(__dirname, '..');++const VERSION = require('../package.json').version;++const TEMPLATES = {++ web: { example: 'browser-app', what: 'a plain web page (no build step) that signs in with OpenVibe (PKCE) and calls a public API' },++ server: { example: 'node-server-app', what: 'a Node server with a client-credentials app token and registry discovery' },++ bot: { example: 'chat-bot', what: 'a chat bot that stays in its one configured room and always identifies as a bot' },++ mod: { example: 'mod-manifest', what: 'a mod manifest validated against the schema, the capability catalog and compatibility ranges' },++};++const SKIP = new Set(['node_modules', '.env', '.DS_Store']);++++function usage(code = 0) {++ const lines = ['Usage: create-openvibe-app <directory> [--template web|server|bot|mod]', '', 'Templates:'];++ for (const [k, t] of Object.entries(TEMPLATES)) lines.push(` ${k.padEnd(7)} ${t.what}`);++ (code ? console.error : console.log)(lines.join('\n'));++ process.exit(code);++}++++function parse(argv) {++ const out = { dir: null, template: null };++ for (let i = 0; i < argv.length; i++) {++ const a = argv[i];++ if (a === '--help' || a === '-h') usage(0);++ else if (a === '--list') usage(0);++ else if (a === '--template' || a === '-t') out.template = argv[++i];++ else if (a.startsWith('--template=')) out.template = a.slice(11);++ else if (a.startsWith('-')) { console.error(`Unknown option ${a}`); usage(2); }++ else if (!out.dir) out.dir = a;++ else { console.error(`Unexpected argument ${a}`); usage(2); }++ }++ return out;++}++++/** A package name npm accepts, from the directory name. */++function packageName(dir) {++ const base = path.basename(path.resolve(dir)).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[-]+$/g, '');++ return base || 'openvibe-app';++}++++function copyDir(from, to) {++ fs.mkdirSync(to, { recursive: true });++ for (const e of fs.readdirSync(from, { withFileTypes: true })) {++ if (SKIP.has(e.name)) continue;++ const s = path.join(from, e.name), d = path.join(to, e.name);++ if (e.isDirectory()) copyDir(s, d);++ else if (e.isFile()) fs.copyFileSync(s, d);++ }++}++++async function ask() {++ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });++ const keys = Object.keys(TEMPLATES);++ console.log('Which template?');++ keys.forEach((k, i) => console.log(` ${i + 1}. ${k.padEnd(7)} ${TEMPLATES[k].what}`));++ const answer = await new Promise((r) => rl.question('Number or name [1]: ', r));++ rl.close();++ const t = answer.trim();++ if (!t) return keys[0];++ return TEMPLATES[t] ? t : keys[Number(t) - 1];++}++++function create(dir, template) {++ const t = TEMPLATES[template];++ if (!t) throw new Error(`Unknown template "${template}" (${Object.keys(TEMPLATES).join(', ')})`);++ const target = path.resolve(dir);++ if (fs.existsSync(target) && fs.readdirSync(target).length) throw new Error(`${dir} exists and is not empty`);++ const src = path.join(ROOT, 'examples', t.example);++ copyDir(src, target);++++ const name = packageName(dir);++ const pkgPath = path.join(target, 'package.json');++ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));++ const next = { name, version: '0.1.0', private: true, description: pkg.description, ...pkg };++ next.name = name; next.version = '0.1.0';++ fs.writeFileSync(pkgPath, `${JSON.stringify(next, null, 2)}\n`);++++ fs.writeFileSync(path.join(target, '.gitignore'), 'node_modules/\n.env\n');++ const readme = path.join(target, 'README.md');++ const body = fs.existsSync(readme) ? fs.readFileSync(readme, 'utf8').replace(/^# .*\n/, '') : '';++ fs.writeFileSync(readme, `# ${name}\n\nStarted with create-openvibe-app ${VERSION} from the \`${template}\` template (OpenVibe.Examples \`examples/${t.example}\`): ${t.what}.\n${body}`);++ return { target, name };++}++++async function main() {++ const opts = parse(process.argv.slice(2));++ if (!opts.dir) usage(2);++ let template = opts.template;++ if (!template) template = process.stdin.isTTY && process.stdout.isTTY ? await ask() : 'web';++ const { target, name } = create(opts.dir, template);++ const rel = path.relative(process.cwd(), target) || '.';++ console.log(`\nCreated ${name} in ${rel} from the ${template} template.\n\nNext:\n cd ${rel}\n npm install\n cp .env.example .env # then fill in your app's values (never commit .env)\n npm test # the template's smoke test, no network needed\n npm start\n\nCreate a project and an app at https://openvibe.codes, and read https://openvibe.codes/docs.`);++}++++if (require.main === module) main().catch((err) => { console.error(err.message); process.exit(1); });++++module.exports = { TEMPLATES, create, packageName };nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++/**++ * create-openvibe-app (WS-N task 6): every template scaffolds a working app. The copy is the tested++ * example without node_modules or .env, renamed, with a .gitignore, and its own smoke test passes++ * where it lands (resolving the SDK from this repository's install, as `npm install` would).++ * A non-empty target and an unknown template are refused; nothing needs the network.++ */++const assert = require('node:assert/strict');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { spawnSync } = require('child_process');++++const ROOT = path.join(__dirname, '..');++const BIN = path.join(ROOT, 'scripts', 'create-openvibe-app.js');++const { TEMPLATES, packageName } = require(BIN);++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'create-ov-app-'));++const run = (args, opts = {}) => spawnSync(process.execPath, [BIN, ...args], { cwd: tmp, encoding: 'utf8', ...opts });++++try {++ assert.equal(require(path.join(ROOT, 'package.json')).bin['create-openvibe-app'], 'scripts/create-openvibe-app.js', 'the package exposes the bin');++ assert.equal(packageName('My Cool App!'), 'my-cool-app');++++ for (const [template, t] of Object.entries(TEMPLATES)) {++ const dir = `app-${template}`;++ const r = run([dir, '--template', template]);++ assert.equal(r.status, 0, r.stderr);++ assert.match(r.stdout, new RegExp(`Created app-${template} in ${dir} from the ${template} template`));++ const target = path.join(tmp, dir);++ const files = fs.readdirSync(target);++ assert.ok(!files.includes('node_modules') && !files.includes('.env'), `${template}: no node_modules or .env copied`);++ assert.ok(files.includes('.gitignore') && files.includes('.env.example') && files.includes('README.md'), `${template}: .gitignore, .env.example, README`);++ assert.equal(fs.readFileSync(path.join(target, '.gitignore'), 'utf8'), 'node_modules/\n.env\n');++ const pkg = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8'));++ const orig = require(path.join(ROOT, 'examples', t.example, 'package.json'));++ assert.equal(pkg.name, `app-${template}`);++ assert.equal(pkg.version, '0.1.0');++ assert.deepEqual(pkg.dependencies || {}, orig.dependencies || {}, `${template}: the pinned dependencies are the example's`);++ assert.match(fs.readFileSync(path.join(target, 'README.md'), 'utf8'), new RegExp(`^# app-${template}\\n\\nStarted with create-openvibe-app`));++ // The scaffolded app's own smoke test, where it landed.++ const smoke = spawnSync(process.execPath, ['test/smoke.test.js'], { cwd: target, encoding: 'utf8', env: { ...process.env, NODE_PATH: path.join(ROOT, 'node_modules') }, timeout: 60000 });++ assert.equal(smoke.status, 0, `${template} smoke:\n${smoke.stdout}\n${smoke.stderr}`);++ }++++ // Refusals: a non-empty directory, an unknown template.++ let r = run(['app-web', '--template', 'web']);++ assert.notEqual(r.status, 0); assert.match(r.stderr, /exists and is not empty/);++ r = run(['other', '--template', 'nope']);++ assert.notEqual(r.status, 0); assert.match(r.stderr, /Unknown template "nope"/);++ // Without a terminal and without --template, the web template.++ r = run(['plain'], { input: '' });++ assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /from the web template/);++ console.log('create-openvibe-app: all checks passed');++} finally {++ fs.rmSync(tmp, { recursive: true, force: true });++}
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
> Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials
> Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials
> and public APIs, nothing else.
> and public APIs, nothing else.
-**Status:** alpha, 0.2.0 (roadmap Wave 20). Nine 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). Nine 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
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
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.
is not part of CI or `npm test`, and it was not run against production as part of this release.
@@ -34,6 +34,27 @@ Each example folder has a README, a `.env.example`, its own `package.json` and a
@@ -34,6 +34,27 @@ Each example folder has a README, a `.env.example`, its own `package.json` and a
`test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`,
`test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`,
`OV_CLIENT_SECRET`, `OV_PROJECT_ID`, …); secrets are never logged or sent to a browser.
`OV_CLIENT_SECRET`, `OV_PROJECT_ID`, …); secrets are never logged or sent to a browser.
+## Start a new app
+
+`create-openvibe-app` copies one of these examples into a new folder as your starting point, with
+its smoke test, a `.gitignore`, the package renamed and the SDK pinned to the same tag. It needs no
+network beyond fetching this package.
+
+```bash
+npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web
+cd my-app && npm install && cp .env.example .env && npm test
+```
+
+| Template | Starts from | For |
+|---|---|---|
+| `web` | [browser-app](examples/browser-app) | a plain web page that signs in with OpenVibe (PKCE) and calls a public API |
+| `server` | [node-server-app](examples/node-server-app) | a Node server with a client-credentials app token and registry discovery |
+| `bot` | [chat-bot](examples/chat-bot) | a chat bot that stays in its one room and always identifies as a bot |
+| `mod` | [mod-manifest](examples/mod-manifest) | a mod manifest validated against the schema, capabilities and compatibility ranges |
+
+Without `--template` it asks on a terminal (and picks `web` otherwise). `npm create openvibe-app`
+will work the same way once the package is on the npm registry; until then use the `npx` line.
+
## End-to-end walkthrough
## End-to-end walkthrough
From a new account to the examples running against the platform, with public surfaces only.
From a new account to the examples running against the platform, with public surfaces only.
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W20",
"wave": "W20",
- "version": "0.2.0",
+ "version": "0.3.0",
"sdk": "openvibe-sdk v0.4.0",
"sdk": "openvibe-sdk v0.4.0",
"contracts": "openvibe-contracts v0.28.0 (mod-manifest example)",
"contracts": "openvibe-contracts v0.28.0 (mod-manifest example)",
"ci": "every example's smoke test runs on Node 22.22.1 against openvibe-sdk/testing's mock platform (developer-app rules for Events, Media and Tools as in production) plus a local Chat WebSocket mock; none reaches the network; a second job runs the developer path check (scripts/developer-path.js) against the same mock platform",
"ci": "every example's smoke test runs on Node 22.22.1 against openvibe-sdk/testing's mock platform (developer-app rules for Events, Media and Tools as in production) plus a local Chat WebSocket mock; none reaches the network; a second job runs the developer path check (scripts/developer-path.js) against the same mock platform",
@@ -1,16 +1,19 @@
@@ -1,16 +1,19 @@
{
{
"name": "openvibe-examples",
"name": "openvibe-examples",
- "version": "0.2.0",
+ "version": "0.3.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-examples",
"name": "openvibe-examples",
- "version": "0.2.0",
+ "version": "0.3.0",
"license": "MIT",
"license": "MIT",
"workspaces": [
"workspaces": [
"examples/*"
"examples/*"
],
],
+ "bin": {
+ "create-openvibe-app": "scripts/create-openvibe-app.js"
+ },
"devDependencies": {
"devDependencies": {
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0"
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0"
},
},
@@ -1,10 +1,13 @@
@@ -1,10 +1,13 @@
{
{
"name": "openvibe-examples",
"name": "openvibe-examples",
- "version": "0.2.0",
+ "version": "0.3.0",
"private": true,
"private": true,
"description": "Executable public integration examples for the OpenVibe platform: SDK, scoped credentials and public APIs only.",
"description": "Executable public integration examples for the OpenVibe platform: SDK, scoped credentials and public APIs only.",
"license": "MIT",
"license": "MIT",
"repository": "github:OpenVibers/OpenVibe.Examples",
"repository": "github:OpenVibers/OpenVibe.Examples",
+ "bin": {
+ "create-openvibe-app": "scripts/create-openvibe-app.js"
+ },
"workspaces": [
"workspaces": [
"examples/*"
"examples/*"
],
],
new file mode 100755
new file mode 100755
@@ -0,0 +1,116 @@
@@ -0,0 +1,116 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * create-openvibe-app (roadmap WS-N task 6): start a new OpenVibe app from one of the tested examples.
+ *
+ * npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 \
+ * create-openvibe-app my-app --template web
+ * node scripts/create-openvibe-app.js my-app --template bot (from a checkout)
+ *
+ * Templates are the examples in this repository, copied as they are tested in CI (smoke test
+ * included), with the package renamed, a .gitignore added and the SDK and contracts pinned to the
+ * tags the examples use. Nothing is downloaded and nothing is sent anywhere: the templates travel
+ * inside this package. The target directory must not exist or must be empty.
+ *
+ * --template <web|server|bot|mod> default: asked on a terminal, else web
+ * --list the templates, then exit
+ */
+const fs = require('fs');
+const path = require('path');
+const readline = require('readline');
+
+const ROOT = path.join(__dirname, '..');
+const VERSION = require('../package.json').version;
+const TEMPLATES = {
+ web: { example: 'browser-app', what: 'a plain web page (no build step) that signs in with OpenVibe (PKCE) and calls a public API' },
+ server: { example: 'node-server-app', what: 'a Node server with a client-credentials app token and registry discovery' },
+ bot: { example: 'chat-bot', what: 'a chat bot that stays in its one configured room and always identifies as a bot' },
+ mod: { example: 'mod-manifest', what: 'a mod manifest validated against the schema, the capability catalog and compatibility ranges' },
+};
+const SKIP = new Set(['node_modules', '.env', '.DS_Store']);
+
+function usage(code = 0) {
+ const lines = ['Usage: create-openvibe-app <directory> [--template web|server|bot|mod]', '', 'Templates:'];
+ for (const [k, t] of Object.entries(TEMPLATES)) lines.push(` ${k.padEnd(7)} ${t.what}`);
+ (code ? console.error : console.log)(lines.join('\n'));
+ process.exit(code);
+}
+
+function parse(argv) {
+ const out = { dir: null, template: null };
+ for (let i = 0; i < argv.length; i++) {
+ const a = argv[i];
+ if (a === '--help' || a === '-h') usage(0);
+ else if (a === '--list') usage(0);
+ else if (a === '--template' || a === '-t') out.template = argv[++i];
+ else if (a.startsWith('--template=')) out.template = a.slice(11);
+ else if (a.startsWith('-')) { console.error(`Unknown option ${a}`); usage(2); }
+ else if (!out.dir) out.dir = a;
+ else { console.error(`Unexpected argument ${a}`); usage(2); }
+ }
+ return out;
+}
+
+/** A package name npm accepts, from the directory name. */
+function packageName(dir) {
+ const base = path.basename(path.resolve(dir)).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[-]+$/g, '');
+ return base || 'openvibe-app';
+}
+
+function copyDir(from, to) {
+ fs.mkdirSync(to, { recursive: true });
+ for (const e of fs.readdirSync(from, { withFileTypes: true })) {
+ if (SKIP.has(e.name)) continue;
+ const s = path.join(from, e.name), d = path.join(to, e.name);
+ if (e.isDirectory()) copyDir(s, d);
+ else if (e.isFile()) fs.copyFileSync(s, d);
+ }
+}
+
+async function ask() {
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+ const keys = Object.keys(TEMPLATES);
+ console.log('Which template?');
+ keys.forEach((k, i) => console.log(` ${i + 1}. ${k.padEnd(7)} ${TEMPLATES[k].what}`));
+ const answer = await new Promise((r) => rl.question('Number or name [1]: ', r));
+ rl.close();
+ const t = answer.trim();
+ if (!t) return keys[0];
+ return TEMPLATES[t] ? t : keys[Number(t) - 1];
+}
+
+function create(dir, template) {
+ const t = TEMPLATES[template];
+ if (!t) throw new Error(`Unknown template "${template}" (${Object.keys(TEMPLATES).join(', ')})`);
+ const target = path.resolve(dir);
+ if (fs.existsSync(target) && fs.readdirSync(target).length) throw new Error(`${dir} exists and is not empty`);
+ const src = path.join(ROOT, 'examples', t.example);
+ copyDir(src, target);
+
+ const name = packageName(dir);
+ const pkgPath = path.join(target, 'package.json');
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
+ const next = { name, version: '0.1.0', private: true, description: pkg.description, ...pkg };
+ next.name = name; next.version = '0.1.0';
+ fs.writeFileSync(pkgPath, `${JSON.stringify(next, null, 2)}\n`);
+
+ fs.writeFileSync(path.join(target, '.gitignore'), 'node_modules/\n.env\n');
+ const readme = path.join(target, 'README.md');
+ const body = fs.existsSync(readme) ? fs.readFileSync(readme, 'utf8').replace(/^# .*\n/, '') : '';
+ fs.writeFileSync(readme, `# ${name}\n\nStarted with create-openvibe-app ${VERSION} from the \`${template}\` template (OpenVibe.Examples \`examples/${t.example}\`): ${t.what}.\n${body}`);
+ return { target, name };
+}
+
+async function main() {
+ const opts = parse(process.argv.slice(2));
+ if (!opts.dir) usage(2);
+ let template = opts.template;
+ if (!template) template = process.stdin.isTTY && process.stdout.isTTY ? await ask() : 'web';
+ const { target, name } = create(opts.dir, template);
+ const rel = path.relative(process.cwd(), target) || '.';
+ console.log(`\nCreated ${name} in ${rel} from the ${template} template.\n\nNext:\n cd ${rel}\n npm install\n cp .env.example .env # then fill in your app's values (never commit .env)\n npm test # the template's smoke test, no network needed\n npm start\n\nCreate a project and an app at https://openvibe.codes, and read https://openvibe.codes/docs.`);
+}
+
+if (require.main === module) main().catch((err) => { console.error(err.message); process.exit(1); });
+
+module.exports = { TEMPLATES, create, packageName };
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+'use strict';
+/**
+ * create-openvibe-app (WS-N task 6): every template scaffolds a working app. The copy is the tested
+ * example without node_modules or .env, renamed, with a .gitignore, and its own smoke test passes
+ * where it lands (resolving the SDK from this repository's install, as `npm install` would).
+ * A non-empty target and an unknown template are refused; nothing needs the network.
+ */
+const assert = require('node:assert/strict');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const ROOT = path.join(__dirname, '..');
+const BIN = path.join(ROOT, 'scripts', 'create-openvibe-app.js');
+const { TEMPLATES, packageName } = require(BIN);
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'create-ov-app-'));
+const run = (args, opts = {}) => spawnSync(process.execPath, [BIN, ...args], { cwd: tmp, encoding: 'utf8', ...opts });
+
+try {
+ assert.equal(require(path.join(ROOT, 'package.json')).bin['create-openvibe-app'], 'scripts/create-openvibe-app.js', 'the package exposes the bin');
+ assert.equal(packageName('My Cool App!'), 'my-cool-app');
+
+ for (const [template, t] of Object.entries(TEMPLATES)) {
+ const dir = `app-${template}`;
+ const r = run([dir, '--template', template]);
+ assert.equal(r.status, 0, r.stderr);
+ assert.match(r.stdout, new RegExp(`Created app-${template} in ${dir} from the ${template} template`));
+ const target = path.join(tmp, dir);
+ const files = fs.readdirSync(target);
+ assert.ok(!files.includes('node_modules') && !files.includes('.env'), `${template}: no node_modules or .env copied`);
+ assert.ok(files.includes('.gitignore') && files.includes('.env.example') && files.includes('README.md'), `${template}: .gitignore, .env.example, README`);
+ assert.equal(fs.readFileSync(path.join(target, '.gitignore'), 'utf8'), 'node_modules/\n.env\n');
+ const pkg = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8'));
+ const orig = require(path.join(ROOT, 'examples', t.example, 'package.json'));
+ assert.equal(pkg.name, `app-${template}`);
+ assert.equal(pkg.version, '0.1.0');
+ assert.deepEqual(pkg.dependencies || {}, orig.dependencies || {}, `${template}: the pinned dependencies are the example's`);
+ assert.match(fs.readFileSync(path.join(target, 'README.md'), 'utf8'), new RegExp(`^# app-${template}\\n\\nStarted with create-openvibe-app`));
+ // The scaffolded app's own smoke test, where it landed.
+ const smoke = spawnSync(process.execPath, ['test/smoke.test.js'], { cwd: target, encoding: 'utf8', env: { ...process.env, NODE_PATH: path.join(ROOT, 'node_modules') }, timeout: 60000 });
+ assert.equal(smoke.status, 0, `${template} smoke:\n${smoke.stdout}\n${smoke.stderr}`);
+ }
+
+ // Refusals: a non-empty directory, an unknown template.
+ let r = run(['app-web', '--template', 'web']);
+ assert.notEqual(r.status, 0); assert.match(r.stderr, /exists and is not empty/);
+ r = run(['other', '--template', 'nope']);
+ assert.notEqual(r.status, 0); assert.match(r.stderr, /Unknown template "nope"/);
+ // Without a terminal and without --template, the web template.
+ r = run(['plain'], { input: '' });
+ assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /from the web template/);
+ console.log('create-openvibe-app: all checks passed');
+} finally {
+ fs.rmSync(tmp, { recursive: true, force: true });
+}