@grainulation/grainulation 1.0.0 → 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 +46 -62
- package/bin/grainulation.js +0 -2
- package/lib/doctor.js +93 -17
- package/lib/ecosystem.js +0 -2
- package/lib/pm.js +50 -34
- package/lib/router.js +51 -29
- package/lib/server.mjs +159 -54
- package/lib/setup.js +2 -7
- package/package.json +14 -4
- package/public/grainulation-tokens.css +75 -80
- package/public/index.html +1 -1
package/README.md
CHANGED
|
@@ -1,85 +1,71 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="site/wordmark.svg" alt="grainulation" width="400">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@grainulation/grainulation"><img src="https://img.shields.io/npm/v/@grainulation/grainulation?label=%40grainulation%2Fgrainulation" alt="npm version"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@grainulation/grainulation"><img src="https://img.shields.io/npm/dm/@grainulation/grainulation" alt="npm downloads"></a>
|
|
8
|
+
<a href="https://github.com/grainulation/grainulation/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
|
|
9
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@grainulation/grainulation" alt="node"></a>
|
|
10
|
+
<a href="https://github.com/grainulation/grainulation/actions"><img src="https://github.com/grainulation/grainulation/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
11
|
+
<a href="https://deepwiki.com/grainulation/grainulation"><img src="https://deepwiki.com/badge.svg" alt="Explore on DeepWiki"></a>
|
|
12
|
+
</p>
|
|
4
13
|
|
|
5
|
-
|
|
14
|
+
<p align="center"><strong>Structured research for decisions that satisfice.</strong></p>
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
Most decisions fail not because the team lacked data, but because they lacked a process for turning data into evidence and evidence into conviction. Grainulation is that process.
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
You start with a question. You grow evidence: claims with types, confidence levels, and evidence tiers. You challenge what you find. You look for blind spots. And only when the evidence compiles -- when conflicts are resolved and gaps are acknowledged -- do you write the brief.
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
## Install
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
The brief is not the goal. The brief is the receipt. The goal is the thinking that got you there.
|
|
16
|
-
|
|
17
|
-
## The journey
|
|
18
|
-
|
|
19
|
-
```mermaid
|
|
20
|
-
flowchart LR
|
|
21
|
-
Q["Question"] -->|"/init"| S["Seed Claims"]
|
|
22
|
-
S -->|"/research"| C["Grow Evidence"]
|
|
23
|
-
C -->|"/compile"| B["Compile Brief"]
|
|
24
|
-
C -->|"/challenge /blind-spot"| A["Adversarial Pressure"]
|
|
25
|
-
A -->|"/witness /feedback"| C
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g @grainulation/grainulation
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## The ecosystem
|
|
31
|
-
|
|
32
|
-
Eight tools. Each does one thing. Use what you need.
|
|
33
|
-
|
|
34
|
-
| Tool | What it does | Install |
|
|
35
|
-
|------|-------------|---------|
|
|
36
|
-
| **wheat** | Grows evidence. Research sprint engine. | `npx @grainulation/wheat init` |
|
|
37
|
-
| **farmer** | Permission dashboard. Approve AI actions in real time. | `npm i -g @grainulation/farmer` |
|
|
38
|
-
| **barn** | Shared tools. Claim schemas, templates, validators. | `npm i -g @grainulation/barn` |
|
|
39
|
-
| **mill** | Processes output. Export to PDF, slides, wiki. | `npm i -g @grainulation/mill` |
|
|
40
|
-
| **silo** | Stores knowledge. Reusable claim libraries. | `npm i -g @grainulation/silo` |
|
|
41
|
-
| **harvest** | Analytics. Cross-sprint learning and prediction scoring. | `npm i -g @grainulation/harvest` |
|
|
42
|
-
| **orchard** | Orchestration. Multi-sprint coordination. | `npm i -g @grainulation/orchard` |
|
|
43
|
-
| **grainulation** | The machine. Unified CLI and brand. | `npm i -g grainulation` |
|
|
44
|
-
|
|
45
|
-
**You don't need all eight.** Start with wheat. That's it. One command:
|
|
26
|
+
Or start a research sprint directly:
|
|
46
27
|
|
|
47
28
|
```bash
|
|
48
29
|
npx @grainulation/wheat init
|
|
49
30
|
```
|
|
50
31
|
|
|
51
|
-
Everything else is optional. Add tools when you feel the friction.
|
|
52
|
-
|
|
53
32
|
## Quick start
|
|
54
33
|
|
|
55
34
|
```bash
|
|
56
|
-
#
|
|
57
|
-
|
|
35
|
+
grainulation # Ecosystem overview
|
|
36
|
+
grainulation doctor # Health check: which tools, which versions
|
|
37
|
+
grainulation setup # Install the right tools for your role
|
|
38
|
+
grainulation wheat init # Delegate to any tool
|
|
39
|
+
grainulation farmer start
|
|
40
|
+
```
|
|
58
41
|
|
|
59
|
-
|
|
60
|
-
npm install -g @grainulation/grainulation
|
|
42
|
+
## The ecosystem
|
|
61
43
|
|
|
62
|
-
|
|
63
|
-
grainulation doctor
|
|
44
|
+
Eight tools. Each does one thing. Use what you need.
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
| Tool | What it does | Install |
|
|
47
|
+
| ------------------------------------------------------------ | ----------------------------------------------------------------------------- | ------------------------------------- |
|
|
48
|
+
| [wheat](https://github.com/grainulation/wheat) | Research engine. Grow structured evidence. | `npx @grainulation/wheat init` |
|
|
49
|
+
| [farmer](https://github.com/grainulation/farmer) | Permission dashboard. Approve AI actions in real time (admin + viewer roles). | `npm i -g @grainulation/farmer` |
|
|
50
|
+
| [barn](https://github.com/grainulation/barn) | Shared tools. Templates, validators, sprint detection. | `npm i -g @grainulation/barn` |
|
|
51
|
+
| [mill](https://github.com/grainulation/mill) | Format conversion. Export to PDF, CSV, slides, 24 formats. | `npm i -g @grainulation/mill` |
|
|
52
|
+
| [silo](https://github.com/grainulation/silo) | Knowledge storage. Reusable claim libraries and packs. | `npm i -g @grainulation/silo` |
|
|
53
|
+
| [harvest](https://github.com/grainulation/harvest) | Analytics. Cross-sprint patterns and prediction scoring. | `npm i -g @grainulation/harvest` |
|
|
54
|
+
| [orchard](https://github.com/grainulation/orchard) | Orchestration. Multi-sprint coordination and dependencies. | `npm i -g @grainulation/orchard` |
|
|
55
|
+
| [grainulation](https://github.com/grainulation/grainulation) | Unified CLI. Single entry point to the ecosystem. | `npm i -g @grainulation/grainulation` |
|
|
67
56
|
|
|
68
|
-
|
|
69
|
-
grainulation wheat init
|
|
70
|
-
grainulation farmer start
|
|
71
|
-
```
|
|
57
|
+
**You don't need all eight.** Start with wheat. That's it. One command. Everything else is optional -- add tools when you feel the friction.
|
|
72
58
|
|
|
73
|
-
## The
|
|
59
|
+
## The journey
|
|
74
60
|
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
```
|
|
62
|
+
Question --> Seed Claims --> Grow Evidence --> Compile Brief
|
|
63
|
+
/init /research /challenge /brief
|
|
64
|
+
/blind-spot
|
|
65
|
+
/witness
|
|
80
66
|
```
|
|
81
67
|
|
|
82
|
-
|
|
68
|
+
Every step is tracked. Every claim has provenance. Every decision is reproducible.
|
|
83
69
|
|
|
84
70
|
## Philosophy
|
|
85
71
|
|
|
@@ -87,18 +73,16 @@ The CLI is the wayfinder. It doesn't do the work -- it points you to the tool th
|
|
|
87
73
|
|
|
88
74
|
**Claims over opinions.** Every finding is a typed claim with an evidence tier. "I think" becomes "r003: factual, tested -- measured 340ms p95 latency under load."
|
|
89
75
|
|
|
90
|
-
**Adversarial pressure over consensus.** The `/challenge` command exists because comfortable agreement is the enemy of good decisions.
|
|
76
|
+
**Adversarial pressure over consensus.** The `/challenge` command exists because comfortable agreement is the enemy of good decisions.
|
|
91
77
|
|
|
92
78
|
**Process over heroics.** A reproducible sprint that anyone can pick up beats a brilliant analysis that lives in one person's head.
|
|
93
79
|
|
|
94
80
|
## Zero dependencies
|
|
95
81
|
|
|
96
|
-
Every grainulation tool runs on Node built-ins only. No npm install waterfall. No left-pad. No supply chain anxiety.
|
|
82
|
+
Every grainulation tool runs on Node built-ins only. No npm install waterfall. No left-pad. No supply chain anxiety.
|
|
97
83
|
|
|
98
84
|
## The name
|
|
99
85
|
|
|
100
|
-
The name comes last.
|
|
101
|
-
|
|
102
86
|
You build the crop (wheat), the steward (farmer), the barn, the mill, the silo, the harvest, the orchard -- and only then do you name the machine that connects them all.
|
|
103
87
|
|
|
104
88
|
Grainulation: the machine that processes the grain.
|
package/bin/grainulation.js
CHANGED
package/lib/doctor.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
const { execSync } = require('node:child_process');
|
|
4
2
|
const { existsSync } = require('node:fs');
|
|
5
3
|
const path = require('node:path');
|
|
@@ -22,6 +20,7 @@ function checkGlobal(packageName) {
|
|
|
22
20
|
const out = execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, {
|
|
23
21
|
stdio: 'pipe',
|
|
24
22
|
encoding: 'utf-8',
|
|
23
|
+
timeout: 5000,
|
|
25
24
|
});
|
|
26
25
|
const match = out.match(new RegExp(`${escapeRegex(packageName)}@(\\S+)`));
|
|
27
26
|
return match ? { version: match[1], method: 'global' } : null;
|
|
@@ -39,6 +38,7 @@ function checkNpxCache(packageName) {
|
|
|
39
38
|
const prefix = execSync('npm config get cache', {
|
|
40
39
|
stdio: 'pipe',
|
|
41
40
|
encoding: 'utf-8',
|
|
41
|
+
timeout: 5000,
|
|
42
42
|
}).trim();
|
|
43
43
|
const npxDir = path.join(prefix, '_npx');
|
|
44
44
|
if (!existsSync(npxDir)) return null;
|
|
@@ -95,9 +95,7 @@ function checkSource(packageName) {
|
|
|
95
95
|
path.join(process.cwd(), 'packages', packageName.replace(/^@[^/]+\//, '')),
|
|
96
96
|
];
|
|
97
97
|
for (const candidate of candidates) {
|
|
98
|
-
const pkgJson = candidate.endsWith('package.json')
|
|
99
|
-
? candidate
|
|
100
|
-
: path.join(candidate, 'package.json');
|
|
98
|
+
const pkgJson = candidate.endsWith('package.json') ? candidate : path.join(candidate, 'package.json');
|
|
101
99
|
if (existsSync(pkgJson)) {
|
|
102
100
|
try {
|
|
103
101
|
const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
|
|
@@ -165,16 +163,62 @@ function getNodeVersion() {
|
|
|
165
163
|
|
|
166
164
|
function getNpmVersion() {
|
|
167
165
|
try {
|
|
168
|
-
return execSync('npm --version', {
|
|
166
|
+
return execSync('npm --version', {
|
|
167
|
+
stdio: 'pipe',
|
|
168
|
+
encoding: 'utf-8',
|
|
169
|
+
timeout: 5000,
|
|
170
|
+
}).trim();
|
|
169
171
|
} catch {
|
|
170
172
|
return 'not found';
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
function getPnpmVersion() {
|
|
177
|
+
try {
|
|
178
|
+
return execSync('pnpm --version', {
|
|
179
|
+
stdio: 'pipe',
|
|
180
|
+
encoding: 'utf-8',
|
|
181
|
+
timeout: 5000,
|
|
182
|
+
}).trim();
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getBiomeVersion() {
|
|
189
|
+
try {
|
|
190
|
+
const out = execSync('npx biome --version', {
|
|
191
|
+
stdio: 'pipe',
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
timeout: 5000,
|
|
194
|
+
}).trim();
|
|
195
|
+
const match = out.match(/(\d+\.\d+\.\d+\S*)/);
|
|
196
|
+
return match ? match[1] : out;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getHooksPath() {
|
|
203
|
+
try {
|
|
204
|
+
return execSync('git config core.hooksPath', {
|
|
205
|
+
stdio: 'pipe',
|
|
206
|
+
encoding: 'utf-8',
|
|
207
|
+
timeout: 5000,
|
|
208
|
+
}).trim();
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
174
214
|
function run(opts) {
|
|
175
|
-
const json = opts
|
|
215
|
+
const json = opts?.json;
|
|
176
216
|
const tools = getInstallable();
|
|
177
217
|
|
|
218
|
+
const pnpmVersion = getPnpmVersion();
|
|
219
|
+
const biomeVersion = getBiomeVersion();
|
|
220
|
+
const hooksPath = getHooksPath();
|
|
221
|
+
|
|
178
222
|
if (json) {
|
|
179
223
|
const toolResults = [];
|
|
180
224
|
for (const tool of tools) {
|
|
@@ -187,12 +231,20 @@ function run(opts) {
|
|
|
187
231
|
method: result ? result.method : null,
|
|
188
232
|
});
|
|
189
233
|
}
|
|
190
|
-
console.log(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
console.log(
|
|
235
|
+
JSON.stringify({
|
|
236
|
+
environment: {
|
|
237
|
+
node: getNodeVersion(),
|
|
238
|
+
npm: getNpmVersion(),
|
|
239
|
+
pnpm: pnpmVersion,
|
|
240
|
+
biome: biomeVersion,
|
|
241
|
+
hooksPath: hooksPath,
|
|
242
|
+
},
|
|
243
|
+
tools: toolResults,
|
|
244
|
+
installed: toolResults.filter((t) => t.installed).length,
|
|
245
|
+
missing: toolResults.filter((t) => !t.installed).length,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
196
248
|
return;
|
|
197
249
|
}
|
|
198
250
|
|
|
@@ -208,6 +260,25 @@ function run(opts) {
|
|
|
208
260
|
console.log(' \x1b[2mEnvironment:\x1b[0m');
|
|
209
261
|
console.log(` Node ${getNodeVersion()}`);
|
|
210
262
|
console.log(` npm v${getNpmVersion()}`);
|
|
263
|
+
if (pnpmVersion) {
|
|
264
|
+
console.log(` pnpm v${pnpmVersion}`);
|
|
265
|
+
} else {
|
|
266
|
+
console.log(' pnpm \x1b[2mnot found\x1b[0m');
|
|
267
|
+
}
|
|
268
|
+
console.log('');
|
|
269
|
+
|
|
270
|
+
// DX tooling
|
|
271
|
+
console.log(' \x1b[2mDX tooling:\x1b[0m');
|
|
272
|
+
if (biomeVersion) {
|
|
273
|
+
console.log(` \x1b[32m\u2713\x1b[0m Biome v${biomeVersion}`);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(' \x1b[2m\u2717 Biome not found (pnpm install to set up)\x1b[0m');
|
|
276
|
+
}
|
|
277
|
+
if (hooksPath) {
|
|
278
|
+
console.log(` \x1b[32m\u2713\x1b[0m Git hooks ${hooksPath}`);
|
|
279
|
+
} else {
|
|
280
|
+
console.log(' \x1b[2m\u2717 Git hooks not configured (run: git config core.hooksPath .githooks)\x1b[0m');
|
|
281
|
+
}
|
|
211
282
|
console.log('');
|
|
212
283
|
|
|
213
284
|
// Tools
|
|
@@ -217,9 +288,7 @@ function run(opts) {
|
|
|
217
288
|
if (result) {
|
|
218
289
|
installed++;
|
|
219
290
|
const ver = `v${result.version}`.padEnd(10);
|
|
220
|
-
console.log(
|
|
221
|
-
` \x1b[32m\u2713\x1b[0m ${tool.name.padEnd(12)} ${ver} \x1b[2m(${result.method})\x1b[0m`
|
|
222
|
-
);
|
|
291
|
+
console.log(` \x1b[32m\u2713\x1b[0m ${tool.name.padEnd(12)} ${ver} \x1b[2m(${result.method})\x1b[0m`);
|
|
223
292
|
} else {
|
|
224
293
|
missing++;
|
|
225
294
|
console.log(` \x1b[2m\u2717 ${tool.name.padEnd(12)} -- (not found)\x1b[0m`);
|
|
@@ -242,4 +311,11 @@ function run(opts) {
|
|
|
242
311
|
console.log('');
|
|
243
312
|
}
|
|
244
313
|
|
|
245
|
-
module.exports = {
|
|
314
|
+
module.exports = {
|
|
315
|
+
run,
|
|
316
|
+
getVersion,
|
|
317
|
+
detect,
|
|
318
|
+
getPnpmVersion,
|
|
319
|
+
getBiomeVersion,
|
|
320
|
+
getHooksPath,
|
|
321
|
+
};
|
package/lib/ecosystem.js
CHANGED
package/lib/pm.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Process Manager — start, stop, and monitor grainulation tools.
|
|
5
3
|
*
|
|
@@ -7,7 +5,7 @@
|
|
|
7
5
|
* This module spawns/kills them and probes ports for health.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
const { spawn,
|
|
8
|
+
const { spawn, execFileSync } = require('node:child_process');
|
|
11
9
|
const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
|
|
12
10
|
const { join } = require('node:path');
|
|
13
11
|
const http = require('node:http');
|
|
@@ -19,7 +17,11 @@ const PID_DIR = join(PM_DIR, 'pids');
|
|
|
19
17
|
const CONFIG_FILE = join(PM_DIR, 'config.json');
|
|
20
18
|
|
|
21
19
|
function loadConfig() {
|
|
22
|
-
try {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function ensureDirs() {
|
|
@@ -36,7 +38,7 @@ function readPid(toolName) {
|
|
|
36
38
|
if (!existsSync(f)) return null;
|
|
37
39
|
try {
|
|
38
40
|
const pid = parseInt(readFileSync(f, 'utf8').trim(), 10);
|
|
39
|
-
if (isNaN(pid)) return null;
|
|
41
|
+
if (Number.isNaN(pid)) return null;
|
|
40
42
|
// Check if process is alive
|
|
41
43
|
process.kill(pid, 0);
|
|
42
44
|
return pid;
|
|
@@ -52,7 +54,9 @@ function writePid(toolName, pid) {
|
|
|
52
54
|
|
|
53
55
|
function removePid(toolName) {
|
|
54
56
|
const f = pidFile(toolName);
|
|
55
|
-
try {
|
|
57
|
+
try {
|
|
58
|
+
require('node:fs').unlinkSync(f);
|
|
59
|
+
} catch {}
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
/**
|
|
@@ -69,7 +73,10 @@ function probe(port, timeoutMs = 2000) {
|
|
|
69
73
|
resolve({ alive: true, statusCode: res.statusCode, latencyMs });
|
|
70
74
|
});
|
|
71
75
|
req.on('error', () => resolve({ alive: false }));
|
|
72
|
-
req.on('timeout', () => {
|
|
76
|
+
req.on('timeout', () => {
|
|
77
|
+
req.destroy();
|
|
78
|
+
resolve({ alive: false });
|
|
79
|
+
});
|
|
73
80
|
});
|
|
74
81
|
}
|
|
75
82
|
|
|
@@ -78,10 +85,7 @@ function probe(port, timeoutMs = 2000) {
|
|
|
78
85
|
*/
|
|
79
86
|
function findBin(tool) {
|
|
80
87
|
const shortName = tool.package.replace(/^@[^/]+\//, '');
|
|
81
|
-
const candidates = [
|
|
82
|
-
join(__dirname, '..', '..', shortName),
|
|
83
|
-
join(process.cwd(), '..', shortName),
|
|
84
|
-
];
|
|
88
|
+
const candidates = [join(__dirname, '..', '..', shortName), join(process.cwd(), '..', shortName)];
|
|
85
89
|
for (const dir of candidates) {
|
|
86
90
|
try {
|
|
87
91
|
const pkgPath = join(dir, 'package.json');
|
|
@@ -97,7 +101,7 @@ function findBin(tool) {
|
|
|
97
101
|
}
|
|
98
102
|
} catch {}
|
|
99
103
|
}
|
|
100
|
-
return { cmd: 'npx', args: [tool.package]
|
|
104
|
+
return { cmd: 'npx', args: [tool.package] };
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
/**
|
|
@@ -128,7 +132,7 @@ function startTool(toolName, extraArgs = []) {
|
|
|
128
132
|
const child = spawn(bin.cmd, args, {
|
|
129
133
|
stdio: 'ignore',
|
|
130
134
|
detached: true,
|
|
131
|
-
shell:
|
|
135
|
+
shell: false,
|
|
132
136
|
});
|
|
133
137
|
|
|
134
138
|
child.unref();
|
|
@@ -143,10 +147,18 @@ function startTool(toolName, extraArgs = []) {
|
|
|
143
147
|
*/
|
|
144
148
|
function findPidByPort(port) {
|
|
145
149
|
try {
|
|
146
|
-
const out =
|
|
147
|
-
|
|
150
|
+
const out = execFileSync('lsof', ['-ti', `:${port}`], {
|
|
151
|
+
timeout: 3000,
|
|
152
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
153
|
+
});
|
|
154
|
+
const pids = out
|
|
155
|
+
.toString()
|
|
156
|
+
.trim()
|
|
157
|
+
.split('\n')
|
|
158
|
+
.map((s) => parseInt(s, 10))
|
|
159
|
+
.filter((n) => !Number.isNaN(n) && n > 0);
|
|
148
160
|
// Return the first PID that isn't our own process
|
|
149
|
-
return pids.find(p => p !== process.pid) || null;
|
|
161
|
+
return pids.find((p) => p !== process.pid) || null;
|
|
150
162
|
} catch {
|
|
151
163
|
return null;
|
|
152
164
|
}
|
|
@@ -181,19 +193,21 @@ function stopTool(toolName) {
|
|
|
181
193
|
*/
|
|
182
194
|
async function ps() {
|
|
183
195
|
const tools = getInstallable();
|
|
184
|
-
const results = await Promise.all(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
const results = await Promise.all(
|
|
197
|
+
tools.map(async (tool) => {
|
|
198
|
+
const pid = readPid(tool.name);
|
|
199
|
+
const health = await probe(tool.port);
|
|
200
|
+
return {
|
|
201
|
+
name: tool.name,
|
|
202
|
+
port: tool.port,
|
|
203
|
+
role: tool.role,
|
|
204
|
+
pid: pid || null,
|
|
205
|
+
alive: health.alive,
|
|
206
|
+
latencyMs: health.latencyMs || null,
|
|
207
|
+
statusCode: health.statusCode || null,
|
|
208
|
+
};
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
197
211
|
return results;
|
|
198
212
|
}
|
|
199
213
|
|
|
@@ -203,8 +217,12 @@ async function ps() {
|
|
|
203
217
|
*/
|
|
204
218
|
function up(toolNames, extraArgs = []) {
|
|
205
219
|
const defaults = ['farmer', 'wheat'];
|
|
206
|
-
const names =
|
|
207
|
-
|
|
220
|
+
const names =
|
|
221
|
+
!toolNames || toolNames.length === 0
|
|
222
|
+
? defaults
|
|
223
|
+
: toolNames[0] === 'all'
|
|
224
|
+
? getInstallable().map((t) => t.name)
|
|
225
|
+
: toolNames;
|
|
208
226
|
|
|
209
227
|
const results = [];
|
|
210
228
|
for (const name of names) {
|
|
@@ -222,9 +240,7 @@ function up(toolNames, extraArgs = []) {
|
|
|
222
240
|
* Stop multiple tools. Default: stop all running.
|
|
223
241
|
*/
|
|
224
242
|
function down(toolNames) {
|
|
225
|
-
const names =
|
|
226
|
-
? getInstallable().map(t => t.name)
|
|
227
|
-
: toolNames;
|
|
243
|
+
const names = !toolNames || toolNames.length === 0 ? getInstallable().map((t) => t.name) : toolNames;
|
|
228
244
|
|
|
229
245
|
const results = [];
|
|
230
246
|
for (const name of names) {
|