@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/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, execSync } = require("node:child_process");
9
- const {
10
- existsSync,
11
- readFileSync,
12
- writeFileSync,
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("node:os").homedir(), ".grainulation");
21
- const PID_DIR = join(PM_DIR, "pids");
22
- const CONFIG_FILE = join(PM_DIR, "config.json");
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, "utf8"));
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, "utf8").trim(), 10);
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("node:fs").unlinkSync(f);
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
- { hostname: "127.0.0.1", port, path: "/health", timeout: timeoutMs },
76
- (res) => {
77
- const latencyMs = Date.now() - start;
78
- // Consume body
79
- res.resume();
80
- resolve({ alive: true, statusCode: res.statusCode, latencyMs });
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, "package.json");
91
+ const pkgPath = join(dir, 'package.json');
103
92
  if (!existsSync(pkgPath)) continue;
104
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
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("node:path").resolve(dir, binFile);
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: "npx", args: [tool.package], shell: true };
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("--root") || extraArgs.includes("--dir");
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: "ignore",
133
+ stdio: 'ignore',
156
134
  detached: true,
157
- shell: bin.shell || false,
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 = execSync(`lsof -ti :${port}`, {
150
+ const out = execFileSync('lsof', ['-ti', `:${port}`], {
173
151
  timeout: 3000,
174
- stdio: ["ignore", "pipe", "pipe"],
152
+ stdio: ['ignore', 'pipe', 'pipe'],
175
153
  });
176
154
  const pids = out
177
155
  .toString()
178
156
  .trim()
179
- .split("\n")
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: "not running" };
178
+ if (!pid) return { stopped: false, reason: 'not running' };
201
179
  }
202
180
 
203
181
  try {
204
- process.kill(pid, "SIGTERM");
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: "process already dead" };
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 = ["farmer", "wheat"];
219
+ const defaults = ['farmer', 'wheat'];
242
220
  const names =
243
221
  !toolNames || toolNames.length === 0
244
222
  ? defaults
245
- : toolNames[0] === "all"
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) {