@grainulation/grainulation 1.0.1 → 1.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/README.md +1 -1
- package/bin/grainulation.js +13 -14
- package/lib/doctor.js +67 -104
- package/lib/ecosystem.js +57 -65
- package/lib/pm.js +44 -69
- package/lib/router.js +179 -215
- package/lib/server.mjs +23 -34
- package/lib/setup.js +42 -51
- package/package.json +1 -2
- package/public/index.html +1 -1
package/lib/pm.js
CHANGED
|
@@ -5,25 +5,20 @@
|
|
|
5
5
|
* This module spawns/kills them and probes ports for health.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { spawn,
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
mkdirSync,
|
|
14
|
-
} = require("node:fs");
|
|
15
|
-
const { join } = require("node:path");
|
|
16
|
-
const http = require("node:http");
|
|
17
|
-
const { getAll, getByName, getInstallable } = require("./ecosystem");
|
|
8
|
+
const { spawn, execFileSync } = require('node:child_process');
|
|
9
|
+
const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
|
|
10
|
+
const { join } = require('node:path');
|
|
11
|
+
const http = require('node:http');
|
|
12
|
+
const { getAll, getByName, getInstallable } = require('./ecosystem');
|
|
18
13
|
|
|
19
14
|
// PID tracking directory
|
|
20
|
-
const PM_DIR = join(require(
|
|
21
|
-
const PID_DIR = join(PM_DIR,
|
|
22
|
-
const CONFIG_FILE = join(PM_DIR,
|
|
15
|
+
const PM_DIR = join(require('node:os').homedir(), '.grainulation');
|
|
16
|
+
const PID_DIR = join(PM_DIR, 'pids');
|
|
17
|
+
const CONFIG_FILE = join(PM_DIR, 'config.json');
|
|
23
18
|
|
|
24
19
|
function loadConfig() {
|
|
25
20
|
try {
|
|
26
|
-
return JSON.parse(readFileSync(CONFIG_FILE,
|
|
21
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
27
22
|
} catch {
|
|
28
23
|
return {};
|
|
29
24
|
}
|
|
@@ -42,8 +37,8 @@ function readPid(toolName) {
|
|
|
42
37
|
const f = pidFile(toolName);
|
|
43
38
|
if (!existsSync(f)) return null;
|
|
44
39
|
try {
|
|
45
|
-
const pid = parseInt(readFileSync(f,
|
|
46
|
-
if (isNaN(pid)) return null;
|
|
40
|
+
const pid = parseInt(readFileSync(f, 'utf8').trim(), 10);
|
|
41
|
+
if (Number.isNaN(pid)) return null;
|
|
47
42
|
// Check if process is alive
|
|
48
43
|
process.kill(pid, 0);
|
|
49
44
|
return pid;
|
|
@@ -60,7 +55,7 @@ function writePid(toolName, pid) {
|
|
|
60
55
|
function removePid(toolName) {
|
|
61
56
|
const f = pidFile(toolName);
|
|
62
57
|
try {
|
|
63
|
-
require(
|
|
58
|
+
require('node:fs').unlinkSync(f);
|
|
64
59
|
} catch {}
|
|
65
60
|
}
|
|
66
61
|
|
|
@@ -71,17 +66,14 @@ function removePid(toolName) {
|
|
|
71
66
|
function probe(port, timeoutMs = 2000) {
|
|
72
67
|
return new Promise((resolve) => {
|
|
73
68
|
const start = Date.now();
|
|
74
|
-
const req = http.get(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
req.on("error", () => resolve({ alive: false }));
|
|
84
|
-
req.on("timeout", () => {
|
|
69
|
+
const req = http.get({ hostname: '127.0.0.1', port, path: '/health', timeout: timeoutMs }, (res) => {
|
|
70
|
+
const latencyMs = Date.now() - start;
|
|
71
|
+
// Consume body
|
|
72
|
+
res.resume();
|
|
73
|
+
resolve({ alive: true, statusCode: res.statusCode, latencyMs });
|
|
74
|
+
});
|
|
75
|
+
req.on('error', () => resolve({ alive: false }));
|
|
76
|
+
req.on('timeout', () => {
|
|
85
77
|
req.destroy();
|
|
86
78
|
resolve({ alive: false });
|
|
87
79
|
});
|
|
@@ -92,29 +84,24 @@ function probe(port, timeoutMs = 2000) {
|
|
|
92
84
|
* Find the bin path for a tool — prefers source checkout, falls back to npx.
|
|
93
85
|
*/
|
|
94
86
|
function findBin(tool) {
|
|
95
|
-
const shortName = tool.package.replace(/^@[^/]+\//,
|
|
96
|
-
const candidates = [
|
|
97
|
-
join(__dirname, "..", "..", shortName),
|
|
98
|
-
join(process.cwd(), "..", shortName),
|
|
99
|
-
];
|
|
87
|
+
const shortName = tool.package.replace(/^@[^/]+\//, '');
|
|
88
|
+
const candidates = [join(__dirname, '..', '..', shortName), join(process.cwd(), '..', shortName)];
|
|
100
89
|
for (const dir of candidates) {
|
|
101
90
|
try {
|
|
102
|
-
const pkgPath = join(dir,
|
|
91
|
+
const pkgPath = join(dir, 'package.json');
|
|
103
92
|
if (!existsSync(pkgPath)) continue;
|
|
104
|
-
const pkg = JSON.parse(readFileSync(pkgPath,
|
|
93
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
105
94
|
if (pkg.name !== tool.package) continue;
|
|
106
95
|
if (pkg.bin) {
|
|
107
|
-
const binFile =
|
|
108
|
-
typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
|
|
96
|
+
const binFile = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin)[0];
|
|
109
97
|
if (binFile) {
|
|
110
|
-
const binPath = require(
|
|
111
|
-
if (existsSync(binPath))
|
|
112
|
-
return { cmd: process.execPath, args: [binPath] };
|
|
98
|
+
const binPath = require('node:path').resolve(dir, binFile);
|
|
99
|
+
if (existsSync(binPath)) return { cmd: process.execPath, args: [binPath] };
|
|
113
100
|
}
|
|
114
101
|
}
|
|
115
102
|
} catch {}
|
|
116
103
|
}
|
|
117
|
-
return { cmd:
|
|
104
|
+
return { cmd: 'npx', args: [tool.package] };
|
|
118
105
|
}
|
|
119
106
|
|
|
120
107
|
/**
|
|
@@ -123,8 +110,7 @@ function findBin(tool) {
|
|
|
123
110
|
function startTool(toolName, extraArgs = []) {
|
|
124
111
|
const tool = getByName(toolName);
|
|
125
112
|
if (!tool) throw new Error(`Unknown tool: ${toolName}`);
|
|
126
|
-
if (toolName === "grainulation")
|
|
127
|
-
throw new Error('Use "grainulation serve" directly');
|
|
113
|
+
if (toolName === 'grainulation') throw new Error('Use "grainulation serve" directly');
|
|
128
114
|
|
|
129
115
|
// Check if already running
|
|
130
116
|
const existing = readPid(toolName);
|
|
@@ -133,28 +119,20 @@ function startTool(toolName, extraArgs = []) {
|
|
|
133
119
|
}
|
|
134
120
|
|
|
135
121
|
// Auto-inject --root and --dir from config if not explicitly provided
|
|
136
|
-
const hasRoot = extraArgs.includes(
|
|
122
|
+
const hasRoot = extraArgs.includes('--root') || extraArgs.includes('--dir');
|
|
137
123
|
let rootArgs = [];
|
|
138
124
|
if (!hasRoot) {
|
|
139
125
|
const cfg = loadConfig();
|
|
140
|
-
if (cfg.sprintRoot)
|
|
141
|
-
rootArgs = ["--root", cfg.sprintRoot, "--dir", cfg.sprintRoot];
|
|
126
|
+
if (cfg.sprintRoot) rootArgs = ['--root', cfg.sprintRoot, '--dir', cfg.sprintRoot];
|
|
142
127
|
}
|
|
143
128
|
|
|
144
129
|
const bin = findBin(tool);
|
|
145
|
-
const args = [
|
|
146
|
-
...bin.args,
|
|
147
|
-
...tool.serveCmd,
|
|
148
|
-
"--port",
|
|
149
|
-
String(tool.port),
|
|
150
|
-
...rootArgs,
|
|
151
|
-
...extraArgs,
|
|
152
|
-
];
|
|
130
|
+
const args = [...bin.args, ...tool.serveCmd, '--port', String(tool.port), ...rootArgs, ...extraArgs];
|
|
153
131
|
|
|
154
132
|
const child = spawn(bin.cmd, args, {
|
|
155
|
-
stdio:
|
|
133
|
+
stdio: 'ignore',
|
|
156
134
|
detached: true,
|
|
157
|
-
shell:
|
|
135
|
+
shell: false,
|
|
158
136
|
});
|
|
159
137
|
|
|
160
138
|
child.unref();
|
|
@@ -169,16 +147,16 @@ function startTool(toolName, extraArgs = []) {
|
|
|
169
147
|
*/
|
|
170
148
|
function findPidByPort(port) {
|
|
171
149
|
try {
|
|
172
|
-
const out =
|
|
150
|
+
const out = execFileSync('lsof', ['-ti', `:${port}`], {
|
|
173
151
|
timeout: 3000,
|
|
174
|
-
stdio: [
|
|
152
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
153
|
});
|
|
176
154
|
const pids = out
|
|
177
155
|
.toString()
|
|
178
156
|
.trim()
|
|
179
|
-
.split(
|
|
157
|
+
.split('\n')
|
|
180
158
|
.map((s) => parseInt(s, 10))
|
|
181
|
-
.filter((n) => !isNaN(n) && n > 0);
|
|
159
|
+
.filter((n) => !Number.isNaN(n) && n > 0);
|
|
182
160
|
// Return the first PID that isn't our own process
|
|
183
161
|
return pids.find((p) => p !== process.pid) || null;
|
|
184
162
|
} catch {
|
|
@@ -197,16 +175,16 @@ function stopTool(toolName) {
|
|
|
197
175
|
if (!pid) {
|
|
198
176
|
const tool = getByName(toolName);
|
|
199
177
|
if (tool) pid = findPidByPort(tool.port);
|
|
200
|
-
if (!pid) return { stopped: false, reason:
|
|
178
|
+
if (!pid) return { stopped: false, reason: 'not running' };
|
|
201
179
|
}
|
|
202
180
|
|
|
203
181
|
try {
|
|
204
|
-
process.kill(pid,
|
|
182
|
+
process.kill(pid, 'SIGTERM');
|
|
205
183
|
removePid(toolName);
|
|
206
184
|
return { stopped: true, pid };
|
|
207
185
|
} catch {
|
|
208
186
|
removePid(toolName);
|
|
209
|
-
return { stopped: false, reason:
|
|
187
|
+
return { stopped: false, reason: 'process already dead' };
|
|
210
188
|
}
|
|
211
189
|
}
|
|
212
190
|
|
|
@@ -238,11 +216,11 @@ async function ps() {
|
|
|
238
216
|
* 'all' starts everything except grainulation itself.
|
|
239
217
|
*/
|
|
240
218
|
function up(toolNames, extraArgs = []) {
|
|
241
|
-
const defaults = [
|
|
219
|
+
const defaults = ['farmer', 'wheat'];
|
|
242
220
|
const names =
|
|
243
221
|
!toolNames || toolNames.length === 0
|
|
244
222
|
? defaults
|
|
245
|
-
: toolNames[0] ===
|
|
223
|
+
: toolNames[0] === 'all'
|
|
246
224
|
? getInstallable().map((t) => t.name)
|
|
247
225
|
: toolNames;
|
|
248
226
|
|
|
@@ -262,10 +240,7 @@ function up(toolNames, extraArgs = []) {
|
|
|
262
240
|
* Stop multiple tools. Default: stop all running.
|
|
263
241
|
*/
|
|
264
242
|
function down(toolNames) {
|
|
265
|
-
const names =
|
|
266
|
-
!toolNames || toolNames.length === 0
|
|
267
|
-
? getInstallable().map((t) => t.name)
|
|
268
|
-
: toolNames;
|
|
243
|
+
const names = !toolNames || toolNames.length === 0 ? getInstallable().map((t) => t.name) : toolNames;
|
|
269
244
|
|
|
270
245
|
const results = [];
|
|
271
246
|
for (const name of names) {
|