incanto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- package/templates/agent8-server.ts +138 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"frame": {
|
|
3
|
+
"width": 192,
|
|
4
|
+
"height": 192
|
|
5
|
+
},
|
|
6
|
+
"origin": {
|
|
7
|
+
"x": 0.5,
|
|
8
|
+
"y": 0.6
|
|
9
|
+
},
|
|
10
|
+
"body": {
|
|
11
|
+
"size": {
|
|
12
|
+
"width": 60,
|
|
13
|
+
"height": 60
|
|
14
|
+
},
|
|
15
|
+
"offset": {
|
|
16
|
+
"x": 66,
|
|
17
|
+
"y": 76
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"animations": {
|
|
21
|
+
"idle": {
|
|
22
|
+
"start": 0,
|
|
23
|
+
"end": 6,
|
|
24
|
+
"frameRate": 10,
|
|
25
|
+
"repeat": -1
|
|
26
|
+
},
|
|
27
|
+
"move": {
|
|
28
|
+
"start": 7,
|
|
29
|
+
"end": 12,
|
|
30
|
+
"frameRate": 10,
|
|
31
|
+
"repeat": -1
|
|
32
|
+
},
|
|
33
|
+
"attack": {
|
|
34
|
+
"start": 14,
|
|
35
|
+
"end": 19,
|
|
36
|
+
"frameRate": 10,
|
|
37
|
+
"repeat": 0
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"frame": {
|
|
3
|
+
"width": 192,
|
|
4
|
+
"height": 192
|
|
5
|
+
},
|
|
6
|
+
"direction": "right",
|
|
7
|
+
"origin": {
|
|
8
|
+
"x": 0.5,
|
|
9
|
+
"y": 0.61
|
|
10
|
+
},
|
|
11
|
+
"body": {
|
|
12
|
+
"size": {
|
|
13
|
+
"width": 60,
|
|
14
|
+
"height": 60
|
|
15
|
+
},
|
|
16
|
+
"offset": {
|
|
17
|
+
"x": 66,
|
|
18
|
+
"y": 76
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"animations": {
|
|
22
|
+
"idle": {
|
|
23
|
+
"start": 0,
|
|
24
|
+
"end": 5,
|
|
25
|
+
"frameRate": 10,
|
|
26
|
+
"repeat": -1
|
|
27
|
+
},
|
|
28
|
+
"move": {
|
|
29
|
+
"start": 6,
|
|
30
|
+
"end": 11,
|
|
31
|
+
"frameRate": 10,
|
|
32
|
+
"repeat": -1
|
|
33
|
+
},
|
|
34
|
+
"attack": {
|
|
35
|
+
"start": 12,
|
|
36
|
+
"end": 17,
|
|
37
|
+
"frameRate": 10,
|
|
38
|
+
"repeat": 0
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* incanto-assets — the built-in asset catalog (shipped IN the package, no
|
|
4
|
+
* network, owner-licensed). Find art, copy it into your project, and get the
|
|
5
|
+
* ready-to-paste scene JSON.
|
|
6
|
+
*
|
|
7
|
+
* npx incanto-assets list [--json] what ships (characters/tiles/items/effects)
|
|
8
|
+
* npx incanto-assets info <name> description + animation details
|
|
9
|
+
* npx incanto-assets copy <name> --out public/assets
|
|
10
|
+
* copy the file(s) + print the scene snippet
|
|
11
|
+
*
|
|
12
|
+
* External art still works the usual way (any URL in scene assets{}) — this
|
|
13
|
+
* is the zero-setup starting set.
|
|
14
|
+
*/
|
|
15
|
+
import { copyFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const ASSETS = join(PKG, 'assets');
|
|
21
|
+
|
|
22
|
+
function catalog() {
|
|
23
|
+
return JSON.parse(readFileSync(join(ASSETS, 'catalog.json'), 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
27
|
+
|
|
28
|
+
if (!command || command === '--help' || command === '-h') {
|
|
29
|
+
console.log(`Usage:
|
|
30
|
+
incanto-assets list [--json]
|
|
31
|
+
incanto-assets info <name>
|
|
32
|
+
incanto-assets copy <name> --out <dir>`);
|
|
33
|
+
process.exit(command ? 0 : 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (command === 'list') {
|
|
37
|
+
const entries = catalog();
|
|
38
|
+
if (rest.includes('--json')) {
|
|
39
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
40
|
+
} else {
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
const anim = e.animation ? ' [animated]' : '';
|
|
43
|
+
console.log(
|
|
44
|
+
`${e.name.padEnd(20)} ${e.kind.padEnd(10)}${anim.padEnd(11)} ${e.description.slice(0, 80)}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
console.log(`\n${entries.length} built-in asset(s) — incanto-assets info <name> for details`);
|
|
48
|
+
}
|
|
49
|
+
// NO process.exit() here: piped stdout flushes asynchronously on POSIX, so
|
|
50
|
+
// exit() truncates any output past one ~8 KB pipe buffer (the catalog
|
|
51
|
+
// crossed that line when the bark/ground textures joined it). Falling off
|
|
52
|
+
// the end of the script lets node drain stdout before exiting.
|
|
53
|
+
process.exitCode = 0;
|
|
54
|
+
} else {
|
|
55
|
+
const name = rest.find((a) => !a.startsWith('--'));
|
|
56
|
+
const entry = catalog().find((e) => e.name === name);
|
|
57
|
+
if (!entry) {
|
|
58
|
+
const names = catalog().map((e) => e.name);
|
|
59
|
+
console.error(`unknown asset '${name ?? ''}'. Available: [${names.join(', ')}]`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === 'info') {
|
|
64
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
65
|
+
if (entry.animation) {
|
|
66
|
+
const meta = JSON.parse(readFileSync(join(ASSETS, entry.animation), 'utf-8'));
|
|
67
|
+
console.log('\nanimations:', Object.keys(meta.animations ?? {}).join(', '));
|
|
68
|
+
}
|
|
69
|
+
process.exitCode = 0; // see the list note — exit() truncates piped stdout
|
|
70
|
+
} else if (command === 'copy') {
|
|
71
|
+
const outFlag = rest.indexOf('--out');
|
|
72
|
+
const outDir = resolve(outFlag !== -1 ? rest[outFlag + 1] : 'public/assets');
|
|
73
|
+
mkdirSync(outDir, { recursive: true });
|
|
74
|
+
const copied = [];
|
|
75
|
+
for (const file of [entry.file, entry.animation].filter(Boolean)) {
|
|
76
|
+
const target = join(outDir, basename(file));
|
|
77
|
+
copyFileSync(join(ASSETS, file), target);
|
|
78
|
+
copied.push(target);
|
|
79
|
+
}
|
|
80
|
+
for (const t of copied) console.log(`copied ${t}`);
|
|
81
|
+
|
|
82
|
+
// Ready-to-paste scene JSON (sheet assets get full animation wiring).
|
|
83
|
+
const url = `assets/${basename(entry.file)}`;
|
|
84
|
+
if (entry.animation) {
|
|
85
|
+
const { spriteFromLibraryMeta } = await import(
|
|
86
|
+
pathToFileURL(join(PKG, 'dist', '2d.js')).href
|
|
87
|
+
);
|
|
88
|
+
const meta = JSON.parse(readFileSync(join(ASSETS, entry.animation), 'utf-8'));
|
|
89
|
+
const { asset, props } = spriteFromLibraryMeta(meta, { url, assetKey: entry.name });
|
|
90
|
+
console.log(`\nscene assets entry:\n "${entry.name}": ${JSON.stringify(asset)}`);
|
|
91
|
+
console.log(
|
|
92
|
+
`\nnode:\n ${JSON.stringify({ name: entry.name, type: 'AnimatedSprite2D', props }, null, 2)}`,
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
console.log(
|
|
96
|
+
`\nscene assets entry:\n "${entry.name}": ${JSON.stringify({ type: 'texture', url })}`,
|
|
97
|
+
);
|
|
98
|
+
console.log(
|
|
99
|
+
`\nnode:\n ${JSON.stringify({ name: entry.name, type: 'Sprite2D', props: { texture: `$${entry.name}` } })}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
process.exitCode = 0;
|
|
103
|
+
} else {
|
|
104
|
+
console.error(`unknown command '${command}'`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* incanto-check — headless scene validation. Every hard load error the engine
|
|
4
|
+
* would throw in the browser, on the command line instead (the agent's
|
|
5
|
+
* after-every-edit check; production VMs have no browser).
|
|
6
|
+
*
|
|
7
|
+
* npx incanto-check → validates every *.scene.json under cwd
|
|
8
|
+
* npx incanto-check src/game.scene.json → specific files (or directories)
|
|
9
|
+
* npx incanto-check --json → machine-readable report
|
|
10
|
+
* npx incanto-check --strict-behaviors → unregistered script names fail too
|
|
11
|
+
*
|
|
12
|
+
* Unregistered behaviors are STUBBED by default: structure-only validation
|
|
13
|
+
* that needs no TypeScript. Sub-scene `instance` paths resolve relative to
|
|
14
|
+
* each scene file.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const args = { files: [], json: false, strict: false, help: false, invalid: false };
|
|
24
|
+
for (const a of argv) {
|
|
25
|
+
if (a === '--json') args.json = true;
|
|
26
|
+
else if (a === '--strict-behaviors') args.strict = true;
|
|
27
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
28
|
+
else if (a.startsWith('--')) {
|
|
29
|
+
console.error(`unknown flag: ${a}`);
|
|
30
|
+
args.invalid = true;
|
|
31
|
+
} else args.files.push(a);
|
|
32
|
+
}
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const args = parseArgs(process.argv.slice(2));
|
|
37
|
+
if (args.help || args.invalid) {
|
|
38
|
+
console.log(`Usage: npx incanto-check [files|dirs...] [--json] [--strict-behaviors]
|
|
39
|
+
|
|
40
|
+
Validates *.scene.json files headlessly with the installed engine.
|
|
41
|
+
With no arguments, scans the current directory recursively
|
|
42
|
+
(skipping node_modules and dist).`);
|
|
43
|
+
process.exit(args.invalid ? 1 : 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectScenes(entry) {
|
|
47
|
+
const stat = statSync(entry);
|
|
48
|
+
if (stat.isFile()) return entry.endsWith('.scene.json') ? [entry] : [];
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const item of readdirSync(entry, { withFileTypes: true })) {
|
|
51
|
+
if (item.name === 'node_modules' || item.name === 'dist' || item.name.startsWith('.')) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const full = join(entry, item.name);
|
|
55
|
+
if (lstatSync(full).isSymbolicLink()) continue; // no symlink-loop recursion
|
|
56
|
+
out.push(...collectScenes(full));
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const roots = args.files.length > 0 ? args.files : ['.'];
|
|
62
|
+
for (const r of roots) {
|
|
63
|
+
if (!existsSync(resolve(r))) {
|
|
64
|
+
console.error(`no such file or directory: ${r}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const files = roots.flatMap((r) => collectScenes(resolve(r)));
|
|
69
|
+
if (files.length === 0) {
|
|
70
|
+
console.error('no *.scene.json files found');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { validateScene } = await import(pathToFileURL(join(DIST, 'test.js')).href);
|
|
75
|
+
|
|
76
|
+
const results = files.map((file) => {
|
|
77
|
+
let json;
|
|
78
|
+
try {
|
|
79
|
+
json = JSON.parse(readFileSync(file, 'utf-8'));
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return { file, ok: false, code: 'BAD_JSON', message: String(e?.message ?? e) };
|
|
82
|
+
}
|
|
83
|
+
const res = validateScene(json, {
|
|
84
|
+
strictBehaviors: args.strict,
|
|
85
|
+
resolveScene: (p) => JSON.parse(readFileSync(resolve(dirname(file), p), 'utf-8')),
|
|
86
|
+
});
|
|
87
|
+
if (res.ok) return { file, ok: true };
|
|
88
|
+
return {
|
|
89
|
+
file,
|
|
90
|
+
ok: false,
|
|
91
|
+
code: res.error?.code,
|
|
92
|
+
message: res.error?.message,
|
|
93
|
+
details: res.error?.details,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const failed = results.filter((r) => !r.ok);
|
|
98
|
+
if (args.json) {
|
|
99
|
+
console.log(JSON.stringify({ ok: failed.length === 0, results }, null, 2));
|
|
100
|
+
} else {
|
|
101
|
+
for (const r of results) {
|
|
102
|
+
if (r.ok) console.log(` ok ${r.file}`);
|
|
103
|
+
else console.log(`FAIL ${r.file}\n [${r.code}] ${r.message}`);
|
|
104
|
+
}
|
|
105
|
+
console.log(`\n${results.length - failed.length}/${results.length} scene(s) valid`);
|
|
106
|
+
}
|
|
107
|
+
process.exit(failed.length === 0 ? 0 : 1);
|