zombie-vibe 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 +21 -0
- package/README.md +56 -0
- package/bin/zombie-vibe.mjs +21 -0
- package/examples/claude-or-codex-hooks.json +16 -0
- package/examples/config.json +5 -0
- package/examples/cursor-hooks.json +11 -0
- package/examples/windsurf-hooks.json +10 -0
- package/lib/installer.mjs +117 -0
- package/package.json +27 -0
- package/runtime/hook.mjs +118 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zombie Vibe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Zombie Vibe Hooks
|
|
2
|
+
|
|
3
|
+
Zombie Vibe installs deterministic prompt-submission hooks for Codex, Claude Code, Cursor, Windsurf, and Grok. The hook records when a prompt was submitted, the editor and session, the local timezone offset, and prompt length. It immediately discards the prompt text.
|
|
4
|
+
|
|
5
|
+
Events live in a private persistent queue at `~/.zombie-vibe/events.jsonl`. When the queue reaches 10 events, the oldest 10 are sent to the Zombie Vibe API. Failed uploads stay queued for a later prompt or manual flush.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Create an API key after signing in at `https://cloudcodesarena.xyz/#account`, then install globally for every supported editor:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx zombie-vibe install --api-key zp_live_YOUR_KEY
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install for one editor:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx zombie-vibe install --editor codex --api-key zp_live_YOUR_KEY
|
|
19
|
+
npx zombie-vibe install --editor claude --api-key zp_live_YOUR_KEY
|
|
20
|
+
npx zombie-vibe install --editor cursor --api-key zp_live_YOUR_KEY
|
|
21
|
+
npx zombie-vibe install --editor windsurf --api-key zp_live_YOUR_KEY
|
|
22
|
+
npx zombie-vibe install --editor grok --api-key zp_live_YOUR_KEY
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The installer copies a stable runtime to `~/.zombie-vibe/hook.mjs`, stores the key in a mode-`0600` config file, merges the hook into the editor's existing JSON, and creates a backup before every edit. Restart an open editor after installation.
|
|
26
|
+
|
|
27
|
+
## Hook locations
|
|
28
|
+
|
|
29
|
+
| Editor | Global configuration | Event |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| Codex CLI/Desktop | `~/.codex/hooks.json` | `UserPromptSubmit` |
|
|
32
|
+
| Claude Code/Desktop | `~/.claude/settings.json` | `UserPromptSubmit` |
|
|
33
|
+
| Cursor | `~/.cursor/hooks.json` | `beforeSubmitPrompt` |
|
|
34
|
+
| Windsurf | `~/.codeium/windsurf/hooks.json` | `pre_user_prompt` |
|
|
35
|
+
| Grok | `~/.grok/hooks/zombie-vibe.json` | `UserPromptSubmit` |
|
|
36
|
+
|
|
37
|
+
## Operations
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx zombie-vibe status
|
|
41
|
+
npx zombie-vibe flush
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Manual installation
|
|
45
|
+
|
|
46
|
+
Copy `runtime/hook.mjs` to `~/.zombie-vibe/hook.mjs`, copy `examples/config.json` to `~/.zombie-vibe/config.json`, replace the API-key placeholder, and restrict the config with `chmod 600 ~/.zombie-vibe/config.json`. Then merge the relevant file from `examples/` into the editor configuration shown above. Replace `--agent codex` with `--agent claude` or `--agent grok` when using the Claude-compatible example for those editors.
|
|
47
|
+
|
|
48
|
+
## Publish to npm
|
|
49
|
+
|
|
50
|
+
Update the version, confirm the package name and repository URL, authenticate with `npm login`, then run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run publish:npm
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The publishing script runs tests, previews the package tarball, verifies npm authentication, and publishes publicly.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { install, printHelp, showStatus } from "../lib/installer.mjs";
|
|
3
|
+
import { captureFromStdin, flushQueue } from "../runtime/hook.mjs";
|
|
4
|
+
|
|
5
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
if (command === "install") await install(args);
|
|
9
|
+
else if (command === "status") await showStatus();
|
|
10
|
+
else if (command === "capture") await captureFromStdin(args);
|
|
11
|
+
else if (command === "flush") await flushQueue({ force: true });
|
|
12
|
+
else if (["help", "--help", "-h"].includes(command)) printHelp();
|
|
13
|
+
else {
|
|
14
|
+
console.error(`Unknown command: ${command}`);
|
|
15
|
+
printHelp();
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error(`Zombie Vibe: ${error instanceof Error ? error.message : error}`);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { chmod, copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const home = homedir();
|
|
9
|
+
const runtimeDir = join(home, ".zombie-vibe");
|
|
10
|
+
const runtimePath = join(runtimeDir, "hook.mjs");
|
|
11
|
+
const configPath = join(runtimeDir, "config.json");
|
|
12
|
+
const supported = ["codex", "claude", "cursor", "windsurf", "grok"];
|
|
13
|
+
|
|
14
|
+
function option(args, name) {
|
|
15
|
+
const equals = args.find((arg) => arg.startsWith(`${name}=`));
|
|
16
|
+
if (equals) return equals.slice(name.length + 1);
|
|
17
|
+
const index = args.indexOf(name);
|
|
18
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shellQuote(value) {
|
|
22
|
+
return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readJson(path, fallback = {}) {
|
|
26
|
+
try { return JSON.parse(await readFile(path, "utf8")); } catch (error) {
|
|
27
|
+
if (error?.code === "ENOENT") return fallback;
|
|
28
|
+
throw new Error(`Cannot parse ${path}. Fix the JSON and run install again.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function writeJson(path, value, privateFile = false) {
|
|
33
|
+
await mkdir(dirname(path), { recursive: true });
|
|
34
|
+
if (existsSync(path)) await copyFile(path, `${path}.zombie-vibe.backup`);
|
|
35
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
36
|
+
await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: privateFile ? 0o600 : 0o644 });
|
|
37
|
+
await rename(temp, path);
|
|
38
|
+
if (privateFile) await chmod(path, 0o600);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function nestedHook(command) {
|
|
42
|
+
return { hooks: [{ type: "command", command, timeout: 5, async: true }] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addUnique(list, entry) {
|
|
46
|
+
const current = Array.isArray(list) ? list : [];
|
|
47
|
+
return current.some((item) => JSON.stringify(item).includes(".zombie-vibe")) ? current : [...current, entry];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function upsertHook(editor, document, command) {
|
|
51
|
+
const output = { ...document, hooks: { ...(document.hooks || {}) } };
|
|
52
|
+
if (editor === "cursor") {
|
|
53
|
+
output.version ||= 1;
|
|
54
|
+
output.hooks.beforeSubmitPrompt = addUnique(output.hooks.beforeSubmitPrompt, { command, timeout: 5 });
|
|
55
|
+
} else if (editor === "windsurf") {
|
|
56
|
+
output.hooks.pre_user_prompt = addUnique(output.hooks.pre_user_prompt, { command, show_output: false });
|
|
57
|
+
} else {
|
|
58
|
+
output.hooks.UserPromptSubmit = addUnique(output.hooks.UserPromptSubmit, nestedHook(command));
|
|
59
|
+
}
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hookFile(editor) {
|
|
64
|
+
return {
|
|
65
|
+
codex: join(home, ".codex", "hooks.json"),
|
|
66
|
+
claude: join(home, ".claude", "settings.json"),
|
|
67
|
+
cursor: join(home, ".cursor", "hooks.json"),
|
|
68
|
+
windsurf: join(home, ".codeium", "windsurf", "hooks.json"),
|
|
69
|
+
grok: join(home, ".grok", "hooks", "zombie-vibe.json"),
|
|
70
|
+
}[editor];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function install(args) {
|
|
74
|
+
const requested = (option(args, "--editor") || "all").toLowerCase();
|
|
75
|
+
const editors = requested === "all" ? supported : requested.split(",").map((value) => value.trim());
|
|
76
|
+
const unknown = editors.filter((editor) => !supported.includes(editor));
|
|
77
|
+
if (unknown.length) throw new Error(`Unsupported editor: ${unknown.join(", ")}. Use ${supported.join(", ")}, or all.`);
|
|
78
|
+
const apiKey = option(args, "--api-key") || process.env.ZOMBIE_VIBE_API_KEY;
|
|
79
|
+
const apiUrl = (option(args, "--api-url") || "https://api.codersarena.xyz/api/v1/events").replace(/\/$/, "");
|
|
80
|
+
|
|
81
|
+
await mkdir(runtimeDir, { recursive: true });
|
|
82
|
+
await copyFile(join(packageRoot, "runtime", "hook.mjs"), runtimePath);
|
|
83
|
+
const existing = await readJson(configPath, {});
|
|
84
|
+
await writeJson(configPath, { apiUrl, apiKey: apiKey || existing.apiKey || "", batchSize: 10, installedAt: new Date().toISOString() }, true);
|
|
85
|
+
|
|
86
|
+
for (const editor of editors) {
|
|
87
|
+
const path = hookFile(editor);
|
|
88
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(runtimePath)} --agent ${editor}`;
|
|
89
|
+
await writeJson(path, upsertHook(editor, await readJson(path, {}), command));
|
|
90
|
+
console.log(`✓ ${editor.padEnd(8)} ${path}`);
|
|
91
|
+
}
|
|
92
|
+
console.log(`✓ runtime ${runtimePath}`);
|
|
93
|
+
console.log(`✓ config ${configPath}`);
|
|
94
|
+
if (!apiKey) console.log("! No API key yet. Re-run with --api-key zp_live_… before expecting uploads.");
|
|
95
|
+
console.log("\nHooks are installed globally. Restart open editors so they reload their configuration.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function showStatus() {
|
|
99
|
+
const config = await readJson(configPath, {});
|
|
100
|
+
const queuePath = join(runtimeDir, "events.jsonl");
|
|
101
|
+
const queued = existsSync(queuePath) ? (await readFile(queuePath, "utf8")).split("\n").filter(Boolean).length : 0;
|
|
102
|
+
console.log(JSON.stringify({ installed: existsSync(runtimePath), configured: Boolean(config.apiKey), apiUrl: config.apiUrl, queued, hooks: Object.fromEntries(supported.map((editor) => [editor, existsSync(hookFile(editor))])) }, null, 2));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function printHelp() {
|
|
106
|
+
console.log(`Zombie Vibe prompt hooks
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
npx zombie-vibe install --api-key zp_live_…
|
|
110
|
+
npx zombie-vibe install --editor codex --api-key zp_live_…
|
|
111
|
+
npx zombie-vibe install --editor claude,cursor --api-key zp_live_…
|
|
112
|
+
npx zombie-vibe status
|
|
113
|
+
npx zombie-vibe flush
|
|
114
|
+
|
|
115
|
+
Editors: codex, claude, cursor, windsurf, grok, all (default)
|
|
116
|
+
Privacy: prompt text is read only to calculate its length, then discarded.`);
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zombie-vibe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install privacy-first prompt timing hooks for Codex, Claude Code, Cursor, Windsurf, and Grok.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zombie-vibe": "bin/zombie-vibe.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"runtime/",
|
|
13
|
+
"examples/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"pack:check": "npm pack --dry-run",
|
|
20
|
+
"publish:npm": "bash scripts/publish-npm.sh"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["codex", "claude-code", "cursor", "windsurf", "grok", "hooks", "developer-wellness"],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": { "node": ">=20" },
|
|
25
|
+
"repository": { "type": "git", "url": "git+https://github.com/codersarena/zombie-vibe-skills.git" },
|
|
26
|
+
"homepage": "https://cloudcodesarena.xyz/#integrations"
|
|
27
|
+
}
|
package/runtime/hook.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
const dataDir = join(homedir(), ".zombie-vibe");
|
|
8
|
+
const configPath = join(dataDir, "config.json");
|
|
9
|
+
const queuePath = join(dataDir, "events.jsonl");
|
|
10
|
+
const lockPath = join(dataDir, "events.lock");
|
|
11
|
+
|
|
12
|
+
function arg(name, args = process.argv.slice(2)) {
|
|
13
|
+
const index = args.indexOf(name);
|
|
14
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function promptFrom(payload) {
|
|
18
|
+
const candidate = payload?.prompt ?? payload?.user_prompt ?? payload?.message ?? payload?.tool_info?.user_prompt ?? payload?.input?.prompt ?? "";
|
|
19
|
+
return typeof candidate === "string" ? candidate : JSON.stringify(candidate || "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function eventFromPayload(payload, agent = "unknown", now = new Date()) {
|
|
23
|
+
const prompt = promptFrom(payload);
|
|
24
|
+
const session = payload?.session_id ?? payload?.sessionId ?? payload?.conversation_id ?? payload?.trajectory_id ?? `anonymous-${randomBytes(6).toString("hex")}`;
|
|
25
|
+
return {
|
|
26
|
+
id: randomBytes(12).toString("base64url"),
|
|
27
|
+
occurred_at: now.toISOString(),
|
|
28
|
+
agent: String(agent).slice(0, 40),
|
|
29
|
+
session_id: String(session).slice(0, 160),
|
|
30
|
+
prompt_length: [...prompt].length,
|
|
31
|
+
timezone_offset_minutes: now.getTimezoneOffset(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readJson(path, fallback) {
|
|
36
|
+
try { return JSON.parse(await readFile(path, "utf8")); } catch { return fallback; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function lock() {
|
|
40
|
+
await mkdir(dataDir, { recursive: true });
|
|
41
|
+
for (let attempt = 0; attempt < 80; attempt += 1) {
|
|
42
|
+
try { return await open(lockPath, "wx", 0o600); } catch (error) {
|
|
43
|
+
if (error?.code !== "EEXIST") throw error;
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error("queue is busy; next prompt will retry");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function withQueue(operation) {
|
|
51
|
+
const handle = await lock();
|
|
52
|
+
try {
|
|
53
|
+
let events = [];
|
|
54
|
+
try { events = (await readFile(queuePath, "utf8")).split("\n").filter(Boolean).map((line) => JSON.parse(line)); } catch (error) {
|
|
55
|
+
if (error?.code !== "ENOENT") throw error;
|
|
56
|
+
}
|
|
57
|
+
const next = await operation(events);
|
|
58
|
+
const temp = `${queuePath}.${process.pid}.tmp`;
|
|
59
|
+
await writeFile(temp, next.map((event) => JSON.stringify(event)).join("\n") + (next.length ? "\n" : ""), { mode: 0o600 });
|
|
60
|
+
await rename(temp, queuePath);
|
|
61
|
+
return next;
|
|
62
|
+
} finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
await unlink(lockPath).catch(() => {});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function sendBatch(config, events) {
|
|
69
|
+
if (!config.apiKey || !config.apiUrl) return false;
|
|
70
|
+
const response = await fetch(config.apiUrl, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json", "user-agent": "zombie-vibe-hook/0.1.0" },
|
|
73
|
+
body: JSON.stringify({ events }),
|
|
74
|
+
signal: AbortSignal.timeout(3000),
|
|
75
|
+
});
|
|
76
|
+
return response.ok;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function capture(payload, agent) {
|
|
80
|
+
const config = await readJson(configPath, { batchSize: 10 });
|
|
81
|
+
const batchSize = Math.max(1, Math.min(10, Number(config.batchSize || 10)));
|
|
82
|
+
await withQueue(async (events) => {
|
|
83
|
+
events.push(eventFromPayload(payload, agent));
|
|
84
|
+
if (events.length < batchSize) return events;
|
|
85
|
+
try {
|
|
86
|
+
const batch = events.slice(0, batchSize);
|
|
87
|
+
return await sendBatch(config, batch) ? events.slice(batchSize) : events;
|
|
88
|
+
} catch { return events; }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function flushQueue({ force = false } = {}) {
|
|
93
|
+
const config = await readJson(configPath, { batchSize: 10 });
|
|
94
|
+
const batchSize = Math.max(1, Math.min(10, Number(config.batchSize || 10)));
|
|
95
|
+
let sent = 0;
|
|
96
|
+
await withQueue(async (events) => {
|
|
97
|
+
if (!events.length || (!force && events.length < batchSize)) return events;
|
|
98
|
+
const count = force ? Math.min(batchSize, events.length) : batchSize;
|
|
99
|
+
try {
|
|
100
|
+
if (await sendBatch(config, events.slice(0, count))) { sent = count; return events.slice(count); }
|
|
101
|
+
} catch {}
|
|
102
|
+
return events;
|
|
103
|
+
});
|
|
104
|
+
return sent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function captureFromStdin(args = process.argv.slice(2)) {
|
|
108
|
+
let input = "";
|
|
109
|
+
process.stdin.setEncoding("utf8");
|
|
110
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
111
|
+
let payload = {};
|
|
112
|
+
try { payload = JSON.parse(input || "{}"); } catch {}
|
|
113
|
+
await capture(payload, arg("--agent", args) || "unknown");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
117
|
+
captureFromStdin().catch(() => { process.exitCode = 0; });
|
|
118
|
+
}
|