@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/lib/router.js CHANGED
@@ -1,6 +1,4 @@
1
- 'use strict';
2
-
3
- const { execSync, spawn } = require('node:child_process');
1
+ const { execFileSync, spawn } = require('node:child_process');
4
2
  const { existsSync } = require('node:fs');
5
3
  const path = require('node:path');
6
4
  const { getByName, getInstallable } = require('./ecosystem');
@@ -11,9 +9,7 @@ const { getByName, getInstallable } = require('./ecosystem');
11
9
  * Given a command or nothing at all, figure out where to send the user.
12
10
  */
13
11
 
14
- const DELEGATE_COMMANDS = new Set(
15
- getInstallable().map((t) => t.name)
16
- );
12
+ const DELEGATE_COMMANDS = new Set(getInstallable().map((t) => t.name));
17
13
 
18
14
  function overviewData() {
19
15
  const { TOOLS } = require('./ecosystem');
@@ -71,7 +67,9 @@ function overview() {
71
67
  function isInstalled(packageName) {
72
68
  try {
73
69
  if (packageName === 'grainulation') return true;
74
- execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, { stdio: 'pipe' });
70
+ execFileSync('npm', ['list', '-g', packageName, '--depth=0'], {
71
+ stdio: 'pipe',
72
+ });
75
73
  return true;
76
74
  } catch {
77
75
  try {
@@ -89,10 +87,7 @@ function isInstalled(packageName) {
89
87
  */
90
88
  function findSourceBin(tool) {
91
89
  const shortName = tool.package.replace(/^@[^/]+\//, '');
92
- const candidates = [
93
- path.join(__dirname, '..', '..', shortName),
94
- path.join(process.cwd(), '..', shortName),
95
- ];
90
+ const candidates = [path.join(__dirname, '..', '..', shortName), path.join(process.cwd(), '..', shortName)];
96
91
  for (const dir of candidates) {
97
92
  try {
98
93
  const pkgPath = path.join(dir, 'package.json');
@@ -135,7 +130,7 @@ function delegate(toolName, args) {
135
130
 
136
131
  const child = spawn(cmd, cmdArgs, {
137
132
  stdio: 'inherit',
138
- shell: cmd === 'npx',
133
+ shell: false,
139
134
  });
140
135
 
141
136
  child.on('close', (code) => {
@@ -154,7 +149,7 @@ function delegate(toolName, args) {
154
149
  * Checks for existing claims.json, package.json, git repo, etc.
155
150
  */
156
151
  function init(args, opts) {
157
- const json = opts && opts.json;
152
+ const json = opts?.json;
158
153
  const cwd = process.cwd();
159
154
  const hasClaims = existsSync(path.join(cwd, 'claims.json'));
160
155
  const hasCompilation = existsSync(path.join(cwd, 'compilation.json'));
@@ -164,7 +159,14 @@ function init(args, opts) {
164
159
  if (json) {
165
160
  // Pass --json through to wheat init
166
161
  if (hasClaims && hasCompilation) {
167
- console.log(JSON.stringify({ status: 'exists', directory: cwd, hasClaims, hasCompilation }));
162
+ console.log(
163
+ JSON.stringify({
164
+ status: 'exists',
165
+ directory: cwd,
166
+ hasClaims,
167
+ hasCompilation,
168
+ }),
169
+ );
168
170
  return;
169
171
  }
170
172
  if (hasClaims) {
@@ -174,7 +176,13 @@ function init(args, opts) {
174
176
  const { detect } = require('./doctor');
175
177
  const wheatInfo = detect('@grainulation/wheat');
176
178
  if (!wheatInfo) {
177
- console.log(JSON.stringify({ status: 'missing', tool: 'wheat', install: 'npm install -g @grainulation/wheat' }));
179
+ console.log(
180
+ JSON.stringify({
181
+ status: 'missing',
182
+ tool: 'wheat',
183
+ install: 'npm install -g @grainulation/wheat',
184
+ }),
185
+ );
178
186
  return;
179
187
  }
180
188
  delegate('wheat', ['init', '--json', ...args]);
@@ -255,7 +263,7 @@ function statusData() {
255
263
  try {
256
264
  const claimsRaw = require('node:fs').readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
257
265
  const claims = JSON.parse(claimsRaw);
258
- const claimList = Array.isArray(claims) ? claims : (claims.claims || []);
266
+ const claimList = Array.isArray(claims) ? claims : claims.claims || [];
259
267
  const byType = {};
260
268
  for (const c of claimList) {
261
269
  const t = c.type || 'unknown';
@@ -292,7 +300,7 @@ function statusData() {
292
300
  }
293
301
 
294
302
  function status(opts) {
295
- if (opts && opts.json) {
303
+ if (opts?.json) {
296
304
  console.log(JSON.stringify(statusData()));
297
305
  return;
298
306
  }
@@ -333,7 +341,7 @@ function status(opts) {
333
341
  try {
334
342
  const claimsRaw = require('node:fs').readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
335
343
  const claims = JSON.parse(claimsRaw);
336
- const claimList = Array.isArray(claims) ? claims : (claims.claims || []);
344
+ const claimList = Array.isArray(claims) ? claims : claims.claims || [];
337
345
  const total = claimList.length;
338
346
  const byType = {};
339
347
  for (const c of claimList) {
@@ -394,7 +402,7 @@ function status(opts) {
394
402
  function route(args, opts) {
395
403
  const command = args[0];
396
404
  const rest = args.slice(1);
397
- const json = opts && opts.json;
405
+ const json = opts?.json;
398
406
 
399
407
  // No args — show overview
400
408
  if (!command) {
@@ -476,10 +484,10 @@ function route(args, opts) {
476
484
 
477
485
  function pmUp(args, opts) {
478
486
  const pm = require('./pm');
479
- const toolNames = args.filter(a => !a.startsWith('-'));
487
+ const toolNames = args.filter((a) => !a.startsWith('-'));
480
488
  const results = pm.up(toolNames.length > 0 ? toolNames : undefined);
481
489
 
482
- if (opts && opts.json) {
490
+ if (opts?.json) {
483
491
  console.log(JSON.stringify(results));
484
492
  return;
485
493
  }
@@ -503,7 +511,7 @@ function pmUp(args, opts) {
503
511
  // Wait a moment then probe health
504
512
  setTimeout(async () => {
505
513
  const statuses = await pm.ps();
506
- const running = statuses.filter(s => s.alive);
514
+ const running = statuses.filter((s) => s.alive);
507
515
  if (running.length > 0) {
508
516
  console.log(' \x1b[2mHealth check:\x1b[0m');
509
517
  for (const s of running) {
@@ -516,10 +524,10 @@ function pmUp(args, opts) {
516
524
 
517
525
  function pmDown(args, opts) {
518
526
  const pm = require('./pm');
519
- const toolNames = args.filter(a => !a.startsWith('-'));
527
+ const toolNames = args.filter((a) => !a.startsWith('-'));
520
528
  const results = pm.down(toolNames.length > 0 ? toolNames : undefined);
521
529
 
522
- if (opts && opts.json) {
530
+ if (opts?.json) {
523
531
  console.log(JSON.stringify(results));
524
532
  return;
525
533
  }
@@ -547,7 +555,7 @@ async function pmPs(opts) {
547
555
  const pm = require('./pm');
548
556
  const statuses = await pm.ps();
549
557
 
550
- if (opts && opts.json) {
558
+ if (opts?.json) {
551
559
  console.log(JSON.stringify(statuses));
552
560
  return;
553
561
  }
@@ -556,15 +564,17 @@ async function pmPs(opts) {
556
564
  console.log(' \x1b[1;33mgrainulation ps\x1b[0m');
557
565
  console.log('');
558
566
 
559
- const running = statuses.filter(s => s.alive);
560
- const stopped = statuses.filter(s => !s.alive);
567
+ const running = statuses.filter((s) => s.alive);
568
+ const stopped = statuses.filter((s) => !s.alive);
561
569
 
562
570
  if (running.length > 0) {
563
571
  console.log(' \x1b[2mRunning:\x1b[0m');
564
572
  for (const s of running) {
565
573
  const pidStr = s.pid ? `pid ${s.pid}` : 'unknown pid';
566
574
  const latency = s.latencyMs ? `${s.latencyMs}ms` : '';
567
- console.log(` \x1b[32m+\x1b[0m ${s.name.padEnd(12)} :${String(s.port).padEnd(6)} ${pidStr.padEnd(14)} ${latency}`);
575
+ console.log(
576
+ ` \x1b[32m+\x1b[0m ${s.name.padEnd(12)} :${String(s.port).padEnd(6)} ${pidStr.padEnd(14)} ${latency}`,
577
+ );
568
578
  }
569
579
  console.log('');
570
580
  }
@@ -583,4 +593,16 @@ async function pmPs(opts) {
583
593
  }
584
594
  }
585
595
 
586
- module.exports = { route, overview, overviewData, isInstalled, delegate, init, status, statusData, pmUp, pmDown, pmPs };
596
+ module.exports = {
597
+ route,
598
+ overview,
599
+ overviewData,
600
+ isInstalled,
601
+ delegate,
602
+ init,
603
+ status,
604
+ statusData,
605
+ pmUp,
606
+ pmDown,
607
+ pmPs,
608
+ };
package/lib/server.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
4
  * grainulation serve — local HTTP server for the ecosystem control center
4
5
  *
@@ -10,12 +11,12 @@
10
11
  * grainulation serve [--port 9098]
11
12
  */
12
13
 
13
- import { createServer } from 'node:http';
14
- import { readFileSync, existsSync, readdirSync, statSync, mkdirSync, writeFileSync, renameSync, watchFile } from 'node:fs';
15
- import { join, resolve, extname, dirname } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
14
  import { execSync } from 'node:child_process';
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
16
+ import { createServer } from 'node:http';
18
17
  import { createRequire } from 'node:module';
18
+ import { dirname, extname, join, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
19
20
 
20
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
22
  const require = createRequire(import.meta.url);
@@ -72,19 +73,75 @@ const ROUTES = [
72
73
  // ── Tool registry ─────────────────────────────────────────────────────────────
73
74
 
74
75
  const TOOLS = [
75
- { name: 'wheat', pkg: '@grainulation/wheat', port: 9091, accent: '#fbbf24', role: 'Research sprint engine', category: 'core' },
76
- { name: 'farmer', pkg: '@grainulation/farmer', port: 9090, accent: '#3b82f6', role: 'Permission dashboard', category: 'core' },
77
- { name: 'barn', pkg: '@grainulation/barn', port: 9093, accent: '#f43f5e', role: 'Design system & templates', category: 'foundation' },
78
- { name: 'mill', pkg: '@grainulation/mill', port: 9094, accent: '#a78bfa', role: 'Export & publish engine', category: 'output' },
79
- { name: 'silo', pkg: '@grainulation/silo', port: 9095, accent: '#6ee7b7', role: 'Reusable claim libraries', category: 'storage' },
80
- { name: 'harvest', pkg: '@grainulation/harvest', port: 9096, accent: '#fb923c', role: 'Analytics & retrospectives', category: 'analytics' },
81
- { name: 'orchard', pkg: '@grainulation/orchard', port: 9097, accent: '#14b8a6', role: 'Multi-sprint orchestrator', category: 'orchestration' },
82
- { name: 'grainulation', pkg: '@grainulation/grainulation', port: 9098, accent: '#9ca3af', role: 'Ecosystem entry point', category: 'meta' },
76
+ {
77
+ name: 'wheat',
78
+ pkg: '@grainulation/wheat',
79
+ port: 9091,
80
+ accent: '#fbbf24',
81
+ role: 'Research sprint engine',
82
+ category: 'core',
83
+ },
84
+ {
85
+ name: 'farmer',
86
+ pkg: '@grainulation/farmer',
87
+ port: 9090,
88
+ accent: '#3b82f6',
89
+ role: 'Permission dashboard',
90
+ category: 'core',
91
+ },
92
+ {
93
+ name: 'barn',
94
+ pkg: '@grainulation/barn',
95
+ port: 9093,
96
+ accent: '#f43f5e',
97
+ role: 'Design system & templates',
98
+ category: 'foundation',
99
+ },
100
+ {
101
+ name: 'mill',
102
+ pkg: '@grainulation/mill',
103
+ port: 9094,
104
+ accent: '#a78bfa',
105
+ role: 'Export & publish engine',
106
+ category: 'output',
107
+ },
108
+ {
109
+ name: 'silo',
110
+ pkg: '@grainulation/silo',
111
+ port: 9095,
112
+ accent: '#6ee7b7',
113
+ role: 'Reusable claim libraries',
114
+ category: 'storage',
115
+ },
116
+ {
117
+ name: 'harvest',
118
+ pkg: '@grainulation/harvest',
119
+ port: 9096,
120
+ accent: '#fb923c',
121
+ role: 'Analytics & retrospectives',
122
+ category: 'analytics',
123
+ },
124
+ {
125
+ name: 'orchard',
126
+ pkg: '@grainulation/orchard',
127
+ port: 9097,
128
+ accent: '#14b8a6',
129
+ role: 'Multi-sprint orchestrator',
130
+ category: 'orchestration',
131
+ },
132
+ {
133
+ name: 'grainulation',
134
+ pkg: '@grainulation/grainulation',
135
+ port: 9098,
136
+ accent: '#9ca3af',
137
+ role: 'Ecosystem entry point',
138
+ category: 'meta',
139
+ },
83
140
  ];
84
141
 
85
142
  // ── State ─────────────────────────────────────────────────────────────────────
86
143
 
87
- let state = {
144
+ const state = {
88
145
  ecosystem: [],
89
146
  doctorResults: null,
90
147
  lastCheck: null,
@@ -95,7 +152,11 @@ const sseClients = new Set();
95
152
  function broadcast(event) {
96
153
  const data = `data: ${JSON.stringify(event)}\n\n`;
97
154
  for (const res of sseClients) {
98
- try { res.write(data); } catch { sseClients.delete(res); }
155
+ try {
156
+ res.write(data);
157
+ } catch {
158
+ sseClients.delete(res);
159
+ }
99
160
  }
100
161
  }
101
162
 
@@ -116,9 +177,11 @@ function detectTool(pkg) {
116
177
  // 1. Global npm
117
178
  try {
118
179
  const out = execSync(`npm list -g ${pkg} --depth=0 2>/dev/null`, { stdio: 'pipe', encoding: 'utf-8' });
119
- const match = out.match(new RegExp(escapeRegex(pkg) + '@(\\S+)'));
180
+ const match = out.match(new RegExp(`${escapeRegex(pkg)}@(\\S+)`));
120
181
  if (match) return { installed: true, version: match[1], method: 'global' };
121
- } catch { /* not found */ }
182
+ } catch {
183
+ /* not found */
184
+ }
122
185
 
123
186
  // 2. npx cache
124
187
  try {
@@ -139,7 +202,9 @@ function detectTool(pkg) {
139
202
  }
140
203
  }
141
204
  }
142
- } catch { /* not found */ }
205
+ } catch {
206
+ /* not found */
207
+ }
143
208
 
144
209
  // 3. Local node_modules
145
210
  const localPkg = join(process.cwd(), 'node_modules', pkg, 'package.json');
@@ -147,7 +212,9 @@ function detectTool(pkg) {
147
212
  try {
148
213
  const p = JSON.parse(readFileSync(localPkg, 'utf8'));
149
214
  return { installed: true, version: p.version || 'installed', method: 'local' };
150
- } catch { /* ignore */ }
215
+ } catch {
216
+ /* ignore */
217
+ }
151
218
  }
152
219
 
153
220
  // 4. Sibling source directory
@@ -159,7 +226,9 @@ function detectTool(pkg) {
159
226
  if (p.name === pkg) {
160
227
  return { installed: true, version: p.version || 'source', method: 'source' };
161
228
  }
162
- } catch { /* ignore */ }
229
+ } catch {
230
+ /* ignore */
231
+ }
163
232
  }
164
233
 
165
234
  return { installed: false, version: null, method: null };
@@ -174,14 +243,16 @@ function runDoctor() {
174
243
  let npmVersion = 'unknown';
175
244
  try {
176
245
  npmVersion = execSync('npm --version', { stdio: 'pipe', encoding: 'utf-8' }).trim();
177
- } catch { /* ignore */ }
246
+ } catch {
247
+ /* ignore */
248
+ }
178
249
 
179
250
  const checks = [];
180
251
 
181
252
  // Environment checks
182
253
  checks.push({
183
254
  name: 'Node.js',
184
- status: parseInt(nodeVersion.slice(1)) >= 18 ? 'pass' : 'warning',
255
+ status: parseInt(nodeVersion.slice(1), 10) >= 18 ? 'pass' : 'warning',
185
256
  detail: nodeVersion,
186
257
  category: 'environment',
187
258
  });
@@ -197,7 +268,7 @@ function runDoctor() {
197
268
  const toolStatuses = [];
198
269
  for (const tool of TOOLS) {
199
270
  const result = detectTool(tool.pkg);
200
- const status = result.installed ? 'pass' : (tool.category === 'core' ? 'warning' : 'info');
271
+ const status = result.installed ? 'pass' : tool.category === 'core' ? 'warning' : 'info';
201
272
 
202
273
  checks.push({
203
274
  name: tool.name,
@@ -214,9 +285,9 @@ function runDoctor() {
214
285
  });
215
286
  }
216
287
 
217
- const passCount = checks.filter(c => c.status === 'pass').length;
218
- const warnCount = checks.filter(c => c.status === 'warning').length;
219
- const failCount = checks.filter(c => c.status === 'fail').length;
288
+ const passCount = checks.filter((c) => c.status === 'pass').length;
289
+ const warnCount = checks.filter((c) => c.status === 'warning').length;
290
+ const failCount = checks.filter((c) => c.status === 'fail').length;
220
291
 
221
292
  return {
222
293
  checks,
@@ -231,7 +302,7 @@ function runDoctor() {
231
302
  // ── Ecosystem — aggregate status ──────────────────────────────────────────────
232
303
 
233
304
  function getEcosystem() {
234
- return TOOLS.map(tool => {
305
+ return TOOLS.map((tool) => {
235
306
  const result = detectTool(tool.pkg);
236
307
  return {
237
308
  ...tool,
@@ -254,25 +325,36 @@ function scaffold(targetDir, options = {}) {
254
325
  mkdirSync(dir, { recursive: true });
255
326
 
256
327
  // claims.json (atomic write-then-rename)
257
- const claimsData = JSON.stringify({
258
- claims: [],
259
- meta: { created: new Date().toISOString(), tool: 'grainulation' }
260
- }, null, 2) + '\n';
261
- const tmpClaims = join(dir, 'claims.json.tmp.' + process.pid);
328
+ const claimsData = `${JSON.stringify(
329
+ {
330
+ claims: [],
331
+ meta: { created: new Date().toISOString(), tool: 'grainulation' },
332
+ },
333
+ null,
334
+ 2,
335
+ )}\n`;
336
+ const tmpClaims = join(dir, `claims.json.tmp.${process.pid}`);
262
337
  writeFileSync(tmpClaims, claimsData);
263
338
  renameSync(tmpClaims, join(dir, 'claims.json'));
264
339
 
265
340
  // CLAUDE.md
266
341
  const question = options.question || 'What should we build?';
267
- writeFileSync(join(dir, 'CLAUDE.md'), `# Sprint\n\n**Question:** ${question}\n\n**Constraints:**\n- (add constraints here)\n\n**Done looks like:** (describe the output)\n`);
342
+ writeFileSync(
343
+ join(dir, 'CLAUDE.md'),
344
+ `# Sprint\n\n**Question:** ${question}\n\n**Constraints:**\n- (add constraints here)\n\n**Done looks like:** (describe the output)\n`,
345
+ );
268
346
 
269
347
  // orchard.json (if multi-sprint, atomic write-then-rename)
270
348
  if (options.includeOrchard) {
271
- const orchardData = JSON.stringify({
272
- sprints: [],
273
- settings: { sync_interval: 'manual' }
274
- }, null, 2) + '\n';
275
- const tmpOrchard = join(dir, 'orchard.json.tmp.' + process.pid);
349
+ const orchardData = `${JSON.stringify(
350
+ {
351
+ sprints: [],
352
+ settings: { sync_interval: 'manual' },
353
+ },
354
+ null,
355
+ 2,
356
+ )}\n`;
357
+ const tmpOrchard = join(dir, `orchard.json.tmp.${process.pid}`);
276
358
  writeFileSync(tmpOrchard, orchardData);
277
359
  renameSync(tmpOrchard, join(dir, 'orchard.json'));
278
360
  }
@@ -310,10 +392,13 @@ function json(res, data, status = 200) {
310
392
  function readBody(req) {
311
393
  return new Promise((resolve, reject) => {
312
394
  const chunks = [];
313
- req.on('data', c => chunks.push(c));
395
+ req.on('data', (c) => chunks.push(c));
314
396
  req.on('end', () => {
315
- try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
316
- catch { resolve({}); }
397
+ try {
398
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
399
+ } catch {
400
+ resolve({});
401
+ }
317
402
  });
318
403
  req.on('error', reject);
319
404
  });
@@ -347,7 +432,7 @@ table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border-bottom:1
347
432
  th{color:#9ca3af}code{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:13px}</style></head>
348
433
  <body><h1>grainulation API</h1><p>${ROUTES.length} endpoints</p>
349
434
  <table><tr><th>Method</th><th>Path</th><th>Description</th></tr>
350
- ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</code></td><td>'+r.description+'</td></tr>').join('')}
435
+ ${ROUTES.map((r) => `<tr><td><code>${r.method}</code></td><td><code>${r.path}</code></td><td>${r.description}</td></tr>`).join('')}
351
436
  </table></body></html>`;
352
437
  res.writeHead(200, { 'Content-Type': 'text/html' });
353
438
  res.end(html);
@@ -359,15 +444,23 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
359
444
  res.writeHead(200, {
360
445
  'Content-Type': 'text/event-stream',
361
446
  'Cache-Control': 'no-cache',
362
- 'Connection': 'keep-alive',
447
+ Connection: 'keep-alive',
363
448
  });
364
449
  res.write(`data: ${JSON.stringify({ type: 'state', data: state })}\n\n`);
365
450
  const heartbeat = setInterval(() => {
366
- try { res.write(': heartbeat\n\n'); } catch { clearInterval(heartbeat); }
451
+ try {
452
+ res.write(': heartbeat\n\n');
453
+ } catch {
454
+ clearInterval(heartbeat);
455
+ }
367
456
  }, 15000);
368
457
  sseClients.add(res);
369
458
  vlog('sse', `client connected (${sseClients.size} total)`);
370
- req.on('close', () => { clearInterval(heartbeat); sseClients.delete(res); vlog('sse', `client disconnected (${sseClients.size} total)`); });
459
+ req.on('close', () => {
460
+ clearInterval(heartbeat);
461
+ sseClients.delete(res);
462
+ vlog('sse', `client disconnected (${sseClients.size} total)`);
463
+ });
371
464
  return;
372
465
  }
373
466
 
@@ -388,7 +481,7 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
388
481
  // ── API: tool detail ──
389
482
  if (req.method === 'GET' && url.pathname.startsWith('/api/tools/')) {
390
483
  const name = url.pathname.split('/').pop();
391
- const tool = state.ecosystem.find(t => t.name === name);
484
+ const tool = state.ecosystem.find((t) => t.name === name);
392
485
  if (!tool) {
393
486
  json(res, { error: 'Tool not found' }, 404);
394
487
  return;
@@ -401,9 +494,12 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
401
494
  if (req.method === 'POST' && url.pathname === '/api/pm/start') {
402
495
  const body = await readBody(req);
403
496
  const toolName = body.tool;
404
- if (!toolName) { json(res, { error: 'Missing tool name' }, 400); return; }
497
+ if (!toolName) {
498
+ json(res, { error: 'Missing tool name' }, 400);
499
+ return;
500
+ }
405
501
  try {
406
- const rootArgs = (body.root || SPRINT_ROOT) ? ['--root', resolve(body.root || SPRINT_ROOT)] : [];
502
+ const rootArgs = body.root || SPRINT_ROOT ? ['--root', resolve(body.root || SPRINT_ROOT)] : [];
407
503
  const result = pm.startTool(toolName, rootArgs);
408
504
  json(res, { ok: true, ...result });
409
505
  setTimeout(refreshState, 1500);
@@ -417,7 +513,10 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
417
513
  if (req.method === 'POST' && url.pathname === '/api/pm/stop') {
418
514
  const body = await readBody(req);
419
515
  const toolName = body.tool;
420
- if (!toolName) { json(res, { error: 'Missing tool name' }, 400); return; }
516
+ if (!toolName) {
517
+ json(res, { error: 'Missing tool name' }, 400);
518
+ return;
519
+ }
421
520
  try {
422
521
  const result = pm.stopTool(toolName);
423
522
  json(res, { ok: true, ...result });
@@ -477,8 +576,8 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
477
576
  }
478
577
 
479
578
  // ── Static files ──
480
- let filePath = url.pathname === '/' ? '/index.html' : url.pathname;
481
- const resolved = resolve(PUBLIC_DIR, '.' + filePath);
579
+ const filePath = url.pathname === '/' ? '/index.html' : url.pathname;
580
+ const resolved = resolve(PUBLIC_DIR, `.${filePath}`);
482
581
 
483
582
  if (!resolved.startsWith(PUBLIC_DIR)) {
484
583
  res.writeHead(403);
@@ -488,7 +587,7 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
488
587
 
489
588
  if (existsSync(resolved) && statSync(resolved).isFile()) {
490
589
  const ext = extname(resolved);
491
- let content = readFileSync(resolved);
590
+ const content = readFileSync(resolved);
492
591
  res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
493
592
  res.end(content);
494
593
  return;
@@ -501,7 +600,11 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
501
600
  // ── Graceful shutdown ─────────────────────────────────────────────────────────
502
601
  const shutdown = (signal) => {
503
602
  console.log(`\ngrainulation: ${signal} received, shutting down...`);
504
- for (const res of sseClients) { try { res.end(); } catch {} }
603
+ for (const res of sseClients) {
604
+ try {
605
+ res.end();
606
+ } catch {}
607
+ }
505
608
  sseClients.clear();
506
609
  server.close(() => process.exit(0));
507
610
  setTimeout(() => process.exit(1), 5000);
@@ -515,8 +618,10 @@ refreshState();
515
618
 
516
619
  server.listen(PORT, '127.0.0.1', () => {
517
620
  vlog('listen', `port=${PORT}`);
518
- const installed = state.ecosystem.filter(t => t.installed).length;
621
+ const installed = state.ecosystem.filter((t) => t.installed).length;
519
622
  console.log(`grainulation: serving on http://localhost:${PORT}`);
520
623
  console.log(` tools: ${installed}/${TOOLS.length} installed`);
521
- console.log(` doctor: ${state.doctorResults.summary.pass} pass, ${state.doctorResults.summary.warning} warn, ${state.doctorResults.summary.fail} fail`);
624
+ console.log(
625
+ ` doctor: ${state.doctorResults.summary.pass} pass, ${state.doctorResults.summary.warning} warn, ${state.doctorResults.summary.fail} fail`,
626
+ );
522
627
  });
package/lib/setup.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const readline = require('node:readline');
4
2
  const { execSync } = require('node:child_process');
5
3
  const { getInstallable, getCategories } = require('./ecosystem');
@@ -63,7 +61,7 @@ async function run() {
63
61
  const answer = await ask(rl, ' Choose (1-4): ');
64
62
  const choice = parseInt(answer, 10);
65
63
 
66
- if (choice < 1 || choice > ROLES.length || isNaN(choice)) {
64
+ if (choice < 1 || choice > ROLES.length || Number.isNaN(choice)) {
67
65
  console.log('\n \x1b[31mInvalid choice.\x1b[0m\n');
68
66
  rl.close();
69
67
  return;
@@ -98,10 +96,7 @@ async function run() {
98
96
  }
99
97
 
100
98
  console.log('');
101
- const confirm = await ask(
102
- rl,
103
- ` Install ${toInstall.length} package(s)? (y/N): `
104
- );
99
+ const confirm = await ask(rl, ` Install ${toInstall.length} package(s)? (y/N): `);
105
100
 
106
101
  if (confirm.toLowerCase() !== 'y') {
107
102
  console.log('\n \x1b[2mAborted.\x1b[0m\n');
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@grainulation/grainulation",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Structured research for decisions that satisfice",
5
5
  "license": "MIT",
6
+ "workspaces": [
7
+ "packages/*"
8
+ ],
6
9
  "bin": {
7
10
  "grainulation": "bin/grainulation.js"
8
11
  },
@@ -20,7 +23,10 @@
20
23
  ],
21
24
  "scripts": {
22
25
  "test": "node test/basic.test.js",
23
- "start": "node bin/grainulation.js"
26
+ "lint": "biome ci .",
27
+ "format": "biome check --write .",
28
+ "start": "node bin/grainulation.js",
29
+ "postinstall": "git config core.hooksPath .githooks || true"
24
30
  },
25
31
  "keywords": [
26
32
  "research",
@@ -38,7 +44,11 @@
38
44
  },
39
45
  "homepage": "https://grainulation.com",
40
46
  "engines": {
41
- "node": ">=18"
47
+ "node": ">=20"
42
48
  },
43
- "author": "grainulation contributors"
49
+ "packageManager": "pnpm@10.30.0",
50
+ "author": "grainulation contributors",
51
+ "devDependencies": {
52
+ "@biomejs/biome": "2.4.8"
53
+ }
44
54
  }