@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/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: 'wheat',
13
- package: '@grainulation/wheat',
14
- icon: 'W',
15
- role: 'Grows evidence',
16
- description: 'Research sprint engine. Ask a question, grow claims, compile a brief.',
17
- category: 'core',
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: ['serve'],
18
+ serveCmd: ["serve"],
20
19
  entryPoint: true,
21
20
  },
22
21
  {
23
- name: 'farmer',
24
- package: '@grainulation/farmer',
25
- icon: 'F',
26
- role: 'Permission dashboard',
27
- description: 'Permission dashboard. Approve tool calls, review AI actions in real time.',
28
- category: 'core',
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: ['start'],
30
+ serveCmd: ["start"],
31
31
  entryPoint: false,
32
32
  },
33
33
  {
34
- name: 'barn',
35
- package: '@grainulation/barn',
36
- icon: 'B',
37
- role: 'Shared tools',
38
- description: 'Public utilities. Claim schemas, HTML templates, shared validators.',
39
- category: 'foundation',
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: ['serve'],
42
+ serveCmd: ["serve"],
42
43
  entryPoint: false,
43
44
  },
44
45
  {
45
- name: 'mill',
46
- package: '@grainulation/mill',
47
- icon: 'M',
48
- role: 'Processes output',
49
- description: 'Export and publish. Turn compiled research into PDFs, slides, wikis.',
50
- category: 'output',
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: ['serve'],
54
+ serveCmd: ["serve"],
53
55
  entryPoint: false,
54
56
  },
55
57
  {
56
- name: 'silo',
57
- package: '@grainulation/silo',
58
- icon: 'S',
59
- role: 'Stores knowledge',
60
- description: 'Reusable claim libraries. Share vetted claims across sprints and teams.',
61
- category: 'storage',
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: ['serve'],
66
+ serveCmd: ["serve"],
64
67
  entryPoint: false,
65
68
  },
66
69
  {
67
- name: 'harvest',
68
- package: '@grainulation/harvest',
69
- icon: 'H',
70
- role: 'Analytics & retrospectives',
71
- description: 'Cross-sprint learning. Track prediction accuracy, find blind spots over time.',
72
- category: 'analytics',
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: ['serve'],
78
+ serveCmd: ["serve"],
75
79
  entryPoint: false,
76
80
  },
77
81
  {
78
- name: 'orchard',
79
- package: '@grainulation/orchard',
80
- icon: 'O',
81
- role: 'Orchestration',
82
- description: 'Multi-sprint coordination. Run parallel research tracks, merge results.',
83
- category: 'orchestration',
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: ['serve'],
90
+ serveCmd: ["serve"],
86
91
  entryPoint: false,
87
92
  },
88
93
  {
89
- name: 'grainulation',
90
- package: '@grainulation/grainulation',
91
- icon: 'G',
92
- role: 'The machine',
93
- description: 'Process manager and ecosystem hub. Start, stop, and monitor all tools.',
94
- category: 'meta',
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: ['serve'],
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 !== 'grainulation');
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('node:child_process');
11
- const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
12
- const { join } = require('node:path');
13
- const http = require('node:http');
14
- const { getAll, getByName, getInstallable } = require('./ecosystem');
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('node:os').homedir(), '.grainulation');
18
- const PID_DIR = join(PM_DIR, 'pids');
19
- const CONFIG_FILE = join(PM_DIR, 'config.json');
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 { return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; }
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, 'utf8').trim(), 10);
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 { require('node:fs').unlinkSync(f); } catch {}
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({ hostname: '127.0.0.1', port, path: '/health', timeout: timeoutMs }, (res) => {
66
- const latencyMs = Date.now() - start;
67
- // Consume body
68
- res.resume();
69
- resolve({ alive: true, statusCode: res.statusCode, latencyMs });
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, '..', '..', shortName),
83
- join(process.cwd(), '..', shortName),
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, 'package.json');
102
+ const pkgPath = join(dir, "package.json");
88
103
  if (!existsSync(pkgPath)) continue;
89
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
104
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
90
105
  if (pkg.name !== tool.package) continue;
91
106
  if (pkg.bin) {
92
- const binFile = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin)[0];
107
+ const binFile =
108
+ typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
93
109
  if (binFile) {
94
- const binPath = require('node:path').resolve(dir, binFile);
95
- if (existsSync(binPath)) return { cmd: process.execPath, args: [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: 'npx', args: [tool.package], shell: true };
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 === 'grainulation') throw new Error('Use "grainulation serve" directly');
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('--root') || extraArgs.includes('--dir');
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) rootArgs = ['--root', cfg.sprintRoot, '--dir', 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 = [...bin.args, ...tool.serveCmd, '--port', String(tool.port), ...rootArgs, ...extraArgs];
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: 'ignore',
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}`, { timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] });
147
- const pids = out.toString().trim().split('\n').map(s => parseInt(s, 10)).filter(n => !isNaN(n) && n > 0);
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: 'not running' };
200
+ if (!pid) return { stopped: false, reason: "not running" };
167
201
  }
168
202
 
169
203
  try {
170
- process.kill(pid, 'SIGTERM');
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: 'process already dead' };
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(tools.map(async (tool) => {
185
- const pid = readPid(tool.name);
186
- const health = await probe(tool.port);
187
- return {
188
- name: tool.name,
189
- port: tool.port,
190
- role: tool.role,
191
- pid: pid || null,
192
- alive: health.alive,
193
- latencyMs: health.latencyMs || null,
194
- statusCode: health.statusCode || null,
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 = ['farmer', 'wheat'];
206
- const names = (!toolNames || toolNames.length === 0) ? defaults :
207
- (toolNames[0] === 'all' ? getInstallable().map(t => t.name) : toolNames);
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 = (!toolNames || toolNames.length === 0)
226
- ? getInstallable().map(t => t.name)
227
- : toolNames;
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) {