@grainulation/grainulation 1.0.0 → 1.0.1
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 +14 -15
- package/lib/doctor.js +173 -60
- package/lib/ecosystem.js +65 -59
- package/lib/pm.js +100 -59
- package/lib/router.js +241 -183
- package/lib/server.mjs +165 -49
- package/lib/setup.js +47 -43
- package/package.json +15 -4
- package/public/grainulation-tokens.css +75 -80
package/lib/ecosystem.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* The grainulation ecosystem registry.
|
|
5
3
|
*
|
|
@@ -9,91 +7,99 @@
|
|
|
9
7
|
|
|
10
8
|
const TOOLS = [
|
|
11
9
|
{
|
|
12
|
-
name:
|
|
13
|
-
package:
|
|
14
|
-
icon:
|
|
15
|
-
role:
|
|
16
|
-
description:
|
|
17
|
-
|
|
10
|
+
name: "wheat",
|
|
11
|
+
package: "@grainulation/wheat",
|
|
12
|
+
icon: "W",
|
|
13
|
+
role: "Grows evidence",
|
|
14
|
+
description:
|
|
15
|
+
"Research sprint engine. Ask a question, grow claims, compile a brief.",
|
|
16
|
+
category: "core",
|
|
18
17
|
port: 9091,
|
|
19
|
-
serveCmd: [
|
|
18
|
+
serveCmd: ["serve"],
|
|
20
19
|
entryPoint: true,
|
|
21
20
|
},
|
|
22
21
|
{
|
|
23
|
-
name:
|
|
24
|
-
package:
|
|
25
|
-
icon:
|
|
26
|
-
role:
|
|
27
|
-
description:
|
|
28
|
-
|
|
22
|
+
name: "farmer",
|
|
23
|
+
package: "@grainulation/farmer",
|
|
24
|
+
icon: "F",
|
|
25
|
+
role: "Permission dashboard",
|
|
26
|
+
description:
|
|
27
|
+
"Permission dashboard. Approve tool calls, review AI actions in real time.",
|
|
28
|
+
category: "core",
|
|
29
29
|
port: 9090,
|
|
30
|
-
serveCmd: [
|
|
30
|
+
serveCmd: ["start"],
|
|
31
31
|
entryPoint: false,
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
name:
|
|
35
|
-
package:
|
|
36
|
-
icon:
|
|
37
|
-
role:
|
|
38
|
-
description:
|
|
39
|
-
|
|
34
|
+
name: "barn",
|
|
35
|
+
package: "@grainulation/barn",
|
|
36
|
+
icon: "B",
|
|
37
|
+
role: "Shared tools",
|
|
38
|
+
description:
|
|
39
|
+
"Public utilities. Claim schemas, HTML templates, shared validators.",
|
|
40
|
+
category: "foundation",
|
|
40
41
|
port: 9093,
|
|
41
|
-
serveCmd: [
|
|
42
|
+
serveCmd: ["serve"],
|
|
42
43
|
entryPoint: false,
|
|
43
44
|
},
|
|
44
45
|
{
|
|
45
|
-
name:
|
|
46
|
-
package:
|
|
47
|
-
icon:
|
|
48
|
-
role:
|
|
49
|
-
description:
|
|
50
|
-
|
|
46
|
+
name: "mill",
|
|
47
|
+
package: "@grainulation/mill",
|
|
48
|
+
icon: "M",
|
|
49
|
+
role: "Processes output",
|
|
50
|
+
description:
|
|
51
|
+
"Export and publish. Turn compiled research into PDFs, slides, wikis.",
|
|
52
|
+
category: "output",
|
|
51
53
|
port: 9094,
|
|
52
|
-
serveCmd: [
|
|
54
|
+
serveCmd: ["serve"],
|
|
53
55
|
entryPoint: false,
|
|
54
56
|
},
|
|
55
57
|
{
|
|
56
|
-
name:
|
|
57
|
-
package:
|
|
58
|
-
icon:
|
|
59
|
-
role:
|
|
60
|
-
description:
|
|
61
|
-
|
|
58
|
+
name: "silo",
|
|
59
|
+
package: "@grainulation/silo",
|
|
60
|
+
icon: "S",
|
|
61
|
+
role: "Stores knowledge",
|
|
62
|
+
description:
|
|
63
|
+
"Reusable claim libraries. Share vetted claims across sprints and teams.",
|
|
64
|
+
category: "storage",
|
|
62
65
|
port: 9095,
|
|
63
|
-
serveCmd: [
|
|
66
|
+
serveCmd: ["serve"],
|
|
64
67
|
entryPoint: false,
|
|
65
68
|
},
|
|
66
69
|
{
|
|
67
|
-
name:
|
|
68
|
-
package:
|
|
69
|
-
icon:
|
|
70
|
-
role:
|
|
71
|
-
description:
|
|
72
|
-
|
|
70
|
+
name: "harvest",
|
|
71
|
+
package: "@grainulation/harvest",
|
|
72
|
+
icon: "H",
|
|
73
|
+
role: "Analytics & retrospectives",
|
|
74
|
+
description:
|
|
75
|
+
"Cross-sprint learning. Track prediction accuracy, find blind spots over time.",
|
|
76
|
+
category: "analytics",
|
|
73
77
|
port: 9096,
|
|
74
|
-
serveCmd: [
|
|
78
|
+
serveCmd: ["serve"],
|
|
75
79
|
entryPoint: false,
|
|
76
80
|
},
|
|
77
81
|
{
|
|
78
|
-
name:
|
|
79
|
-
package:
|
|
80
|
-
icon:
|
|
81
|
-
role:
|
|
82
|
-
description:
|
|
83
|
-
|
|
82
|
+
name: "orchard",
|
|
83
|
+
package: "@grainulation/orchard",
|
|
84
|
+
icon: "O",
|
|
85
|
+
role: "Orchestration",
|
|
86
|
+
description:
|
|
87
|
+
"Multi-sprint coordination. Run parallel research tracks, merge results.",
|
|
88
|
+
category: "orchestration",
|
|
84
89
|
port: 9097,
|
|
85
|
-
serveCmd: [
|
|
90
|
+
serveCmd: ["serve"],
|
|
86
91
|
entryPoint: false,
|
|
87
92
|
},
|
|
88
93
|
{
|
|
89
|
-
name:
|
|
90
|
-
package:
|
|
91
|
-
icon:
|
|
92
|
-
role:
|
|
93
|
-
description:
|
|
94
|
-
|
|
94
|
+
name: "grainulation",
|
|
95
|
+
package: "@grainulation/grainulation",
|
|
96
|
+
icon: "G",
|
|
97
|
+
role: "The machine",
|
|
98
|
+
description:
|
|
99
|
+
"Process manager and ecosystem hub. Start, stop, and monitor all tools.",
|
|
100
|
+
category: "meta",
|
|
95
101
|
port: 9098,
|
|
96
|
-
serveCmd: [
|
|
102
|
+
serveCmd: ["serve"],
|
|
97
103
|
entryPoint: false,
|
|
98
104
|
},
|
|
99
105
|
];
|
|
@@ -107,7 +113,7 @@ function getByName(name) {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
function getInstallable() {
|
|
110
|
-
return TOOLS.filter((t) => t.name !==
|
|
116
|
+
return TOOLS.filter((t) => t.name !== "grainulation");
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
function getCategories() {
|
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,19 +5,28 @@
|
|
|
7
5
|
* This module spawns/kills them and probes ports for health.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
const { spawn, execSync } = require(
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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");
|
|
15
18
|
|
|
16
19
|
// PID tracking directory
|
|
17
|
-
const PM_DIR = join(require(
|
|
18
|
-
const PID_DIR = join(PM_DIR,
|
|
19
|
-
const CONFIG_FILE = join(PM_DIR,
|
|
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");
|
|
20
23
|
|
|
21
24
|
function loadConfig() {
|
|
22
|
-
try {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
function ensureDirs() {
|
|
@@ -35,7 +42,7 @@ function readPid(toolName) {
|
|
|
35
42
|
const f = pidFile(toolName);
|
|
36
43
|
if (!existsSync(f)) return null;
|
|
37
44
|
try {
|
|
38
|
-
const pid = parseInt(readFileSync(f,
|
|
45
|
+
const pid = parseInt(readFileSync(f, "utf8").trim(), 10);
|
|
39
46
|
if (isNaN(pid)) return null;
|
|
40
47
|
// Check if process is alive
|
|
41
48
|
process.kill(pid, 0);
|
|
@@ -52,7 +59,9 @@ function writePid(toolName, pid) {
|
|
|
52
59
|
|
|
53
60
|
function removePid(toolName) {
|
|
54
61
|
const f = pidFile(toolName);
|
|
55
|
-
try {
|
|
62
|
+
try {
|
|
63
|
+
require("node:fs").unlinkSync(f);
|
|
64
|
+
} catch {}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/**
|
|
@@ -62,14 +71,20 @@ function removePid(toolName) {
|
|
|
62
71
|
function probe(port, timeoutMs = 2000) {
|
|
63
72
|
return new Promise((resolve) => {
|
|
64
73
|
const start = Date.now();
|
|
65
|
-
const req = http.get(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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", () => {
|
|
85
|
+
req.destroy();
|
|
86
|
+
resolve({ alive: false });
|
|
70
87
|
});
|
|
71
|
-
req.on('error', () => resolve({ alive: false }));
|
|
72
|
-
req.on('timeout', () => { req.destroy(); resolve({ alive: false }); });
|
|
73
88
|
});
|
|
74
89
|
}
|
|
75
90
|
|
|
@@ -77,27 +92,29 @@ function probe(port, timeoutMs = 2000) {
|
|
|
77
92
|
* Find the bin path for a tool — prefers source checkout, falls back to npx.
|
|
78
93
|
*/
|
|
79
94
|
function findBin(tool) {
|
|
80
|
-
const shortName = tool.package.replace(/^@[^/]+\//,
|
|
95
|
+
const shortName = tool.package.replace(/^@[^/]+\//, "");
|
|
81
96
|
const candidates = [
|
|
82
|
-
join(__dirname,
|
|
83
|
-
join(process.cwd(),
|
|
97
|
+
join(__dirname, "..", "..", shortName),
|
|
98
|
+
join(process.cwd(), "..", shortName),
|
|
84
99
|
];
|
|
85
100
|
for (const dir of candidates) {
|
|
86
101
|
try {
|
|
87
|
-
const pkgPath = join(dir,
|
|
102
|
+
const pkgPath = join(dir, "package.json");
|
|
88
103
|
if (!existsSync(pkgPath)) continue;
|
|
89
|
-
const pkg = JSON.parse(readFileSync(pkgPath,
|
|
104
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
90
105
|
if (pkg.name !== tool.package) continue;
|
|
91
106
|
if (pkg.bin) {
|
|
92
|
-
const binFile =
|
|
107
|
+
const binFile =
|
|
108
|
+
typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
|
|
93
109
|
if (binFile) {
|
|
94
|
-
const binPath = require(
|
|
95
|
-
if (existsSync(binPath))
|
|
110
|
+
const binPath = require("node:path").resolve(dir, binFile);
|
|
111
|
+
if (existsSync(binPath))
|
|
112
|
+
return { cmd: process.execPath, args: [binPath] };
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
115
|
} catch {}
|
|
99
116
|
}
|
|
100
|
-
return { cmd:
|
|
117
|
+
return { cmd: "npx", args: [tool.package], shell: true };
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
/**
|
|
@@ -106,7 +123,8 @@ function findBin(tool) {
|
|
|
106
123
|
function startTool(toolName, extraArgs = []) {
|
|
107
124
|
const tool = getByName(toolName);
|
|
108
125
|
if (!tool) throw new Error(`Unknown tool: ${toolName}`);
|
|
109
|
-
if (toolName ===
|
|
126
|
+
if (toolName === "grainulation")
|
|
127
|
+
throw new Error('Use "grainulation serve" directly');
|
|
110
128
|
|
|
111
129
|
// Check if already running
|
|
112
130
|
const existing = readPid(toolName);
|
|
@@ -115,18 +133,26 @@ function startTool(toolName, extraArgs = []) {
|
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
// Auto-inject --root and --dir from config if not explicitly provided
|
|
118
|
-
const hasRoot = extraArgs.includes(
|
|
136
|
+
const hasRoot = extraArgs.includes("--root") || extraArgs.includes("--dir");
|
|
119
137
|
let rootArgs = [];
|
|
120
138
|
if (!hasRoot) {
|
|
121
139
|
const cfg = loadConfig();
|
|
122
|
-
if (cfg.sprintRoot)
|
|
140
|
+
if (cfg.sprintRoot)
|
|
141
|
+
rootArgs = ["--root", cfg.sprintRoot, "--dir", cfg.sprintRoot];
|
|
123
142
|
}
|
|
124
143
|
|
|
125
144
|
const bin = findBin(tool);
|
|
126
|
-
const args = [
|
|
145
|
+
const args = [
|
|
146
|
+
...bin.args,
|
|
147
|
+
...tool.serveCmd,
|
|
148
|
+
"--port",
|
|
149
|
+
String(tool.port),
|
|
150
|
+
...rootArgs,
|
|
151
|
+
...extraArgs,
|
|
152
|
+
];
|
|
127
153
|
|
|
128
154
|
const child = spawn(bin.cmd, args, {
|
|
129
|
-
stdio:
|
|
155
|
+
stdio: "ignore",
|
|
130
156
|
detached: true,
|
|
131
157
|
shell: bin.shell || false,
|
|
132
158
|
});
|
|
@@ -143,10 +169,18 @@ function startTool(toolName, extraArgs = []) {
|
|
|
143
169
|
*/
|
|
144
170
|
function findPidByPort(port) {
|
|
145
171
|
try {
|
|
146
|
-
const out = execSync(`lsof -ti :${port}`, {
|
|
147
|
-
|
|
172
|
+
const out = execSync(`lsof -ti :${port}`, {
|
|
173
|
+
timeout: 3000,
|
|
174
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
175
|
+
});
|
|
176
|
+
const pids = out
|
|
177
|
+
.toString()
|
|
178
|
+
.trim()
|
|
179
|
+
.split("\n")
|
|
180
|
+
.map((s) => parseInt(s, 10))
|
|
181
|
+
.filter((n) => !isNaN(n) && n > 0);
|
|
148
182
|
// Return the first PID that isn't our own process
|
|
149
|
-
return pids.find(p => p !== process.pid) || null;
|
|
183
|
+
return pids.find((p) => p !== process.pid) || null;
|
|
150
184
|
} catch {
|
|
151
185
|
return null;
|
|
152
186
|
}
|
|
@@ -163,16 +197,16 @@ function stopTool(toolName) {
|
|
|
163
197
|
if (!pid) {
|
|
164
198
|
const tool = getByName(toolName);
|
|
165
199
|
if (tool) pid = findPidByPort(tool.port);
|
|
166
|
-
if (!pid) return { stopped: false, reason:
|
|
200
|
+
if (!pid) return { stopped: false, reason: "not running" };
|
|
167
201
|
}
|
|
168
202
|
|
|
169
203
|
try {
|
|
170
|
-
process.kill(pid,
|
|
204
|
+
process.kill(pid, "SIGTERM");
|
|
171
205
|
removePid(toolName);
|
|
172
206
|
return { stopped: true, pid };
|
|
173
207
|
} catch {
|
|
174
208
|
removePid(toolName);
|
|
175
|
-
return { stopped: false, reason:
|
|
209
|
+
return { stopped: false, reason: "process already dead" };
|
|
176
210
|
}
|
|
177
211
|
}
|
|
178
212
|
|
|
@@ -181,19 +215,21 @@ function stopTool(toolName) {
|
|
|
181
215
|
*/
|
|
182
216
|
async function ps() {
|
|
183
217
|
const tools = getInstallable();
|
|
184
|
-
const results = await Promise.all(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
218
|
+
const results = await Promise.all(
|
|
219
|
+
tools.map(async (tool) => {
|
|
220
|
+
const pid = readPid(tool.name);
|
|
221
|
+
const health = await probe(tool.port);
|
|
222
|
+
return {
|
|
223
|
+
name: tool.name,
|
|
224
|
+
port: tool.port,
|
|
225
|
+
role: tool.role,
|
|
226
|
+
pid: pid || null,
|
|
227
|
+
alive: health.alive,
|
|
228
|
+
latencyMs: health.latencyMs || null,
|
|
229
|
+
statusCode: health.statusCode || null,
|
|
230
|
+
};
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
197
233
|
return results;
|
|
198
234
|
}
|
|
199
235
|
|
|
@@ -202,9 +238,13 @@ async function ps() {
|
|
|
202
238
|
* 'all' starts everything except grainulation itself.
|
|
203
239
|
*/
|
|
204
240
|
function up(toolNames, extraArgs = []) {
|
|
205
|
-
const defaults = [
|
|
206
|
-
const names =
|
|
207
|
-
|
|
241
|
+
const defaults = ["farmer", "wheat"];
|
|
242
|
+
const names =
|
|
243
|
+
!toolNames || toolNames.length === 0
|
|
244
|
+
? defaults
|
|
245
|
+
: toolNames[0] === "all"
|
|
246
|
+
? getInstallable().map((t) => t.name)
|
|
247
|
+
: toolNames;
|
|
208
248
|
|
|
209
249
|
const results = [];
|
|
210
250
|
for (const name of names) {
|
|
@@ -222,9 +262,10 @@ function up(toolNames, extraArgs = []) {
|
|
|
222
262
|
* Stop multiple tools. Default: stop all running.
|
|
223
263
|
*/
|
|
224
264
|
function down(toolNames) {
|
|
225
|
-
const names =
|
|
226
|
-
|
|
227
|
-
|
|
265
|
+
const names =
|
|
266
|
+
!toolNames || toolNames.length === 0
|
|
267
|
+
? getInstallable().map((t) => t.name)
|
|
268
|
+
: toolNames;
|
|
228
269
|
|
|
229
270
|
const results = [];
|
|
230
271
|
for (const name of names) {
|