@nodegx/mcp 0.2.5 → 0.2.7

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.
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ rem Starts this NodeGX app - see DEPLOY.md. Options: --port <n> (default: the first free port from 3000).
3
+ where node >nul 2>nul || (echo This app needs Node.js 22.13 or later: https://nodejs.org & exit /b 1)
4
+ node "%~dp0start.js" %*
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Starts this NodeGX app: its backend, which also serves the app, on one port.
4
+ *
5
+ * node start.js first free port from 3000 up
6
+ * node start.js --port 8090 exactly 8090, or stop and say it is taken
7
+ * PORT=8090 node start.js the same, for hosts that set PORT
8
+ * node start.js --data-dir /var/lib/my-app/data keep the data outside this folder (a server
9
+ * whose app folder is read-only; a mounted volume)
10
+ * node start.js --check --data-dir <dir> say what a start would do with the rules, or refuse
11
+ * as a start would; writes nothing and starts nothing (a server's
12
+ * provisioning asks this before it replaces a running app)
13
+ *
14
+ * Every other option goes to the backend as it is (`node backend/cli.js help` lists them).
15
+ *
16
+ * Written by a NodeGX full-stack deploy (P26 DEP-002), and rewritten by the next one: change the
17
+ * project and deploy again rather than editing this file.
18
+ *
19
+ * What it does, in order:
20
+ *
21
+ * 1. Checks this Node can keep the data (`node:sqlite`, built in from Node 22.13), and says so in
22
+ * a sentence when it cannot — never a stack trace about a missing built-in module.
23
+ * 2. Picks the port by `app/serve.js`'s rule: the one asked for or a refusal naming it, else the
24
+ * first free from 3000, where free means 127.0.0.1 AND ::1 can both be bound, so
25
+ * `localhost:<port>` reaches this app and not another program.
26
+ * 3. Installs the cloud functions this deploy carries (`backend/workflows/`) into `data/`,
27
+ * replacing the ones the previous deploy installed. The deploy is their source, as a push from
28
+ * the editor is; a function deleted from the project is gone after the next deploy.
29
+ * 4. Installs the project's access rules (`backend/nodegx.security.json`) when this deploy brings
30
+ * rules other than the ones the last deploy installed: on the first start, and after a deploy
31
+ * that changed them, the backend's `data/security.json` is moved aside
32
+ * (`security.json.before-<time>`) and the backend installs the project's. A restart, or a deploy
33
+ * with the same rules, leaves `data/security.json` as it is (Richard, 2026-10-23: a reship
34
+ * brings the project's rules; an edit made on the server lasts until the project's change).
35
+ * 4b. Installs the project's workflows and triggers (`backend/nodegx.automations.json`, P127
36
+ * BKO-001) by the same rule: on a first start, and after a deploy that changed them, the
37
+ * backend's `data/triggers.json` and `data/workflow-defs/` are moved aside (`.before-<time>`) and
38
+ * the backend loads the project's. Webhook secrets stay in `data/secrets.json`, so a sender's
39
+ * secret survives a reship. A restart, or a deploy with the same ones, leaves them as they are.
40
+ * 5. Refuses rules that are switched off (`"devOpen": true`), which is how a development backend's
41
+ * data arrives — and the backend's own interlock only refuses that on a public bind, while this
42
+ * app is usually behind a proxy on 127.0.0.1. Refused before anything in the data is changed.
43
+ * 6. Runs the backend in this process with the app beside its API (`--serve-app`) and the access
44
+ * rules enforced from the first record (`--enforce-rules`).
45
+ *
46
+ * `data/` (or the `--data-dir` given) is the app's: its database, uploads, the admin credential. A
47
+ * deploy never writes there; this script writes only the functions and the rules described above.
48
+ */
49
+ 'use strict';
50
+
51
+ const crypto = require('crypto');
52
+ const fs = require('fs');
53
+ const net = require('net');
54
+ const path = require('path');
55
+
56
+ const HERE = __dirname;
57
+ const APP = path.join(HERE, 'app');
58
+ const BACKEND = path.join(HERE, 'backend');
59
+ const DEFAULT_DATA = path.join(HERE, 'data');
60
+ const POLICY_FILE = 'nodegx.security.json';
61
+ /** The backend's own rules file, in the data folder. */
62
+ const BACKEND_RULES = 'security.json';
63
+ /** Which rules the last deploy installed (their checksum), in the data folder. */
64
+ const RULES_MARKER = 'deployed-rules.json';
65
+ const WORKFLOW_SUFFIX = '.workflow.json';
66
+ /** The project's workflows and triggers (P127 BKO-001). */
67
+ const AUTOMATIONS_FILE = 'nodegx.automations.json';
68
+ /** The backend's own copies of them, in the data folder. */
69
+ const BACKEND_TRIGGERS = 'triggers.json';
70
+ const BACKEND_WORKFLOWS = 'workflow-defs';
71
+ /** Which automations the last deploy installed (their checksum), in the data folder. */
72
+ const AUTOMATIONS_MARKER = 'deployed-automations.json';
73
+
74
+ /** The Node this app needs, and the one property that decides it. */
75
+ const NODE_NEEDED = '22.13';
76
+
77
+ function fail(message) {
78
+ process.stderr.write(`${message}\n`);
79
+ process.exit(1);
80
+ }
81
+
82
+ /**
83
+ * 🔴 The property, not a version table: Node 22.5–22.12 has `node:sqlite` behind a flag, 20 has
84
+ * none, and a version comparison would have to know both. Loading it answers for every one.
85
+ */
86
+ function checkNode() {
87
+ try {
88
+ require('node:sqlite');
89
+ } catch {
90
+ fail(
91
+ `This app needs Node.js ${NODE_NEEDED} or later. It keeps its data with node:sqlite, which is built into ` +
92
+ `Node from ${NODE_NEEDED}, and this computer has Node ${process.version}.\n` +
93
+ 'Install a current Node from https://nodejs.org and run this again.'
94
+ );
95
+ }
96
+ }
97
+
98
+ /** The options this script reads itself; everything else is the backend's. */
99
+ const OWN_OPTIONS = ['--port', '-p', '--data-dir'];
100
+
101
+ /** `--port N`, `--port=N`, `-p N` and `--data-dir` (either form) are this script's. */
102
+ function withoutOwnArgs(argv) {
103
+ const rest = [];
104
+ for (let i = 0; i < argv.length; i++) {
105
+ if (OWN_OPTIONS.includes(argv[i])) {
106
+ i++;
107
+ continue;
108
+ }
109
+ if (OWN_OPTIONS.some((option) => argv[i].startsWith(`${option}=`))) continue;
110
+ rest.push(argv[i]);
111
+ }
112
+ return rest;
113
+ }
114
+
115
+ /**
116
+ * `--data-dir <dir>` or `--data-dir=<dir>`, resolved from where the command was run; `data/` beside
117
+ * this file when it is not given. The functions are installed THERE and the backend keeps its
118
+ * records there, so a folder whose data lives elsewhere never grows a second `data/`.
119
+ */
120
+ function readDataDir(argv) {
121
+ for (let i = 0; i < argv.length; i++) {
122
+ const value = argv[i] === '--data-dir' ? argv[i + 1] : argv[i].startsWith('--data-dir=') ? argv[i].slice(11) : null;
123
+ if (value === null) continue;
124
+ if (!value) throw new Error('--data-dir wants a folder, and got nothing.');
125
+ return path.resolve(value);
126
+ }
127
+ return DEFAULT_DATA;
128
+ }
129
+
130
+ // ── The port: `serve.js`'s rule, copied ─────────────────────────────────────────────────────────
131
+ // ⚠️ A copy of `listenOnFreePort` in the deploy runtime's `serve.js` (noodl-viewer-react
132
+ // `static/deploy/`), not a require of `app/serve.js`. This file ships with the BACKEND, and an app
133
+ // deployed by an older runtime has no `serve.js` with that function in it; a launcher that needs the
134
+ // app's runtime to be new enough fails for a reason that has nothing to do with the backend. Same
135
+ // rule, same sentences. Change one, change both.
136
+
137
+ const DEFAULT_PORT = 3000;
138
+ const PORTS_TO_TRY = 50;
139
+
140
+ /** `--port N`, `--port=N`, `-p N`, or `PORT`; null when nothing was asked. A flag wins. */
141
+ function readPortArg(argv, env) {
142
+ for (let i = 0; i < argv.length; i++) {
143
+ const arg = argv[i];
144
+ if (arg === '--port' || arg === '-p') return parsePort(argv[i + 1], arg);
145
+ if (arg.startsWith('--port=')) return parsePort(arg.slice('--port='.length), '--port');
146
+ }
147
+ if (env.PORT !== undefined && env.PORT !== '') return parsePort(env.PORT, 'PORT');
148
+ return null;
149
+ }
150
+
151
+ function parsePort(value, from) {
152
+ const port = Number(value);
153
+ if (value === undefined || !/^\d+$/.test(String(value)) || port > 65535) {
154
+ throw new Error(`${from} wants a port number between 1 and 65535, and got "${value ?? ''}".`);
155
+ }
156
+ return { port, from };
157
+ }
158
+
159
+ /** Whether `host:port` can be bound right now. A host this machine does not have counts as free. */
160
+ function canBind(port, host) {
161
+ return new Promise((resolve) => {
162
+ const probe = net.createServer();
163
+ probe.once('error', (error) => resolve(error.code === 'EADDRNOTAVAIL' || error.code === 'EAFNOSUPPORT'));
164
+ probe.once('listening', () => probe.close(() => resolve(true)));
165
+ probe.listen({ port, host, exclusive: true });
166
+ });
167
+ }
168
+
169
+ async function loopbackFree(port) {
170
+ return (await canBind(port, '127.0.0.1')) && (await canBind(port, '::1'));
171
+ }
172
+
173
+ function takenMessage(asked) {
174
+ const how = asked.from === 'PORT' ? `PORT=${asked.port}` : `${asked.from} ${asked.port}`;
175
+ return (
176
+ `Port ${asked.port} is already in use by another program (you asked for it with ${how}).\n` +
177
+ `Leave the port out to use the first free one from ${DEFAULT_PORT}, or choose another, e.g. --port ${
178
+ asked.port + 1
179
+ }.`
180
+ );
181
+ }
182
+
183
+ /**
184
+ * The port the backend is to listen on. Asked for one: that one, or an error naming it — never a
185
+ * different one, because a host that set `PORT` sends its traffic to exactly that port. Asked for
186
+ * nothing: the first free from 3000, else 0 (the system picks; the backend prints what it got).
187
+ */
188
+ async function choosePort(argv, env) {
189
+ const asked = readPortArg(argv, env);
190
+ if (asked) {
191
+ if (asked.port !== 0 && !(await loopbackFree(asked.port))) throw new Error(takenMessage(asked));
192
+ return asked.port;
193
+ }
194
+ for (let port = DEFAULT_PORT; port < DEFAULT_PORT + PORTS_TO_TRY && port <= 65535; port++) {
195
+ if (await loopbackFree(port)) return port;
196
+ }
197
+ return 0;
198
+ }
199
+
200
+ /**
201
+ * Replace the installed cloud functions with the ones this deploy carries. Returns the files: one
202
+ * per project (`<project>.workflow.json`), each holding all of its functions.
203
+ */
204
+ function installFunctions(dataDir) {
205
+ const from = path.join(BACKEND, 'workflows');
206
+ const to = path.join(dataDir, 'workflows');
207
+ const carried = fs.existsSync(from) ? fs.readdirSync(from).filter((f) => f.endsWith(WORKFLOW_SUFFIX)) : [];
208
+ if (carried.length === 0 && !fs.existsSync(to)) return carried;
209
+ fs.mkdirSync(to, { recursive: true });
210
+ for (const old of fs.readdirSync(to).filter((f) => f.endsWith(WORKFLOW_SUFFIX))) {
211
+ fs.unlinkSync(path.join(to, old));
212
+ }
213
+ for (const file of carried) fs.copyFileSync(path.join(from, file), path.join(to, file));
214
+ return carried;
215
+ }
216
+
217
+ /**
218
+ * What to do with the access rules, decided before anything is written:
219
+ *
220
+ * none the deploy ships no rules; the backend keeps (or mints) its own
221
+ * unchanged the rules this deploy ships are the ones the last deploy installed
222
+ * install the backend has no rules yet (a first start): it installs the project's
223
+ * replace the backend has rules, and this deploy ships different ones (or no deploy recorded
224
+ * which it installed — a box from before this file existed)
225
+ *
226
+ * A rules file that is not JSON stops here, before the backend's rules are moved; the backend
227
+ * validates the rest when it installs them, in its own words.
228
+ */
229
+ function planRules(dataDir) {
230
+ const shipped = path.join(BACKEND, POLICY_FILE);
231
+ const current = path.join(dataDir, BACKEND_RULES);
232
+ if (!fs.existsSync(shipped)) return { action: 'none', current };
233
+ const bytes = fs.readFileSync(shipped);
234
+ let config;
235
+ try {
236
+ config = JSON.parse(bytes.toString('utf8'));
237
+ } catch (error) {
238
+ fail(
239
+ `This deploy's access rules (backend/${POLICY_FILE}) are not valid JSON: ${error.message}. Deploy the project again.`
240
+ );
241
+ }
242
+ const sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
243
+ let installed = null;
244
+ try {
245
+ installed = JSON.parse(fs.readFileSync(path.join(dataDir, RULES_MARKER), 'utf8')).sha256;
246
+ } catch {
247
+ installed = null;
248
+ }
249
+ let action = 'replace';
250
+ if (!fs.existsSync(current)) action = 'install';
251
+ else if (installed === sha256) action = 'unchanged';
252
+ return { action, current, shipped, config, sha256 };
253
+ }
254
+
255
+ /**
256
+ * What to do with the project's workflows and triggers: the same four answers as `planRules`. The
257
+ * backend has some when it has a `triggers.json` or any workflow definition.
258
+ */
259
+ function planAutomations(dataDir) {
260
+ const shipped = path.join(BACKEND, AUTOMATIONS_FILE);
261
+ const triggers = path.join(dataDir, BACKEND_TRIGGERS);
262
+ const workflows = path.join(dataDir, BACKEND_WORKFLOWS);
263
+ if (!fs.existsSync(shipped)) return { action: 'none', triggers, workflows };
264
+ const bytes = fs.readFileSync(shipped);
265
+ try {
266
+ JSON.parse(bytes.toString('utf8'));
267
+ } catch (error) {
268
+ fail(
269
+ `This deploy's workflows and triggers (backend/${AUTOMATIONS_FILE}) are not valid JSON: ${error.message}. Deploy the project again.`
270
+ );
271
+ }
272
+ const sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
273
+ let installed = null;
274
+ try {
275
+ installed = JSON.parse(fs.readFileSync(path.join(dataDir, AUTOMATIONS_MARKER), 'utf8')).sha256;
276
+ } catch {
277
+ installed = null;
278
+ }
279
+ const hasSome = fs.existsSync(triggers) || (fs.existsSync(workflows) && fs.readdirSync(workflows).length > 0);
280
+ let action = 'replace';
281
+ if (!hasSome) action = 'install';
282
+ else if (installed === sha256) action = 'unchanged';
283
+ return { action, triggers, workflows, sha256 };
284
+ }
285
+
286
+ /** Carry out the plan. Returns where the previous ones were moved, when they were. */
287
+ function applyAutomations(plan, dataDir) {
288
+ if (plan.action !== 'install' && plan.action !== 'replace') return [];
289
+ const moved = [];
290
+ if (plan.action === 'replace') {
291
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
292
+ for (const from of [plan.triggers, plan.workflows]) {
293
+ if (!fs.existsSync(from)) continue;
294
+ const to = `${from}.before-${stamp}`;
295
+ fs.renameSync(from, to);
296
+ moved.push(to);
297
+ }
298
+ }
299
+ fs.writeFileSync(
300
+ path.join(dataDir, AUTOMATIONS_MARKER),
301
+ JSON.stringify({ sha256: plan.sha256, installedAt: new Date().toISOString() }, null, 2) + '\n'
302
+ );
303
+ return moved;
304
+ }
305
+
306
+ function describeAutomations(plan, moved) {
307
+ switch (plan.action) {
308
+ case 'none':
309
+ return "[start] workflows and triggers: this deploy carries none; the backend's own run\n";
310
+ case 'unchanged':
311
+ return '[start] workflows and triggers: unchanged (this deploy\'s are the ones already installed)\n';
312
+ case 'install':
313
+ return `[start] workflows and triggers: installing this deploy's backend/${AUTOMATIONS_FILE}\n`;
314
+ default:
315
+ return (
316
+ `[start] workflows and triggers: replaced with this deploy's backend/${AUTOMATIONS_FILE}` +
317
+ (moved.length ? `; the previous ones are kept as ${moved.join(' and ')}` : '') +
318
+ '\n'
319
+ );
320
+ }
321
+ }
322
+
323
+ /** Carry out the plan. Returns where the previous rules were moved, when they were. */
324
+ function applyRules(plan, dataDir) {
325
+ if (plan.action !== 'install' && plan.action !== 'replace') return null;
326
+ let movedTo = null;
327
+ if (plan.action === 'replace') {
328
+ movedTo = `${plan.current}.before-${new Date().toISOString().replace(/[:.]/g, '-')}`;
329
+ fs.renameSync(plan.current, movedTo);
330
+ }
331
+ fs.writeFileSync(
332
+ path.join(dataDir, RULES_MARKER),
333
+ JSON.stringify({ sha256: plan.sha256, installedAt: new Date().toISOString() }, null, 2) + '\n'
334
+ );
335
+ return movedTo;
336
+ }
337
+
338
+ /**
339
+ * 🔴 A data folder with `"devOpen": true` is refused. Dev-open turns every access rule off, and the
340
+ * backend's interlock (BAK-003) refuses it only when the bind is not loopback — but a deployed
341
+ * folder normally binds 127.0.0.1 with a proxy in front, so the interlock never fires and the app
342
+ * is public with no rules at all (P26-VMOPEN: OWN-016's VM layout ran exactly that way). A first
343
+ * start mints `devOpen: false` (`--enforce-rules`), so only data brought from elsewhere trips this.
344
+ * The rules checked are the ones that WILL be enforced: the project's when this start installs
345
+ * them, else the backend's. An unreadable file is left to the backend, which refuses it.
346
+ */
347
+ function refuseDevOpen(plan) {
348
+ const installing = plan.action === 'install' || plan.action === 'replace';
349
+ if (installing) {
350
+ if (plan.config && plan.config.devOpen === true) {
351
+ fail(
352
+ `This deploy's access rules are switched off: backend/${POLICY_FILE} has "devOpen": true. With them off, ` +
353
+ 'anyone who can reach the app reads and changes every record, every account included, so a deployed app ' +
354
+ 'does not start that way.\n' +
355
+ `Set "devOpen": false in the project's ${POLICY_FILE} and deploy again.`
356
+ );
357
+ }
358
+ return;
359
+ }
360
+ let config;
361
+ try {
362
+ config = JSON.parse(fs.readFileSync(plan.current, 'utf8'));
363
+ } catch {
364
+ return;
365
+ }
366
+ if (!config || config.devOpen !== true) return;
367
+ fail(
368
+ `This app's access rules are switched off: ${plan.current} has "devOpen": true. With them off, anyone who can ` +
369
+ 'reach the app reads and changes every record, every account included, so a deployed app does not start ' +
370
+ 'that way.\n' +
371
+ 'That data folder most likely came from a development backend. Set "devOpen": false in that file and ' +
372
+ "start this again (or ship the project's own rules); the rules written in the file then apply."
373
+ );
374
+ }
375
+
376
+ /** The `[start]` line for the rules. */
377
+ function describeRules(plan, movedTo) {
378
+ switch (plan.action) {
379
+ case 'none':
380
+ return "[start] access rules: this deploy carries none; the backend's own apply\n";
381
+ case 'unchanged':
382
+ return `[start] access rules: data/${BACKEND_RULES}, unchanged (this deploy's are the ones already installed)\n`;
383
+ case 'install':
384
+ return `[start] access rules: installing this deploy's backend/${POLICY_FILE}\n`;
385
+ default:
386
+ return (
387
+ `[start] access rules: replaced with this deploy's backend/${POLICY_FILE}; the previous ones are kept ` +
388
+ `as ${movedTo}\n`
389
+ );
390
+ }
391
+ }
392
+
393
+ /** The project's name, as the deploy wrote it into `package.json`; `/health` reports it. */
394
+ function appName() {
395
+ try {
396
+ const pkg = JSON.parse(fs.readFileSync(path.join(HERE, 'package.json'), 'utf8'));
397
+ return pkg.nodegx && typeof pkg.nodegx.name === 'string' && pkg.nodegx.name ? pkg.nodegx.name : null;
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+
403
+ async function main() {
404
+ checkNode();
405
+ if (!fs.existsSync(path.join(APP, 'index.html'))) {
406
+ fail(`This folder has no app/index.html, so there is no app to serve. Deploy the project again.`);
407
+ }
408
+ if (!fs.existsSync(path.join(BACKEND, 'cli.js'))) {
409
+ fail(`This folder has no backend/cli.js, so there is no backend to start. Deploy the project again.`);
410
+ }
411
+
412
+ const argv = process.argv.slice(2);
413
+ let port;
414
+ let dataDir;
415
+ try {
416
+ dataDir = readDataDir(argv);
417
+ if (argv.includes('--check')) {
418
+ // The port is not asked about: the app this would replace is usually the one holding it.
419
+ const plan = planRules(dataDir);
420
+ refuseDevOpen(plan);
421
+ process.stdout.write(describeRules(plan, `${plan.current}.before-<time>`).replace('[start]', '[check]'));
422
+ const automationsPlan = planAutomations(dataDir);
423
+ process.stdout.write(
424
+ describeAutomations(automationsPlan, [`${BACKEND_TRIGGERS}.before-<time>`, `${BACKEND_WORKFLOWS}.before-<time>`]).replace('[start]', '[check]')
425
+ );
426
+ process.exit(0);
427
+ }
428
+ port = await choosePort(argv, process.env);
429
+ } catch (error) {
430
+ fail(error && error.message ? error.message : String(error));
431
+ }
432
+
433
+ fs.mkdirSync(dataDir, { recursive: true });
434
+ const rules = planRules(dataDir);
435
+ refuseDevOpen(rules);
436
+ const automations = planAutomations(dataDir);
437
+ const movedTo = applyRules(rules, dataDir);
438
+ process.stdout.write(describeRules(rules, movedTo));
439
+ process.stdout.write(describeAutomations(automations, applyAutomations(automations, dataDir)));
440
+ const functions = installFunctions(dataDir);
441
+ process.stdout.write(
442
+ functions.length
443
+ ? `[start] cloud functions: installed ${functions.map((f) => `backend/workflows/${f}`).join(', ')}\n`
444
+ : '[start] cloud functions: none in this deploy\n'
445
+ );
446
+
447
+ const args = ['serve', '--data-dir', dataDir, '--port', String(port), '--serve-app', APP, '--enforce-rules'];
448
+ // The backend installs the project's rules only when it has none, and says so loudly when it
449
+ // does not — so it is told about them only on a start that installs them.
450
+ // The same for the workflows and triggers, which it loads only into a backend that has none.
451
+ const installs = (plan) => plan.action === 'install' || plan.action === 'replace';
452
+ if (installs(rules) || installs(automations)) args.push('--project-dir', BACKEND);
453
+ const name = appName();
454
+ if (name) args.push('--backend-name', name);
455
+ args.push(...withoutOwnArgs(argv));
456
+
457
+ const backend = require(path.join(BACKEND, 'cli.js'));
458
+ await backend.main(args);
459
+ }
460
+
461
+ main().catch((error) => {
462
+ // The backend's own words for a refusal (a taken port, a bad policy file) — the same line
463
+ // `node backend/cli.js serve` prints.
464
+ process.stderr.write(`[nodegx-backend] FATAL: ${error && error.message ? error.message : error}\n`);
465
+ process.exit(1);
466
+ });
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Starts this NodeGX app — see DEPLOY.md. Options: --port <n> (default: the first free port from 3000).
3
+ command -v node >/dev/null 2>&1 || { echo "This app needs Node.js 22.13 or later: https://nodejs.org" >&2; exit 1; }
4
+ exec node "$(dirname "$0")/start.js" "$@"