@grainulation/grainulation 1.0.1 → 1.1.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 +1 -1
- package/bin/grainulation.js +13 -14
- package/lib/doctor.js +67 -105
- package/lib/ecosystem.js +57 -65
- package/lib/pm.js +44 -69
- package/lib/router.js +179 -215
- package/lib/server.mjs +33 -38
- package/lib/setup.js +42 -51
- package/package.json +2 -4
- package/public/index.html +1 -1
package/lib/server.mjs
CHANGED
|
@@ -11,17 +11,8 @@
|
|
|
11
11
|
* grainulation serve [--port 9098]
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
existsSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
readdirSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
renameSync,
|
|
21
|
-
statSync,
|
|
22
|
-
watchFile,
|
|
23
|
-
writeFileSync,
|
|
24
|
-
} from 'node:fs';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
25
16
|
import { createServer } from 'node:http';
|
|
26
17
|
import { createRequire } from 'node:module';
|
|
27
18
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
@@ -185,8 +176,11 @@ function detectTool(pkg) {
|
|
|
185
176
|
|
|
186
177
|
// 1. Global npm
|
|
187
178
|
try {
|
|
188
|
-
const out =
|
|
189
|
-
|
|
179
|
+
const out = execFileSync('npm', ['list', '-g', pkg, '--depth=0'], {
|
|
180
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
181
|
+
encoding: 'utf-8',
|
|
182
|
+
});
|
|
183
|
+
const match = out.match(new RegExp(`${escapeRegex(pkg)}@(\\S+)`));
|
|
190
184
|
if (match) return { installed: true, version: match[1], method: 'global' };
|
|
191
185
|
} catch {
|
|
192
186
|
/* not found */
|
|
@@ -194,7 +188,10 @@ function detectTool(pkg) {
|
|
|
194
188
|
|
|
195
189
|
// 2. npx cache
|
|
196
190
|
try {
|
|
197
|
-
const prefix =
|
|
191
|
+
const prefix = execFileSync('npm', ['config', 'get', 'cache'], {
|
|
192
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
193
|
+
encoding: 'utf-8',
|
|
194
|
+
}).trim();
|
|
198
195
|
const npxDir = join(prefix, '_npx');
|
|
199
196
|
if (existsSync(npxDir)) {
|
|
200
197
|
const entries = readdirSync(npxDir, { withFileTypes: true });
|
|
@@ -251,7 +248,7 @@ function runDoctor() {
|
|
|
251
248
|
const nodeVersion = process.version;
|
|
252
249
|
let npmVersion = 'unknown';
|
|
253
250
|
try {
|
|
254
|
-
npmVersion =
|
|
251
|
+
npmVersion = execFileSync('npm', ['--version'], { stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf-8' }).trim();
|
|
255
252
|
} catch {
|
|
256
253
|
/* ignore */
|
|
257
254
|
}
|
|
@@ -261,7 +258,7 @@ function runDoctor() {
|
|
|
261
258
|
// Environment checks
|
|
262
259
|
checks.push({
|
|
263
260
|
name: 'Node.js',
|
|
264
|
-
status: parseInt(nodeVersion.slice(1)) >= 18 ? 'pass' : 'warning',
|
|
261
|
+
status: parseInt(nodeVersion.slice(1), 10) >= 18 ? 'pass' : 'warning',
|
|
265
262
|
detail: nodeVersion,
|
|
266
263
|
category: 'environment',
|
|
267
264
|
});
|
|
@@ -334,16 +331,15 @@ function scaffold(targetDir, options = {}) {
|
|
|
334
331
|
mkdirSync(dir, { recursive: true });
|
|
335
332
|
|
|
336
333
|
// claims.json (atomic write-then-rename)
|
|
337
|
-
const claimsData =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const tmpClaims = join(dir, 'claims.json.tmp.' + process.pid);
|
|
334
|
+
const claimsData = `${JSON.stringify(
|
|
335
|
+
{
|
|
336
|
+
claims: [],
|
|
337
|
+
meta: { created: new Date().toISOString(), tool: 'grainulation' },
|
|
338
|
+
},
|
|
339
|
+
null,
|
|
340
|
+
2,
|
|
341
|
+
)}\n`;
|
|
342
|
+
const tmpClaims = join(dir, `claims.json.tmp.${process.pid}`);
|
|
347
343
|
writeFileSync(tmpClaims, claimsData);
|
|
348
344
|
renameSync(tmpClaims, join(dir, 'claims.json'));
|
|
349
345
|
|
|
@@ -356,16 +352,15 @@ function scaffold(targetDir, options = {}) {
|
|
|
356
352
|
|
|
357
353
|
// orchard.json (if multi-sprint, atomic write-then-rename)
|
|
358
354
|
if (options.includeOrchard) {
|
|
359
|
-
const orchardData =
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const tmpOrchard = join(dir, 'orchard.json.tmp.' + process.pid);
|
|
355
|
+
const orchardData = `${JSON.stringify(
|
|
356
|
+
{
|
|
357
|
+
sprints: [],
|
|
358
|
+
settings: { sync_interval: 'manual' },
|
|
359
|
+
},
|
|
360
|
+
null,
|
|
361
|
+
2,
|
|
362
|
+
)}\n`;
|
|
363
|
+
const tmpOrchard = join(dir, `orchard.json.tmp.${process.pid}`);
|
|
369
364
|
writeFileSync(tmpOrchard, orchardData);
|
|
370
365
|
renameSync(tmpOrchard, join(dir, 'orchard.json'));
|
|
371
366
|
}
|
|
@@ -443,7 +438,7 @@ table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border-bottom:1
|
|
|
443
438
|
th{color:#9ca3af}code{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:13px}</style></head>
|
|
444
439
|
<body><h1>grainulation API</h1><p>${ROUTES.length} endpoints</p>
|
|
445
440
|
<table><tr><th>Method</th><th>Path</th><th>Description</th></tr>
|
|
446
|
-
${ROUTES.map((r) =>
|
|
441
|
+
${ROUTES.map((r) => `<tr><td><code>${r.method}</code></td><td><code>${r.path}</code></td><td>${r.description}</td></tr>`).join('')}
|
|
447
442
|
</table></body></html>`;
|
|
448
443
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
449
444
|
res.end(html);
|
|
@@ -588,7 +583,7 @@ ${ROUTES.map((r) => '<tr><td><code>' + r.method + '</code></td><td><code>' + r.p
|
|
|
588
583
|
|
|
589
584
|
// ── Static files ──
|
|
590
585
|
const filePath = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
591
|
-
const resolved = resolve(PUBLIC_DIR,
|
|
586
|
+
const resolved = resolve(PUBLIC_DIR, `.${filePath}`);
|
|
592
587
|
|
|
593
588
|
if (!resolved.startsWith(PUBLIC_DIR)) {
|
|
594
589
|
res.writeHead(403);
|
package/lib/setup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const readline = require(
|
|
2
|
-
const {
|
|
3
|
-
const { getInstallable, getCategories } = require(
|
|
4
|
-
const { getVersion } = require(
|
|
1
|
+
const readline = require('node:readline');
|
|
2
|
+
const { execFileSync } = require('node:child_process');
|
|
3
|
+
const { getInstallable, getCategories } = require('./ecosystem');
|
|
4
|
+
const { getVersion } = require('./doctor');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Interactive setup wizard.
|
|
@@ -12,24 +12,24 @@ const { getVersion } = require("./doctor");
|
|
|
12
12
|
|
|
13
13
|
const ROLES = [
|
|
14
14
|
{
|
|
15
|
-
name:
|
|
16
|
-
description:
|
|
17
|
-
tools: [
|
|
15
|
+
name: 'Researcher',
|
|
16
|
+
description: 'Run research sprints, grow evidence, write briefs',
|
|
17
|
+
tools: ['wheat'],
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
name:
|
|
21
|
-
description:
|
|
22
|
-
tools: [
|
|
20
|
+
name: 'Researcher + Dashboard',
|
|
21
|
+
description: 'Research sprints with real-time permission dashboard',
|
|
22
|
+
tools: ['wheat', 'farmer'],
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
name:
|
|
26
|
-
description:
|
|
27
|
-
tools: [
|
|
25
|
+
name: 'Team Lead',
|
|
26
|
+
description: 'Coordinate multiple sprints, review analytics',
|
|
27
|
+
tools: ['wheat', 'farmer', 'orchard', 'harvest'],
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
name:
|
|
31
|
-
description:
|
|
32
|
-
tools: [
|
|
30
|
+
name: 'Full Ecosystem',
|
|
31
|
+
description: 'Everything. All 7 tools.',
|
|
32
|
+
tools: ['wheat', 'farmer', 'barn', 'mill', 'silo', 'harvest', 'orchard'],
|
|
33
33
|
},
|
|
34
34
|
];
|
|
35
35
|
|
|
@@ -45,24 +45,24 @@ async function run() {
|
|
|
45
45
|
output: process.stdout,
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
console.log(
|
|
49
|
-
console.log(
|
|
50
|
-
console.log(
|
|
51
|
-
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('');
|
|
52
52
|
|
|
53
53
|
for (let i = 0; i < ROLES.length; i++) {
|
|
54
54
|
const role = ROLES[i];
|
|
55
55
|
console.log(` \x1b[1m${i + 1}.\x1b[0m ${role.name}`);
|
|
56
56
|
console.log(` \x1b[2m${role.description}\x1b[0m`);
|
|
57
|
-
console.log(` Tools: ${role.tools.join(
|
|
58
|
-
console.log(
|
|
57
|
+
console.log(` Tools: ${role.tools.join(', ')}`);
|
|
58
|
+
console.log('');
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const answer = await ask(rl,
|
|
61
|
+
const answer = await ask(rl, ' Choose (1-4): ');
|
|
62
62
|
const choice = parseInt(answer, 10);
|
|
63
63
|
|
|
64
|
-
if (choice < 1 || choice > ROLES.length || isNaN(choice)) {
|
|
65
|
-
console.log(
|
|
64
|
+
if (choice < 1 || choice > ROLES.length || Number.isNaN(choice)) {
|
|
65
|
+
console.log('\n \x1b[31mInvalid choice.\x1b[0m\n');
|
|
66
66
|
rl.close();
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
@@ -70,9 +70,9 @@ async function run() {
|
|
|
70
70
|
const role = ROLES[choice - 1];
|
|
71
71
|
const toInstall = [];
|
|
72
72
|
|
|
73
|
-
console.log(
|
|
73
|
+
console.log('');
|
|
74
74
|
console.log(` \x1b[1mSetting up: ${role.name}\x1b[0m`);
|
|
75
|
-
console.log(
|
|
75
|
+
console.log('');
|
|
76
76
|
|
|
77
77
|
for (const toolName of role.tools) {
|
|
78
78
|
const tool = getInstallable().find((t) => t.name === toolName);
|
|
@@ -80,9 +80,7 @@ async function run() {
|
|
|
80
80
|
|
|
81
81
|
const version = getVersion(tool.package);
|
|
82
82
|
if (version) {
|
|
83
|
-
console.log(
|
|
84
|
-
` \x1b[32m\u2713\x1b[0m ${tool.name} already installed (${version})`,
|
|
85
|
-
);
|
|
83
|
+
console.log(` \x1b[32m\u2713\x1b[0m ${tool.name} already installed (${version})`);
|
|
86
84
|
} else {
|
|
87
85
|
toInstall.push(tool);
|
|
88
86
|
console.log(` \x1b[33m+\x1b[0m ${tool.name} will be installed`);
|
|
@@ -90,44 +88,37 @@ async function run() {
|
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
if (toInstall.length === 0) {
|
|
93
|
-
console.log(
|
|
94
|
-
console.log(
|
|
95
|
-
console.log(
|
|
96
|
-
" Run \x1b[1mnpx @grainulation/wheat init\x1b[0m to start a research sprint.",
|
|
97
|
-
);
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(' \x1b[32mEverything is already installed.\x1b[0m');
|
|
93
|
+
console.log(' Run \x1b[1mnpx @grainulation/wheat init\x1b[0m to start a research sprint.');
|
|
98
94
|
rl.close();
|
|
99
95
|
return;
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
console.log(
|
|
103
|
-
const confirm = await ask(
|
|
104
|
-
rl,
|
|
105
|
-
` Install ${toInstall.length} package(s)? (y/N): `,
|
|
106
|
-
);
|
|
98
|
+
console.log('');
|
|
99
|
+
const confirm = await ask(rl, ` Install ${toInstall.length} package(s)? (y/N): `);
|
|
107
100
|
|
|
108
|
-
if (confirm.toLowerCase() !==
|
|
109
|
-
console.log(
|
|
101
|
+
if (confirm.toLowerCase() !== 'y') {
|
|
102
|
+
console.log('\n \x1b[2mAborted.\x1b[0m\n');
|
|
110
103
|
rl.close();
|
|
111
104
|
return;
|
|
112
105
|
}
|
|
113
106
|
|
|
114
|
-
console.log(
|
|
107
|
+
console.log('');
|
|
115
108
|
for (const tool of toInstall) {
|
|
116
109
|
console.log(` Installing ${tool.package}...`);
|
|
117
110
|
try {
|
|
118
|
-
|
|
111
|
+
execFileSync('npm', ['install', '-g', tool.package], { stdio: 'pipe' });
|
|
119
112
|
console.log(` \x1b[32m\u2713\x1b[0m ${tool.name} installed`);
|
|
120
113
|
} catch (err) {
|
|
121
|
-
console.log(
|
|
122
|
-
` \x1b[31m\u2717\x1b[0m ${tool.name} failed: ${err.message}`,
|
|
123
|
-
);
|
|
114
|
+
console.log(` \x1b[31m\u2717\x1b[0m ${tool.name} failed: ${err.message}`);
|
|
124
115
|
}
|
|
125
116
|
}
|
|
126
117
|
|
|
127
|
-
console.log(
|
|
128
|
-
console.log(
|
|
129
|
-
console.log(
|
|
130
|
-
console.log(
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(' \x1b[32mSetup complete.\x1b[0m');
|
|
120
|
+
console.log(' Start with: npx @grainulation/wheat init');
|
|
121
|
+
console.log('');
|
|
131
122
|
rl.close();
|
|
132
123
|
}
|
|
133
124
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grainulation/grainulation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Structured research for decisions that satisfice",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
6
|
"workspaces": [
|
|
8
7
|
"packages/*"
|
|
9
8
|
],
|
|
@@ -26,8 +25,7 @@
|
|
|
26
25
|
"test": "node test/basic.test.js",
|
|
27
26
|
"lint": "biome ci .",
|
|
28
27
|
"format": "biome check --write .",
|
|
29
|
-
"start": "node bin/grainulation.js"
|
|
30
|
-
"postinstall": "git config core.hooksPath .githooks || true"
|
|
28
|
+
"start": "node bin/grainulation.js"
|
|
31
29
|
},
|
|
32
30
|
"keywords": [
|
|
33
31
|
"research",
|
package/public/index.html
CHANGED
|
@@ -440,7 +440,7 @@ body {
|
|
|
440
440
|
</main>
|
|
441
441
|
</div>
|
|
442
442
|
<footer class="footer">
|
|
443
|
-
<span>grainulation v1.0.
|
|
443
|
+
<span>grainulation v1.0.2 -- @grainulation/grainulation</span>
|
|
444
444
|
<div class="footer-links">
|
|
445
445
|
<a href="http://localhost:9091">wheat</a>
|
|
446
446
|
<a href="http://localhost:9090">farmer</a>
|