@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/lib/router.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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 :
|
|
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
|
|
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 :
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 = {
|
|
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
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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 {
|
|
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)
|
|
180
|
+
const match = out.match(new RegExp(`${escapeRegex(pkg)}@(\\S+)`));
|
|
120
181
|
if (match) return { installed: true, version: match[1], method: 'global' };
|
|
121
|
-
} catch {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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' :
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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(
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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 {
|
|
316
|
-
|
|
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 =>
|
|
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
|
-
|
|
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 {
|
|
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', () => {
|
|
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) {
|
|
497
|
+
if (!toolName) {
|
|
498
|
+
json(res, { error: 'Missing tool name' }, 400);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
405
501
|
try {
|
|
406
|
-
const rootArgs =
|
|
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) {
|
|
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
|
-
|
|
481
|
-
const resolved = resolve(PUBLIC_DIR,
|
|
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
|
-
|
|
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) {
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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": ">=
|
|
47
|
+
"node": ">=20"
|
|
42
48
|
},
|
|
43
|
-
"
|
|
49
|
+
"packageManager": "pnpm@10.30.0",
|
|
50
|
+
"author": "grainulation contributors",
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@biomejs/biome": "2.4.8"
|
|
53
|
+
}
|
|
44
54
|
}
|