@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/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,21 @@
|
|
|
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 {
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
statSync,
|
|
22
|
+
watchFile,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { createServer } from 'node:http';
|
|
18
26
|
import { createRequire } from 'node:module';
|
|
27
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
19
29
|
|
|
20
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
31
|
const require = createRequire(import.meta.url);
|
|
@@ -72,19 +82,75 @@ const ROUTES = [
|
|
|
72
82
|
// ── Tool registry ─────────────────────────────────────────────────────────────
|
|
73
83
|
|
|
74
84
|
const TOOLS = [
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
{
|
|
86
|
+
name: 'wheat',
|
|
87
|
+
pkg: '@grainulation/wheat',
|
|
88
|
+
port: 9091,
|
|
89
|
+
accent: '#fbbf24',
|
|
90
|
+
role: 'Research sprint engine',
|
|
91
|
+
category: 'core',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'farmer',
|
|
95
|
+
pkg: '@grainulation/farmer',
|
|
96
|
+
port: 9090,
|
|
97
|
+
accent: '#3b82f6',
|
|
98
|
+
role: 'Permission dashboard',
|
|
99
|
+
category: 'core',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'barn',
|
|
103
|
+
pkg: '@grainulation/barn',
|
|
104
|
+
port: 9093,
|
|
105
|
+
accent: '#f43f5e',
|
|
106
|
+
role: 'Design system & templates',
|
|
107
|
+
category: 'foundation',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'mill',
|
|
111
|
+
pkg: '@grainulation/mill',
|
|
112
|
+
port: 9094,
|
|
113
|
+
accent: '#a78bfa',
|
|
114
|
+
role: 'Export & publish engine',
|
|
115
|
+
category: 'output',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'silo',
|
|
119
|
+
pkg: '@grainulation/silo',
|
|
120
|
+
port: 9095,
|
|
121
|
+
accent: '#6ee7b7',
|
|
122
|
+
role: 'Reusable claim libraries',
|
|
123
|
+
category: 'storage',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'harvest',
|
|
127
|
+
pkg: '@grainulation/harvest',
|
|
128
|
+
port: 9096,
|
|
129
|
+
accent: '#fb923c',
|
|
130
|
+
role: 'Analytics & retrospectives',
|
|
131
|
+
category: 'analytics',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'orchard',
|
|
135
|
+
pkg: '@grainulation/orchard',
|
|
136
|
+
port: 9097,
|
|
137
|
+
accent: '#14b8a6',
|
|
138
|
+
role: 'Multi-sprint orchestrator',
|
|
139
|
+
category: 'orchestration',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'grainulation',
|
|
143
|
+
pkg: '@grainulation/grainulation',
|
|
144
|
+
port: 9098,
|
|
145
|
+
accent: '#9ca3af',
|
|
146
|
+
role: 'Ecosystem entry point',
|
|
147
|
+
category: 'meta',
|
|
148
|
+
},
|
|
83
149
|
];
|
|
84
150
|
|
|
85
151
|
// ── State ─────────────────────────────────────────────────────────────────────
|
|
86
152
|
|
|
87
|
-
|
|
153
|
+
const state = {
|
|
88
154
|
ecosystem: [],
|
|
89
155
|
doctorResults: null,
|
|
90
156
|
lastCheck: null,
|
|
@@ -95,7 +161,11 @@ const sseClients = new Set();
|
|
|
95
161
|
function broadcast(event) {
|
|
96
162
|
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
97
163
|
for (const res of sseClients) {
|
|
98
|
-
try {
|
|
164
|
+
try {
|
|
165
|
+
res.write(data);
|
|
166
|
+
} catch {
|
|
167
|
+
sseClients.delete(res);
|
|
168
|
+
}
|
|
99
169
|
}
|
|
100
170
|
}
|
|
101
171
|
|
|
@@ -118,7 +188,9 @@ function detectTool(pkg) {
|
|
|
118
188
|
const out = execSync(`npm list -g ${pkg} --depth=0 2>/dev/null`, { stdio: 'pipe', encoding: 'utf-8' });
|
|
119
189
|
const match = out.match(new RegExp(escapeRegex(pkg) + '@(\\S+)'));
|
|
120
190
|
if (match) return { installed: true, version: match[1], method: 'global' };
|
|
121
|
-
} catch {
|
|
191
|
+
} catch {
|
|
192
|
+
/* not found */
|
|
193
|
+
}
|
|
122
194
|
|
|
123
195
|
// 2. npx cache
|
|
124
196
|
try {
|
|
@@ -139,7 +211,9 @@ function detectTool(pkg) {
|
|
|
139
211
|
}
|
|
140
212
|
}
|
|
141
213
|
}
|
|
142
|
-
} catch {
|
|
214
|
+
} catch {
|
|
215
|
+
/* not found */
|
|
216
|
+
}
|
|
143
217
|
|
|
144
218
|
// 3. Local node_modules
|
|
145
219
|
const localPkg = join(process.cwd(), 'node_modules', pkg, 'package.json');
|
|
@@ -147,7 +221,9 @@ function detectTool(pkg) {
|
|
|
147
221
|
try {
|
|
148
222
|
const p = JSON.parse(readFileSync(localPkg, 'utf8'));
|
|
149
223
|
return { installed: true, version: p.version || 'installed', method: 'local' };
|
|
150
|
-
} catch {
|
|
224
|
+
} catch {
|
|
225
|
+
/* ignore */
|
|
226
|
+
}
|
|
151
227
|
}
|
|
152
228
|
|
|
153
229
|
// 4. Sibling source directory
|
|
@@ -159,7 +235,9 @@ function detectTool(pkg) {
|
|
|
159
235
|
if (p.name === pkg) {
|
|
160
236
|
return { installed: true, version: p.version || 'source', method: 'source' };
|
|
161
237
|
}
|
|
162
|
-
} catch {
|
|
238
|
+
} catch {
|
|
239
|
+
/* ignore */
|
|
240
|
+
}
|
|
163
241
|
}
|
|
164
242
|
|
|
165
243
|
return { installed: false, version: null, method: null };
|
|
@@ -174,7 +252,9 @@ function runDoctor() {
|
|
|
174
252
|
let npmVersion = 'unknown';
|
|
175
253
|
try {
|
|
176
254
|
npmVersion = execSync('npm --version', { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
177
|
-
} catch {
|
|
255
|
+
} catch {
|
|
256
|
+
/* ignore */
|
|
257
|
+
}
|
|
178
258
|
|
|
179
259
|
const checks = [];
|
|
180
260
|
|
|
@@ -197,7 +277,7 @@ function runDoctor() {
|
|
|
197
277
|
const toolStatuses = [];
|
|
198
278
|
for (const tool of TOOLS) {
|
|
199
279
|
const result = detectTool(tool.pkg);
|
|
200
|
-
const status = result.installed ? 'pass' :
|
|
280
|
+
const status = result.installed ? 'pass' : tool.category === 'core' ? 'warning' : 'info';
|
|
201
281
|
|
|
202
282
|
checks.push({
|
|
203
283
|
name: tool.name,
|
|
@@ -214,9 +294,9 @@ function runDoctor() {
|
|
|
214
294
|
});
|
|
215
295
|
}
|
|
216
296
|
|
|
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;
|
|
297
|
+
const passCount = checks.filter((c) => c.status === 'pass').length;
|
|
298
|
+
const warnCount = checks.filter((c) => c.status === 'warning').length;
|
|
299
|
+
const failCount = checks.filter((c) => c.status === 'fail').length;
|
|
220
300
|
|
|
221
301
|
return {
|
|
222
302
|
checks,
|
|
@@ -231,7 +311,7 @@ function runDoctor() {
|
|
|
231
311
|
// ── Ecosystem — aggregate status ──────────────────────────────────────────────
|
|
232
312
|
|
|
233
313
|
function getEcosystem() {
|
|
234
|
-
return TOOLS.map(tool => {
|
|
314
|
+
return TOOLS.map((tool) => {
|
|
235
315
|
const result = detectTool(tool.pkg);
|
|
236
316
|
return {
|
|
237
317
|
...tool,
|
|
@@ -254,24 +334,37 @@ function scaffold(targetDir, options = {}) {
|
|
|
254
334
|
mkdirSync(dir, { recursive: true });
|
|
255
335
|
|
|
256
336
|
// claims.json (atomic write-then-rename)
|
|
257
|
-
const claimsData =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
337
|
+
const claimsData =
|
|
338
|
+
JSON.stringify(
|
|
339
|
+
{
|
|
340
|
+
claims: [],
|
|
341
|
+
meta: { created: new Date().toISOString(), tool: 'grainulation' },
|
|
342
|
+
},
|
|
343
|
+
null,
|
|
344
|
+
2,
|
|
345
|
+
) + '\n';
|
|
261
346
|
const tmpClaims = join(dir, 'claims.json.tmp.' + process.pid);
|
|
262
347
|
writeFileSync(tmpClaims, claimsData);
|
|
263
348
|
renameSync(tmpClaims, join(dir, 'claims.json'));
|
|
264
349
|
|
|
265
350
|
// CLAUDE.md
|
|
266
351
|
const question = options.question || 'What should we build?';
|
|
267
|
-
writeFileSync(
|
|
352
|
+
writeFileSync(
|
|
353
|
+
join(dir, 'CLAUDE.md'),
|
|
354
|
+
`# Sprint\n\n**Question:** ${question}\n\n**Constraints:**\n- (add constraints here)\n\n**Done looks like:** (describe the output)\n`,
|
|
355
|
+
);
|
|
268
356
|
|
|
269
357
|
// orchard.json (if multi-sprint, atomic write-then-rename)
|
|
270
358
|
if (options.includeOrchard) {
|
|
271
|
-
const orchardData =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
359
|
+
const orchardData =
|
|
360
|
+
JSON.stringify(
|
|
361
|
+
{
|
|
362
|
+
sprints: [],
|
|
363
|
+
settings: { sync_interval: 'manual' },
|
|
364
|
+
},
|
|
365
|
+
null,
|
|
366
|
+
2,
|
|
367
|
+
) + '\n';
|
|
275
368
|
const tmpOrchard = join(dir, 'orchard.json.tmp.' + process.pid);
|
|
276
369
|
writeFileSync(tmpOrchard, orchardData);
|
|
277
370
|
renameSync(tmpOrchard, join(dir, 'orchard.json'));
|
|
@@ -310,10 +403,13 @@ function json(res, data, status = 200) {
|
|
|
310
403
|
function readBody(req) {
|
|
311
404
|
return new Promise((resolve, reject) => {
|
|
312
405
|
const chunks = [];
|
|
313
|
-
req.on('data', c => chunks.push(c));
|
|
406
|
+
req.on('data', (c) => chunks.push(c));
|
|
314
407
|
req.on('end', () => {
|
|
315
|
-
try {
|
|
316
|
-
|
|
408
|
+
try {
|
|
409
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
410
|
+
} catch {
|
|
411
|
+
resolve({});
|
|
412
|
+
}
|
|
317
413
|
});
|
|
318
414
|
req.on('error', reject);
|
|
319
415
|
});
|
|
@@ -347,7 +443,7 @@ table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border-bottom:1
|
|
|
347
443
|
th{color:#9ca3af}code{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:13px}</style></head>
|
|
348
444
|
<body><h1>grainulation API</h1><p>${ROUTES.length} endpoints</p>
|
|
349
445
|
<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('')}
|
|
446
|
+
${ROUTES.map((r) => '<tr><td><code>' + r.method + '</code></td><td><code>' + r.path + '</code></td><td>' + r.description + '</td></tr>').join('')}
|
|
351
447
|
</table></body></html>`;
|
|
352
448
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
353
449
|
res.end(html);
|
|
@@ -359,15 +455,23 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
359
455
|
res.writeHead(200, {
|
|
360
456
|
'Content-Type': 'text/event-stream',
|
|
361
457
|
'Cache-Control': 'no-cache',
|
|
362
|
-
|
|
458
|
+
Connection: 'keep-alive',
|
|
363
459
|
});
|
|
364
460
|
res.write(`data: ${JSON.stringify({ type: 'state', data: state })}\n\n`);
|
|
365
461
|
const heartbeat = setInterval(() => {
|
|
366
|
-
try {
|
|
462
|
+
try {
|
|
463
|
+
res.write(': heartbeat\n\n');
|
|
464
|
+
} catch {
|
|
465
|
+
clearInterval(heartbeat);
|
|
466
|
+
}
|
|
367
467
|
}, 15000);
|
|
368
468
|
sseClients.add(res);
|
|
369
469
|
vlog('sse', `client connected (${sseClients.size} total)`);
|
|
370
|
-
req.on('close', () => {
|
|
470
|
+
req.on('close', () => {
|
|
471
|
+
clearInterval(heartbeat);
|
|
472
|
+
sseClients.delete(res);
|
|
473
|
+
vlog('sse', `client disconnected (${sseClients.size} total)`);
|
|
474
|
+
});
|
|
371
475
|
return;
|
|
372
476
|
}
|
|
373
477
|
|
|
@@ -388,7 +492,7 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
388
492
|
// ── API: tool detail ──
|
|
389
493
|
if (req.method === 'GET' && url.pathname.startsWith('/api/tools/')) {
|
|
390
494
|
const name = url.pathname.split('/').pop();
|
|
391
|
-
const tool = state.ecosystem.find(t => t.name === name);
|
|
495
|
+
const tool = state.ecosystem.find((t) => t.name === name);
|
|
392
496
|
if (!tool) {
|
|
393
497
|
json(res, { error: 'Tool not found' }, 404);
|
|
394
498
|
return;
|
|
@@ -401,9 +505,12 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
401
505
|
if (req.method === 'POST' && url.pathname === '/api/pm/start') {
|
|
402
506
|
const body = await readBody(req);
|
|
403
507
|
const toolName = body.tool;
|
|
404
|
-
if (!toolName) {
|
|
508
|
+
if (!toolName) {
|
|
509
|
+
json(res, { error: 'Missing tool name' }, 400);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
405
512
|
try {
|
|
406
|
-
const rootArgs =
|
|
513
|
+
const rootArgs = body.root || SPRINT_ROOT ? ['--root', resolve(body.root || SPRINT_ROOT)] : [];
|
|
407
514
|
const result = pm.startTool(toolName, rootArgs);
|
|
408
515
|
json(res, { ok: true, ...result });
|
|
409
516
|
setTimeout(refreshState, 1500);
|
|
@@ -417,7 +524,10 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
417
524
|
if (req.method === 'POST' && url.pathname === '/api/pm/stop') {
|
|
418
525
|
const body = await readBody(req);
|
|
419
526
|
const toolName = body.tool;
|
|
420
|
-
if (!toolName) {
|
|
527
|
+
if (!toolName) {
|
|
528
|
+
json(res, { error: 'Missing tool name' }, 400);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
421
531
|
try {
|
|
422
532
|
const result = pm.stopTool(toolName);
|
|
423
533
|
json(res, { ok: true, ...result });
|
|
@@ -477,7 +587,7 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
477
587
|
}
|
|
478
588
|
|
|
479
589
|
// ── Static files ──
|
|
480
|
-
|
|
590
|
+
const filePath = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
481
591
|
const resolved = resolve(PUBLIC_DIR, '.' + filePath);
|
|
482
592
|
|
|
483
593
|
if (!resolved.startsWith(PUBLIC_DIR)) {
|
|
@@ -488,7 +598,7 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
488
598
|
|
|
489
599
|
if (existsSync(resolved) && statSync(resolved).isFile()) {
|
|
490
600
|
const ext = extname(resolved);
|
|
491
|
-
|
|
601
|
+
const content = readFileSync(resolved);
|
|
492
602
|
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
|
493
603
|
res.end(content);
|
|
494
604
|
return;
|
|
@@ -501,7 +611,11 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
501
611
|
// ── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
502
612
|
const shutdown = (signal) => {
|
|
503
613
|
console.log(`\ngrainulation: ${signal} received, shutting down...`);
|
|
504
|
-
for (const res of sseClients) {
|
|
614
|
+
for (const res of sseClients) {
|
|
615
|
+
try {
|
|
616
|
+
res.end();
|
|
617
|
+
} catch {}
|
|
618
|
+
}
|
|
505
619
|
sseClients.clear();
|
|
506
620
|
server.close(() => process.exit(0));
|
|
507
621
|
setTimeout(() => process.exit(1), 5000);
|
|
@@ -515,8 +629,10 @@ refreshState();
|
|
|
515
629
|
|
|
516
630
|
server.listen(PORT, '127.0.0.1', () => {
|
|
517
631
|
vlog('listen', `port=${PORT}`);
|
|
518
|
-
const installed = state.ecosystem.filter(t => t.installed).length;
|
|
632
|
+
const installed = state.ecosystem.filter((t) => t.installed).length;
|
|
519
633
|
console.log(`grainulation: serving on http://localhost:${PORT}`);
|
|
520
634
|
console.log(` tools: ${installed}/${TOOLS.length} installed`);
|
|
521
|
-
console.log(
|
|
635
|
+
console.log(
|
|
636
|
+
` doctor: ${state.doctorResults.summary.pass} pass, ${state.doctorResults.summary.warning} warn, ${state.doctorResults.summary.fail} fail`,
|
|
637
|
+
);
|
|
522
638
|
});
|
package/lib/setup.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
const { getInstallable, getCategories } = require('./ecosystem');
|
|
6
|
-
const { getVersion } = require('./doctor');
|
|
1
|
+
const readline = require("node:readline");
|
|
2
|
+
const { execSync } = require("node:child_process");
|
|
3
|
+
const { getInstallable, getCategories } = require("./ecosystem");
|
|
4
|
+
const { getVersion } = require("./doctor");
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* Interactive setup wizard.
|
|
@@ -14,24 +12,24 @@ const { getVersion } = require('./doctor');
|
|
|
14
12
|
|
|
15
13
|
const ROLES = [
|
|
16
14
|
{
|
|
17
|
-
name:
|
|
18
|
-
description:
|
|
19
|
-
tools: [
|
|
15
|
+
name: "Researcher",
|
|
16
|
+
description: "Run research sprints, grow evidence, write briefs",
|
|
17
|
+
tools: ["wheat"],
|
|
20
18
|
},
|
|
21
19
|
{
|
|
22
|
-
name:
|
|
23
|
-
description:
|
|
24
|
-
tools: [
|
|
20
|
+
name: "Researcher + Dashboard",
|
|
21
|
+
description: "Research sprints with real-time permission dashboard",
|
|
22
|
+
tools: ["wheat", "farmer"],
|
|
25
23
|
},
|
|
26
24
|
{
|
|
27
|
-
name:
|
|
28
|
-
description:
|
|
29
|
-
tools: [
|
|
25
|
+
name: "Team Lead",
|
|
26
|
+
description: "Coordinate multiple sprints, review analytics",
|
|
27
|
+
tools: ["wheat", "farmer", "orchard", "harvest"],
|
|
30
28
|
},
|
|
31
29
|
{
|
|
32
|
-
name:
|
|
33
|
-
description:
|
|
34
|
-
tools: [
|
|
30
|
+
name: "Full Ecosystem",
|
|
31
|
+
description: "Everything. All 7 tools.",
|
|
32
|
+
tools: ["wheat", "farmer", "barn", "mill", "silo", "harvest", "orchard"],
|
|
35
33
|
},
|
|
36
34
|
];
|
|
37
35
|
|
|
@@ -47,24 +45,24 @@ async function run() {
|
|
|
47
45
|
output: process.stdout,
|
|
48
46
|
});
|
|
49
47
|
|
|
50
|
-
console.log(
|
|
51
|
-
console.log(
|
|
52
|
-
console.log(
|
|
53
|
-
console.log(
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(" \x1b[1;33mgrainulation setup\x1b[0m");
|
|
50
|
+
console.log(" What are you trying to do?");
|
|
51
|
+
console.log("");
|
|
54
52
|
|
|
55
53
|
for (let i = 0; i < ROLES.length; i++) {
|
|
56
54
|
const role = ROLES[i];
|
|
57
55
|
console.log(` \x1b[1m${i + 1}.\x1b[0m ${role.name}`);
|
|
58
56
|
console.log(` \x1b[2m${role.description}\x1b[0m`);
|
|
59
|
-
console.log(` Tools: ${role.tools.join(
|
|
60
|
-
console.log(
|
|
57
|
+
console.log(` Tools: ${role.tools.join(", ")}`);
|
|
58
|
+
console.log("");
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
const answer = await ask(rl,
|
|
61
|
+
const answer = await ask(rl, " Choose (1-4): ");
|
|
64
62
|
const choice = parseInt(answer, 10);
|
|
65
63
|
|
|
66
64
|
if (choice < 1 || choice > ROLES.length || isNaN(choice)) {
|
|
67
|
-
console.log(
|
|
65
|
+
console.log("\n \x1b[31mInvalid choice.\x1b[0m\n");
|
|
68
66
|
rl.close();
|
|
69
67
|
return;
|
|
70
68
|
}
|
|
@@ -72,9 +70,9 @@ async function run() {
|
|
|
72
70
|
const role = ROLES[choice - 1];
|
|
73
71
|
const toInstall = [];
|
|
74
72
|
|
|
75
|
-
console.log(
|
|
73
|
+
console.log("");
|
|
76
74
|
console.log(` \x1b[1mSetting up: ${role.name}\x1b[0m`);
|
|
77
|
-
console.log(
|
|
75
|
+
console.log("");
|
|
78
76
|
|
|
79
77
|
for (const toolName of role.tools) {
|
|
80
78
|
const tool = getInstallable().find((t) => t.name === toolName);
|
|
@@ -82,7 +80,9 @@ async function run() {
|
|
|
82
80
|
|
|
83
81
|
const version = getVersion(tool.package);
|
|
84
82
|
if (version) {
|
|
85
|
-
console.log(
|
|
83
|
+
console.log(
|
|
84
|
+
` \x1b[32m\u2713\x1b[0m ${tool.name} already installed (${version})`,
|
|
85
|
+
);
|
|
86
86
|
} else {
|
|
87
87
|
toInstall.push(tool);
|
|
88
88
|
console.log(` \x1b[33m+\x1b[0m ${tool.name} will be installed`);
|
|
@@ -90,40 +90,44 @@ async function run() {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (toInstall.length === 0) {
|
|
93
|
-
console.log(
|
|
94
|
-
console.log(
|
|
95
|
-
console.log(
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log(" \x1b[32mEverything is already installed.\x1b[0m");
|
|
95
|
+
console.log(
|
|
96
|
+
" Run \x1b[1mnpx @grainulation/wheat init\x1b[0m to start a research sprint.",
|
|
97
|
+
);
|
|
96
98
|
rl.close();
|
|
97
99
|
return;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
console.log(
|
|
102
|
+
console.log("");
|
|
101
103
|
const confirm = await ask(
|
|
102
104
|
rl,
|
|
103
|
-
` Install ${toInstall.length} package(s)? (y/N):
|
|
105
|
+
` Install ${toInstall.length} package(s)? (y/N): `,
|
|
104
106
|
);
|
|
105
107
|
|
|
106
|
-
if (confirm.toLowerCase() !==
|
|
107
|
-
console.log(
|
|
108
|
+
if (confirm.toLowerCase() !== "y") {
|
|
109
|
+
console.log("\n \x1b[2mAborted.\x1b[0m\n");
|
|
108
110
|
rl.close();
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
console.log(
|
|
114
|
+
console.log("");
|
|
113
115
|
for (const tool of toInstall) {
|
|
114
116
|
console.log(` Installing ${tool.package}...`);
|
|
115
117
|
try {
|
|
116
|
-
execSync(`npm install -g ${tool.package}`, { stdio:
|
|
118
|
+
execSync(`npm install -g ${tool.package}`, { stdio: "pipe" });
|
|
117
119
|
console.log(` \x1b[32m\u2713\x1b[0m ${tool.name} installed`);
|
|
118
120
|
} catch (err) {
|
|
119
|
-
console.log(
|
|
121
|
+
console.log(
|
|
122
|
+
` \x1b[31m\u2717\x1b[0m ${tool.name} failed: ${err.message}`,
|
|
123
|
+
);
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
console.log(
|
|
124
|
-
console.log(
|
|
125
|
-
console.log(
|
|
126
|
-
console.log(
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log(" \x1b[32mSetup complete.\x1b[0m");
|
|
129
|
+
console.log(" Start with: npx @grainulation/wheat init");
|
|
130
|
+
console.log("");
|
|
127
131
|
rl.close();
|
|
128
132
|
}
|
|
129
133
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grainulation/grainulation",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Structured research for decisions that satisfice",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"workspaces": [
|
|
8
|
+
"packages/*"
|
|
9
|
+
],
|
|
6
10
|
"bin": {
|
|
7
11
|
"grainulation": "bin/grainulation.js"
|
|
8
12
|
},
|
|
@@ -20,7 +24,10 @@
|
|
|
20
24
|
],
|
|
21
25
|
"scripts": {
|
|
22
26
|
"test": "node test/basic.test.js",
|
|
23
|
-
"
|
|
27
|
+
"lint": "biome ci .",
|
|
28
|
+
"format": "biome check --write .",
|
|
29
|
+
"start": "node bin/grainulation.js",
|
|
30
|
+
"postinstall": "git config core.hooksPath .githooks || true"
|
|
24
31
|
},
|
|
25
32
|
"keywords": [
|
|
26
33
|
"research",
|
|
@@ -38,7 +45,11 @@
|
|
|
38
45
|
},
|
|
39
46
|
"homepage": "https://grainulation.com",
|
|
40
47
|
"engines": {
|
|
41
|
-
"node": ">=
|
|
48
|
+
"node": ">=20"
|
|
42
49
|
},
|
|
43
|
-
"
|
|
50
|
+
"packageManager": "pnpm@10.30.0",
|
|
51
|
+
"author": "grainulation contributors",
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@biomejs/biome": "2.4.8"
|
|
54
|
+
}
|
|
44
55
|
}
|