openvibers/openvibe.food Commits
Drop the skeleton's generator and its check (they belong to the skeleton)
Changes
ddeleted file mode 100644@@@ -1,129 +0,0 @@--#!/usr/bin/env node--'use strict';----/**-- * new-service.js — make a new OpenVibe service from this skeleton.-- *-- * node scripts/new-service.js --id work --name OpenVibe.Work --domain openvibe.work --port 4960 \-- * --tagline "Find work, with an agent on your side." --env WORK --out ../openvibe-work-- *-- * It copies the tree this file lives in (everything except node_modules/, .git/ and .claude/) to --out, replaces-- * the six placeholders in file contents AND in file names, and prints the next steps. Nothing else is touched:-- * no install, no commit, no deploy.-- *-- * The placeholders say: the service id (lowercase; config.service, the session cookies, the OAuth client id, the-- * systemd unit), the display name ("OpenVibe.Work"), the site ("openvibe.work"), the loopback port, the tagline,-- * and the environment prefix (WORK_LIMITS_MINUTE, WORK_PGLITE_DIR, WORK_TEST_STORE …). They are the TOKENS below,-- * built from parts on purpose: this file lives in the tree it copies, and a literal placeholder here would be-- * rewritten when a service is generated from a generated service.-- */--const fs = require('fs');--const path = require('path');----const T = (name) => `__${name}__`;--const TOKENS = [T('ID'), T('NAME'), T('DOMAIN'), T('PORT'), T('TAGLINE'), T('ENV')];--const TOKEN_RE = new RegExp(TOKENS.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');----const SKIP = new Set(['node_modules', '.git', '.claude']);----function usage(msg) {-- if (msg) process.stderr.write(`new-service: ${msg}\n\n`);-- process.stderr.write(`Usage: node scripts/new-service.js --id <id> --name <name> --domain <domain> --port <port> ` +-- `--tagline "<line>" --env <ENV> --out <dir>\n\n` +-- ` --id service id, lowercase (work)\n` +-- ` --name display name (OpenVibe.Work)\n` +-- ` --domain site (openvibe.work)\n` +-- ` --port loopback port (4960)\n` +-- ` --tagline one line (Find work, with an agent on your side.)\n` +-- ` --env environment prefix, uppercase (WORK)\n` +-- ` --out directory to write the new service into (must not exist, or be empty)\n`);-- process.exit(2);--}----function parseArgs(argv) {-- const out = {};-- for (let i = 0; i < argv.length; i++) {-- const a = argv[i];-- if (!a.startsWith('--')) continue;-- const eq = a.indexOf('=');-- if (eq !== -1) { out[a.slice(2, eq)] = a.slice(eq + 1); continue; }-- const next = argv[i + 1];-- if (next == null || next.startsWith('--')) { out[a.slice(2)] = ''; continue; }-- out[a.slice(2)] = next;-- i++;-- }-- return out;--}----function replacements(args) {-- const id = String(args.id || '');-- const name = String(args.name || '');-- const domain = String(args.domain || '');-- const env = String(args.env || '');-- const port = String(args.port || '');-- const tagline = String(args.tagline || '').trim();---- if (!/^[a-z][a-z0-9-]{1,39}$/.test(id)) usage(`--id must be lowercase letters, digits and dashes (got ${JSON.stringify(id)})`);-- if (!name) usage('--name is required');-- if (!/^[a-z0-9][a-z0-9.-]{1,80}$/.test(domain)) usage(`--domain looks wrong (got ${JSON.stringify(domain)})`);-- if (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) usage(`--port must be 1-65535 (got ${JSON.stringify(port)})`);-- if (!/^[A-Z][A-Z0-9_]{0,31}$/.test(env)) usage(`--env must be uppercase letters, digits and underscores (got ${JSON.stringify(env)})`);-- if (!tagline) usage('--tagline is required');-- if (!String(args.out || '')) usage('--out is required');---- return {-- values: { [T('ID')]: id, [T('NAME')]: name, [T('DOMAIN')]: domain, [T('PORT')]: port, [T('TAGLINE')]: tagline, [T('ENV')]: env },-- meta: { id, name, domain, port, env, tagline },-- };--}----const looksBinary = (buf) => buf.includes(0);----function copyTree(src, dest, values) {-- fs.mkdirSync(dest, { recursive: true });-- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {-- if (SKIP.has(entry.name)) continue;-- const from = path.join(src, entry.name);-- const to = path.join(dest, entry.name.replace(TOKEN_RE, (t) => values[t]));-- if (entry.isDirectory()) { copyTree(from, to, values); continue; }-- if (entry.isSymbolicLink()) continue; // node_modules is the only symlink; anything else is environment-- const buf = fs.readFileSync(from);-- const out = looksBinary(buf) ? buf : Buffer.from(buf.toString('utf8').replace(TOKEN_RE, (t) => values[t]), 'utf8');-- fs.writeFileSync(to, out);-- }--}----function main(argv) {-- const args = parseArgs(argv);-- if (args.help != null) usage();-- const { values, meta } = replacements(args);---- const root = path.join(__dirname, '..');-- const out = path.resolve(process.cwd(), String(args.out));-- if (fs.existsSync(out) && fs.readdirSync(out).length) usage(`--out ${out} exists and is not empty`);-- if (out === root || root.startsWith(`${out}${path.sep}`) || out.startsWith(`${root}${path.sep}`)) {-- usage('--out must be outside the skeleton, and must not contain it');-- }---- copyTree(root, out, values);---- process.stdout.write(`Generated ${meta.name} in ${out}\n\n`-- + ` service id ${meta.id}\n`-- + ` name ${meta.name}\n`-- + ` domain ${meta.domain}\n`-- + ` port ${meta.port}\n`-- + ` tagline ${meta.tagline}\n`-- + ` env prefix ${meta.env}\n\n`-- + `Next steps:\n`-- + ` 1. cd ${out}\n`-- + ` 2. npm install\n`-- + ` 3. npm test # every test/*.test.js on temp PGlite databases, with a mock Network\n`-- + ` 4. npm run dev # http://localhost:${meta.port}, PGlite in data/pglite\n`-- + ` 5. Register the service and its capabilities in OpenVibe.Contracts (CI: contracts-service: ${meta.id})\n`-- + ` and with OpenVibe.Services; write its tables in migrations/0001_initial.sql, its routes in\n`-- + ` server/http/api.js (capabilities in server/http/principal.js, budgets in server/http/caller-limits.js)\n`-- + ` and its pages in server/http/pages.js.\n`-- + ` 6. Fill in STATUS.json, README.md and CHANGELOG.md, then deploy with the unit and vhost in deploy/.\n`);--}----main(process.argv.slice(2));ddeleted file mode 100644@@@ -1,139 +0,0 @@--'use strict';--/**-- * The skeleton's own test: generating a service from it must leave no placeholder behind, in file names or in file-- * contents, and the names the generator derives (the package, the config, the deploy files, the environment) must be-- * the ones a service needs. It runs scripts/new-service.js into a temp directory with fixed sample values, inspects-- * the result, and leaves nothing behind.-- *-- * The placeholder strings are built from parts: this file is copied into every generated service, and a literal-- * placeholder here would be rewritten on the way in.-- */--const assert = require('assert');--const fs = require('fs');--const os = require('os');--const path = require('path');--const { execFileSync } = require('child_process');--const { check, done } = require('./helpers/boot');----const TOKEN_NAMES = ['ID', 'NAME', 'DOMAIN', 'PORT', 'TAGLINE', 'ENV'];--const TOKEN_RE = new RegExp(`__(${TOKEN_NAMES.join('|')})__`);--const token = (name) => `__${name}__`;----const ROOT = path.join(__dirname, '..');--const SAMPLE = { id: 'sample', name: 'OpenVibe.Sample', domain: 'openvibe.sample', port: '4999', tagline: 'A sample service.', env: 'SAMPLE' };----const created = [];--const tmpDir = (prefix) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); created.push(d); return d; };----function runGenerator(out) {-- const args = [-- path.join(ROOT, 'scripts', 'new-service.js'),-- '--id', SAMPLE.id, '--name', SAMPLE.name, '--domain', SAMPLE.domain,-- '--port', SAMPLE.port, '--tagline', SAMPLE.tagline, '--env', SAMPLE.env, '--out', out,-- ];-- // stderr is captured, not shown: the generator's refusals are checked below, and a test file's output stays clean.-- return execFileSync(process.execPath, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });--}----function walk(dir, out = []) {-- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {-- const p = path.join(dir, e.name);-- if (e.isDirectory()) walk(p, out); else out.push(p);-- }-- return out;--}--const rel = (p, dir) => path.relative(dir, p).split(path.sep).join('/');--const text = (p) => fs.readFileSync(p, 'utf8');----(async () => {-- // This test is about the skeleton's generator, and only the skeleton can run it: it asserts that generating a-- // service rewrites the placeholders, and a generated service has none left to rewrite (its package name is-- // already its own, its vhost is already named). A generated service says so and skips, rather than reporting a-- // failure it cannot fix — the convention openvibe-shared/test-runner reads (`<label>: skipped (<why>)`), so the-- // file is listed with ○ and never counted as passed.-- if (JSON.parse(text(path.join(ROOT, 'package.json'))).name !== `openvibe-${token('ID')}`) {-- process.stdout.write('skeleton generator: skipped (this is a generated service, not the skeleton it came from; run it in OpenVibe.ServiceSkeleton)\n');-- return done();-- }-- const base = tmpDir('ov-new-service-');-- const dir = path.join(base, 'openvibe-sample');-- const printed = runGenerator(dir);---- await check('the generator writes a service and prints the next steps', () => {-- assert.ok(fs.existsSync(dir), 'no output directory');-- assert.ok(printed.includes(`Generated ${SAMPLE.name} in ${dir}`), printed.slice(0, 200));-- assert.ok(printed.includes('npm install') && printed.includes('npm test'), 'the next steps name the first commands');-- assert.ok(!printed.includes(token('ID')) && !printed.includes(token('NAME')), 'the output still carries a placeholder');-- });---- await check('no placeholder is left in any file name or file content', () => {-- const files = walk(dir);-- assert.ok(files.length > 30, `only ${files.length} files were generated`);-- const badNames = files.map((f) => rel(f, dir)).filter((r) => TOKEN_RE.test(r));-- assert.deepStrictEqual(badNames, [], 'a file name still carries a placeholder');-- const badContents = files.filter((f) => TOKEN_RE.test(text(f))).map((f) => rel(f, dir));-- assert.deepStrictEqual(badContents, [], 'a file still carries a placeholder');-- });---- await check('the package, the config and the environment are the generated service\'s', () => {-- const pkg = JSON.parse(text(path.join(dir, 'package.json')));-- assert.strictEqual(pkg.name, `openvibe-${SAMPLE.id}`);-- assert.strictEqual(pkg.version, '0.1.0');-- // A generated service's own description is the name and its tagline; the skeleton's is the placeholders.-- assert.ok(pkg.description.startsWith(SAMPLE.name), pkg.description);-- assert.ok(pkg.scripts['test:pg'].includes(`${SAMPLE.env}_TEST_STORE=pg`), pkg.scripts['test:pg']);---- const config = text(path.join(dir, 'server', 'config.js'));-- assert.ok(config.includes(`service: '${SAMPLE.id}'`), 'config.service');-- assert.ok(config.includes(`env.PORT, ${SAMPLE.port}`), 'the default port');-- assert.ok(config.includes(`'https://${SAMPLE.domain}'`), 'the production base URL');-- assert.ok(config.includes(`'ov:${SAMPLE.id}:'`), 'the Valkey prefix');-- assert.ok(config.includes(`${SAMPLE.env}_LIMITS_MINUTE`), 'the limits env prefix');-- assert.ok(config.includes(`${SAMPLE.env}_PGLITE_DIR`), 'the PGlite env prefix');---- const env = text(path.join(dir, '.env.example'));-- for (const line of [`PORT=${SAMPLE.port}`, `BASE_URL=https://${SAMPLE.domain}`, `OV_OAUTH_CLIENT_ID=${SAMPLE.id}`, `VALKEY_PREFIX=ov:${SAMPLE.id}:`]) {-- assert.ok(env.includes(line), `.env.example is missing ${line}`);-- }---- const auth = text(path.join(dir, 'server', 'auth', 'sso.js'));-- assert.ok(auth.includes(`'${SAMPLE.id}_at'`) && auth.includes(`'${SAMPLE.id}_rt'`) && auth.includes(`'${SAMPLE.id}_oauth'`), 'the session cookies');---- const status = JSON.parse(text(path.join(dir, 'STATUS.json')));-- assert.strictEqual(status.domain, SAMPLE.domain);-- assert.ok(text(path.join(dir, 'README.md')).includes(SAMPLE.name), 'the README names the service');-- });---- await check('the deploy files carry the domain, the id and the port', () => {-- const nginx = text(path.join(dir, 'deploy', 'nginx', `${SAMPLE.domain}.conf`));-- assert.ok(nginx.includes(`server_name ${SAMPLE.domain} www.${SAMPLE.domain};`), 'the vhost name');-- assert.ok(nginx.includes(`proxy_pass http://127.0.0.1:${SAMPLE.port};`), 'the upstream port');-- assert.ok(nginx.includes(`zone=${SAMPLE.id}_api`), 'the api limit zone');-- assert.ok(!fs.existsSync(path.join(dir, 'deploy', 'nginx', `${token('DOMAIN')}.conf`)), 'the template name is still there');---- const unit = text(path.join(dir, 'deploy', 'systemd', `openvibe-${SAMPLE.id}.service`));-- assert.ok(unit.includes(`Description=${SAMPLE.name}`), 'the unit description');-- assert.ok(unit.includes(`Environment=PORT=${SAMPLE.port}`), 'the unit port');-- assert.ok(unit.includes(`/etc/openvibe/${SAMPLE.id}.env`), 'the environment file');---- assert.ok(fs.existsSync(path.join(dir, 'migrations', '0001_initial.sql')), 'the initial migration');-- assert.ok(fs.existsSync(path.join(dir, 'public', 'css', 'app.css')), 'the stylesheet');-- });---- await check('the copy skips node_modules and .git, and the skeleton\'s own tools travel with it', () => {-- assert.ok(!fs.existsSync(path.join(dir, 'node_modules')), 'node_modules was copied');-- assert.ok(!fs.existsSync(path.join(dir, '.git')), '.git was copied');-- assert.ok(fs.existsSync(path.join(dir, 'scripts', 'new-service.js')), 'the generator is in the generated tree');-- assert.ok(fs.existsSync(path.join(dir, 'test', 'run.js')), 'the test runner is in the generated tree');-- });---- await check('generating refuses to overwrite a directory that has something in it', () => {-- let failed = null;-- try { runGenerator(dir); } catch (err) { failed = err; }-- assert.ok(failed, 'a second run into the same directory must fail');-- assert.match(String((failed && failed.stderr) || ''), /not empty/);-- });---- for (const d of created) fs.rmSync(d, { recursive: true, force: true });-- done();--})().catch((err) => { console.error(err); process.exit(1); });
deleted file mode 100644
deleted file mode 100644
@@ -1,129 +0,0 @@
@@ -1,129 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-
-/**
- * new-service.js — make a new OpenVibe service from this skeleton.
- *
- * node scripts/new-service.js --id work --name OpenVibe.Work --domain openvibe.work --port 4960 \
- * --tagline "Find work, with an agent on your side." --env WORK --out ../openvibe-work
- *
- * It copies the tree this file lives in (everything except node_modules/, .git/ and .claude/) to --out, replaces
- * the six placeholders in file contents AND in file names, and prints the next steps. Nothing else is touched:
- * no install, no commit, no deploy.
- *
- * The placeholders say: the service id (lowercase; config.service, the session cookies, the OAuth client id, the
- * systemd unit), the display name ("OpenVibe.Work"), the site ("openvibe.work"), the loopback port, the tagline,
- * and the environment prefix (WORK_LIMITS_MINUTE, WORK_PGLITE_DIR, WORK_TEST_STORE …). They are the TOKENS below,
- * built from parts on purpose: this file lives in the tree it copies, and a literal placeholder here would be
- * rewritten when a service is generated from a generated service.
- */
-const fs = require('fs');
-const path = require('path');
-
-const T = (name) => `__${name}__`;
-const TOKENS = [T('ID'), T('NAME'), T('DOMAIN'), T('PORT'), T('TAGLINE'), T('ENV')];
-const TOKEN_RE = new RegExp(TOKENS.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
-
-const SKIP = new Set(['node_modules', '.git', '.claude']);
-
-function usage(msg) {
- if (msg) process.stderr.write(`new-service: ${msg}\n\n`);
- process.stderr.write(`Usage: node scripts/new-service.js --id <id> --name <name> --domain <domain> --port <port> ` +
- `--tagline "<line>" --env <ENV> --out <dir>\n\n` +
- ` --id service id, lowercase (work)\n` +
- ` --name display name (OpenVibe.Work)\n` +
- ` --domain site (openvibe.work)\n` +
- ` --port loopback port (4960)\n` +
- ` --tagline one line (Find work, with an agent on your side.)\n` +
- ` --env environment prefix, uppercase (WORK)\n` +
- ` --out directory to write the new service into (must not exist, or be empty)\n`);
- process.exit(2);
-}
-
-function parseArgs(argv) {
- const out = {};
- for (let i = 0; i < argv.length; i++) {
- const a = argv[i];
- if (!a.startsWith('--')) continue;
- const eq = a.indexOf('=');
- if (eq !== -1) { out[a.slice(2, eq)] = a.slice(eq + 1); continue; }
- const next = argv[i + 1];
- if (next == null || next.startsWith('--')) { out[a.slice(2)] = ''; continue; }
- out[a.slice(2)] = next;
- i++;
- }
- return out;
-}
-
-function replacements(args) {
- const id = String(args.id || '');
- const name = String(args.name || '');
- const domain = String(args.domain || '');
- const env = String(args.env || '');
- const port = String(args.port || '');
- const tagline = String(args.tagline || '').trim();
-
- if (!/^[a-z][a-z0-9-]{1,39}$/.test(id)) usage(`--id must be lowercase letters, digits and dashes (got ${JSON.stringify(id)})`);
- if (!name) usage('--name is required');
- if (!/^[a-z0-9][a-z0-9.-]{1,80}$/.test(domain)) usage(`--domain looks wrong (got ${JSON.stringify(domain)})`);
- if (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) usage(`--port must be 1-65535 (got ${JSON.stringify(port)})`);
- if (!/^[A-Z][A-Z0-9_]{0,31}$/.test(env)) usage(`--env must be uppercase letters, digits and underscores (got ${JSON.stringify(env)})`);
- if (!tagline) usage('--tagline is required');
- if (!String(args.out || '')) usage('--out is required');
-
- return {
- values: { [T('ID')]: id, [T('NAME')]: name, [T('DOMAIN')]: domain, [T('PORT')]: port, [T('TAGLINE')]: tagline, [T('ENV')]: env },
- meta: { id, name, domain, port, env, tagline },
- };
-}
-
-const looksBinary = (buf) => buf.includes(0);
-
-function copyTree(src, dest, values) {
- fs.mkdirSync(dest, { recursive: true });
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
- if (SKIP.has(entry.name)) continue;
- const from = path.join(src, entry.name);
- const to = path.join(dest, entry.name.replace(TOKEN_RE, (t) => values[t]));
- if (entry.isDirectory()) { copyTree(from, to, values); continue; }
- if (entry.isSymbolicLink()) continue; // node_modules is the only symlink; anything else is environment
- const buf = fs.readFileSync(from);
- const out = looksBinary(buf) ? buf : Buffer.from(buf.toString('utf8').replace(TOKEN_RE, (t) => values[t]), 'utf8');
- fs.writeFileSync(to, out);
- }
-}
-
-function main(argv) {
- const args = parseArgs(argv);
- if (args.help != null) usage();
- const { values, meta } = replacements(args);
-
- const root = path.join(__dirname, '..');
- const out = path.resolve(process.cwd(), String(args.out));
- if (fs.existsSync(out) && fs.readdirSync(out).length) usage(`--out ${out} exists and is not empty`);
- if (out === root || root.startsWith(`${out}${path.sep}`) || out.startsWith(`${root}${path.sep}`)) {
- usage('--out must be outside the skeleton, and must not contain it');
- }
-
- copyTree(root, out, values);
-
- process.stdout.write(`Generated ${meta.name} in ${out}\n\n`
- + ` service id ${meta.id}\n`
- + ` name ${meta.name}\n`
- + ` domain ${meta.domain}\n`
- + ` port ${meta.port}\n`
- + ` tagline ${meta.tagline}\n`
- + ` env prefix ${meta.env}\n\n`
- + `Next steps:\n`
- + ` 1. cd ${out}\n`
- + ` 2. npm install\n`
- + ` 3. npm test # every test/*.test.js on temp PGlite databases, with a mock Network\n`
- + ` 4. npm run dev # http://localhost:${meta.port}, PGlite in data/pglite\n`
- + ` 5. Register the service and its capabilities in OpenVibe.Contracts (CI: contracts-service: ${meta.id})\n`
- + ` and with OpenVibe.Services; write its tables in migrations/0001_initial.sql, its routes in\n`
- + ` server/http/api.js (capabilities in server/http/principal.js, budgets in server/http/caller-limits.js)\n`
- + ` and its pages in server/http/pages.js.\n`
- + ` 6. Fill in STATUS.json, README.md and CHANGELOG.md, then deploy with the unit and vhost in deploy/.\n`);
-}
-
-main(process.argv.slice(2));
deleted file mode 100644
deleted file mode 100644
@@ -1,139 +0,0 @@
@@ -1,139 +0,0 @@
-'use strict';
-/**
- * The skeleton's own test: generating a service from it must leave no placeholder behind, in file names or in file
- * contents, and the names the generator derives (the package, the config, the deploy files, the environment) must be
- * the ones a service needs. It runs scripts/new-service.js into a temp directory with fixed sample values, inspects
- * the result, and leaves nothing behind.
- *
- * The placeholder strings are built from parts: this file is copied into every generated service, and a literal
- * placeholder here would be rewritten on the way in.
- */
-const assert = require('assert');
-const fs = require('fs');
-const os = require('os');
-const path = require('path');
-const { execFileSync } = require('child_process');
-const { check, done } = require('./helpers/boot');
-
-const TOKEN_NAMES = ['ID', 'NAME', 'DOMAIN', 'PORT', 'TAGLINE', 'ENV'];
-const TOKEN_RE = new RegExp(`__(${TOKEN_NAMES.join('|')})__`);
-const token = (name) => `__${name}__`;
-
-const ROOT = path.join(__dirname, '..');
-const SAMPLE = { id: 'sample', name: 'OpenVibe.Sample', domain: 'openvibe.sample', port: '4999', tagline: 'A sample service.', env: 'SAMPLE' };
-
-const created = [];
-const tmpDir = (prefix) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); created.push(d); return d; };
-
-function runGenerator(out) {
- const args = [
- path.join(ROOT, 'scripts', 'new-service.js'),
- '--id', SAMPLE.id, '--name', SAMPLE.name, '--domain', SAMPLE.domain,
- '--port', SAMPLE.port, '--tagline', SAMPLE.tagline, '--env', SAMPLE.env, '--out', out,
- ];
- // stderr is captured, not shown: the generator's refusals are checked below, and a test file's output stays clean.
- return execFileSync(process.execPath, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
-}
-
-function walk(dir, out = []) {
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
- const p = path.join(dir, e.name);
- if (e.isDirectory()) walk(p, out); else out.push(p);
- }
- return out;
-}
-const rel = (p, dir) => path.relative(dir, p).split(path.sep).join('/');
-const text = (p) => fs.readFileSync(p, 'utf8');
-
-(async () => {
- // This test is about the skeleton's generator, and only the skeleton can run it: it asserts that generating a
- // service rewrites the placeholders, and a generated service has none left to rewrite (its package name is
- // already its own, its vhost is already named). A generated service says so and skips, rather than reporting a
- // failure it cannot fix — the convention openvibe-shared/test-runner reads (`<label>: skipped (<why>)`), so the
- // file is listed with ○ and never counted as passed.
- if (JSON.parse(text(path.join(ROOT, 'package.json'))).name !== `openvibe-${token('ID')}`) {
- process.stdout.write('skeleton generator: skipped (this is a generated service, not the skeleton it came from; run it in OpenVibe.ServiceSkeleton)\n');
- return done();
- }
- const base = tmpDir('ov-new-service-');
- const dir = path.join(base, 'openvibe-sample');
- const printed = runGenerator(dir);
-
- await check('the generator writes a service and prints the next steps', () => {
- assert.ok(fs.existsSync(dir), 'no output directory');
- assert.ok(printed.includes(`Generated ${SAMPLE.name} in ${dir}`), printed.slice(0, 200));
- assert.ok(printed.includes('npm install') && printed.includes('npm test'), 'the next steps name the first commands');
- assert.ok(!printed.includes(token('ID')) && !printed.includes(token('NAME')), 'the output still carries a placeholder');
- });
-
- await check('no placeholder is left in any file name or file content', () => {
- const files = walk(dir);
- assert.ok(files.length > 30, `only ${files.length} files were generated`);
- const badNames = files.map((f) => rel(f, dir)).filter((r) => TOKEN_RE.test(r));
- assert.deepStrictEqual(badNames, [], 'a file name still carries a placeholder');
- const badContents = files.filter((f) => TOKEN_RE.test(text(f))).map((f) => rel(f, dir));
- assert.deepStrictEqual(badContents, [], 'a file still carries a placeholder');
- });
-
- await check('the package, the config and the environment are the generated service\'s', () => {
- const pkg = JSON.parse(text(path.join(dir, 'package.json')));
- assert.strictEqual(pkg.name, `openvibe-${SAMPLE.id}`);
- assert.strictEqual(pkg.version, '0.1.0');
- // A generated service's own description is the name and its tagline; the skeleton's is the placeholders.
- assert.ok(pkg.description.startsWith(SAMPLE.name), pkg.description);
- assert.ok(pkg.scripts['test:pg'].includes(`${SAMPLE.env}_TEST_STORE=pg`), pkg.scripts['test:pg']);
-
- const config = text(path.join(dir, 'server', 'config.js'));
- assert.ok(config.includes(`service: '${SAMPLE.id}'`), 'config.service');
- assert.ok(config.includes(`env.PORT, ${SAMPLE.port}`), 'the default port');
- assert.ok(config.includes(`'https://${SAMPLE.domain}'`), 'the production base URL');
- assert.ok(config.includes(`'ov:${SAMPLE.id}:'`), 'the Valkey prefix');
- assert.ok(config.includes(`${SAMPLE.env}_LIMITS_MINUTE`), 'the limits env prefix');
- assert.ok(config.includes(`${SAMPLE.env}_PGLITE_DIR`), 'the PGlite env prefix');
-
- const env = text(path.join(dir, '.env.example'));
- for (const line of [`PORT=${SAMPLE.port}`, `BASE_URL=https://${SAMPLE.domain}`, `OV_OAUTH_CLIENT_ID=${SAMPLE.id}`, `VALKEY_PREFIX=ov:${SAMPLE.id}:`]) {
- assert.ok(env.includes(line), `.env.example is missing ${line}`);
- }
-
- const auth = text(path.join(dir, 'server', 'auth', 'sso.js'));
- assert.ok(auth.includes(`'${SAMPLE.id}_at'`) && auth.includes(`'${SAMPLE.id}_rt'`) && auth.includes(`'${SAMPLE.id}_oauth'`), 'the session cookies');
-
- const status = JSON.parse(text(path.join(dir, 'STATUS.json')));
- assert.strictEqual(status.domain, SAMPLE.domain);
- assert.ok(text(path.join(dir, 'README.md')).includes(SAMPLE.name), 'the README names the service');
- });
-
- await check('the deploy files carry the domain, the id and the port', () => {
- const nginx = text(path.join(dir, 'deploy', 'nginx', `${SAMPLE.domain}.conf`));
- assert.ok(nginx.includes(`server_name ${SAMPLE.domain} www.${SAMPLE.domain};`), 'the vhost name');
- assert.ok(nginx.includes(`proxy_pass http://127.0.0.1:${SAMPLE.port};`), 'the upstream port');
- assert.ok(nginx.includes(`zone=${SAMPLE.id}_api`), 'the api limit zone');
- assert.ok(!fs.existsSync(path.join(dir, 'deploy', 'nginx', `${token('DOMAIN')}.conf`)), 'the template name is still there');
-
- const unit = text(path.join(dir, 'deploy', 'systemd', `openvibe-${SAMPLE.id}.service`));
- assert.ok(unit.includes(`Description=${SAMPLE.name}`), 'the unit description');
- assert.ok(unit.includes(`Environment=PORT=${SAMPLE.port}`), 'the unit port');
- assert.ok(unit.includes(`/etc/openvibe/${SAMPLE.id}.env`), 'the environment file');
-
- assert.ok(fs.existsSync(path.join(dir, 'migrations', '0001_initial.sql')), 'the initial migration');
- assert.ok(fs.existsSync(path.join(dir, 'public', 'css', 'app.css')), 'the stylesheet');
- });
-
- await check('the copy skips node_modules and .git, and the skeleton\'s own tools travel with it', () => {
- assert.ok(!fs.existsSync(path.join(dir, 'node_modules')), 'node_modules was copied');
- assert.ok(!fs.existsSync(path.join(dir, '.git')), '.git was copied');
- assert.ok(fs.existsSync(path.join(dir, 'scripts', 'new-service.js')), 'the generator is in the generated tree');
- assert.ok(fs.existsSync(path.join(dir, 'test', 'run.js')), 'the test runner is in the generated tree');
- });
-
- await check('generating refuses to overwrite a directory that has something in it', () => {
- let failed = null;
- try { runGenerator(dir); } catch (err) { failed = err; }
- assert.ok(failed, 'a second run into the same directory must fail');
- assert.match(String((failed && failed.stderr) || ''), /not empty/);
- });
-
- for (const d of created) fs.rmSync(d, { recursive: true, force: true });
- done();
-})().catch((err) => { console.error(err); process.exit(1); });