@grainulation/grainulation 1.0.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/LICENSE +21 -0
- package/README.md +108 -0
- package/bin/grainulation.js +52 -0
- package/lib/doctor.js +245 -0
- package/lib/ecosystem.js +122 -0
- package/lib/pm.js +237 -0
- package/lib/router.js +586 -0
- package/lib/server.mjs +522 -0
- package/lib/setup.js +130 -0
- package/package.json +44 -0
- package/public/grainulation-tokens.css +321 -0
- package/public/index.html +1139 -0
package/lib/router.js
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('node:child_process');
|
|
4
|
+
const { existsSync } = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { getByName, getInstallable } = require('./ecosystem');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Intent-based router.
|
|
10
|
+
*
|
|
11
|
+
* Given a command or nothing at all, figure out where to send the user.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DELEGATE_COMMANDS = new Set(
|
|
15
|
+
getInstallable().map((t) => t.name)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
function overviewData() {
|
|
19
|
+
const { TOOLS } = require('./ecosystem');
|
|
20
|
+
return {
|
|
21
|
+
name: 'grainulation',
|
|
22
|
+
description: 'Structured research for decisions that satisfice.',
|
|
23
|
+
tools: TOOLS.map((tool) => ({
|
|
24
|
+
name: tool.name,
|
|
25
|
+
package: tool.package,
|
|
26
|
+
role: tool.role,
|
|
27
|
+
category: tool.category,
|
|
28
|
+
installed: isInstalled(tool.package),
|
|
29
|
+
})),
|
|
30
|
+
commands: ['up', 'down', 'ps', 'init', 'status', 'doctor', '<tool>'],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function overview() {
|
|
35
|
+
const { TOOLS } = require('./ecosystem');
|
|
36
|
+
const lines = [
|
|
37
|
+
'',
|
|
38
|
+
' \x1b[1;33mgrainulation\x1b[0m',
|
|
39
|
+
' Structured research for decisions that satisfice.',
|
|
40
|
+
'',
|
|
41
|
+
' \x1b[2mEcosystem:\x1b[0m',
|
|
42
|
+
'',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const tool of TOOLS) {
|
|
46
|
+
const installed = isInstalled(tool.package);
|
|
47
|
+
const marker = installed ? '\x1b[32m+\x1b[0m' : '\x1b[2m-\x1b[0m';
|
|
48
|
+
const port = tool.port ? `:${tool.port}` : '';
|
|
49
|
+
lines.push(` ${marker} \x1b[1m${tool.name.padEnd(12)}\x1b[0m ${tool.role.padEnd(28)} \x1b[2m${port}\x1b[0m`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(' \x1b[2mProcess management:\x1b[0m');
|
|
54
|
+
lines.push(' grainulation up [tools] Start tool servers (default: farmer + wheat)');
|
|
55
|
+
lines.push(' grainulation down Stop all running tools');
|
|
56
|
+
lines.push(' grainulation ps Show running tools, ports, health');
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push(' \x1b[2mWorkflow:\x1b[0m');
|
|
59
|
+
lines.push(' grainulation init Detect context and start a research sprint');
|
|
60
|
+
lines.push(' grainulation status Cross-tool status: sprints, claims, services');
|
|
61
|
+
lines.push(' grainulation doctor Check ecosystem health (install detection)');
|
|
62
|
+
lines.push(' grainulation <tool> Delegate to a grainulation tool');
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push(' \x1b[2mStart here:\x1b[0m');
|
|
65
|
+
lines.push(' grainulation up && grainulation init');
|
|
66
|
+
lines.push('');
|
|
67
|
+
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isInstalled(packageName) {
|
|
72
|
+
try {
|
|
73
|
+
if (packageName === 'grainulation') return true;
|
|
74
|
+
execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, { stdio: 'pipe' });
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
try {
|
|
78
|
+
require.resolve(packageName);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find a source checkout of a tool (sibling directory).
|
|
88
|
+
* Returns the bin path if found, null otherwise.
|
|
89
|
+
*/
|
|
90
|
+
function findSourceBin(tool) {
|
|
91
|
+
const shortName = tool.package.replace(/^@[^/]+\//, '');
|
|
92
|
+
const candidates = [
|
|
93
|
+
path.join(__dirname, '..', '..', shortName),
|
|
94
|
+
path.join(process.cwd(), '..', shortName),
|
|
95
|
+
];
|
|
96
|
+
for (const dir of candidates) {
|
|
97
|
+
try {
|
|
98
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
99
|
+
if (!existsSync(pkgPath)) continue;
|
|
100
|
+
const pkg = JSON.parse(require('node:fs').readFileSync(pkgPath, 'utf-8'));
|
|
101
|
+
if (pkg.name !== tool.package) continue;
|
|
102
|
+
// Find the bin entry
|
|
103
|
+
if (pkg.bin) {
|
|
104
|
+
const binFile = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin)[0];
|
|
105
|
+
if (binFile) {
|
|
106
|
+
const binPath = path.resolve(dir, binFile);
|
|
107
|
+
if (existsSync(binPath)) return binPath;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// skip
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function delegate(toolName, args) {
|
|
118
|
+
const tool = getByName(toolName);
|
|
119
|
+
if (!tool) {
|
|
120
|
+
console.error(`\x1b[31mgrainulation: unknown tool: ${toolName}\x1b[0m`);
|
|
121
|
+
console.error(`Run \x1b[1mgrainulation\x1b[0m to see available tools.`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Prefer source checkout if available (avoids npm registry round-trip)
|
|
126
|
+
const sourceBin = findSourceBin(tool);
|
|
127
|
+
let cmd, cmdArgs;
|
|
128
|
+
if (sourceBin) {
|
|
129
|
+
cmd = process.execPath;
|
|
130
|
+
cmdArgs = [sourceBin, ...args];
|
|
131
|
+
} else {
|
|
132
|
+
cmd = 'npx';
|
|
133
|
+
cmdArgs = [tool.package, ...args];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const child = spawn(cmd, cmdArgs, {
|
|
137
|
+
stdio: 'inherit',
|
|
138
|
+
shell: cmd === 'npx',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
child.on('close', (code) => {
|
|
142
|
+
process.exit(code ?? 0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
child.on('error', (err) => {
|
|
146
|
+
console.error(`\x1b[31mgrainulation: failed to run ${tool.package}: ${err.message}\x1b[0m`);
|
|
147
|
+
console.error(`Try: npm install -g ${tool.package}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Detect project context and route to wheat init.
|
|
154
|
+
* Checks for existing claims.json, package.json, git repo, etc.
|
|
155
|
+
*/
|
|
156
|
+
function init(args, opts) {
|
|
157
|
+
const json = opts && opts.json;
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const hasClaims = existsSync(path.join(cwd, 'claims.json'));
|
|
160
|
+
const hasCompilation = existsSync(path.join(cwd, 'compilation.json'));
|
|
161
|
+
const hasGit = existsSync(path.join(cwd, '.git'));
|
|
162
|
+
const hasPkg = existsSync(path.join(cwd, 'package.json'));
|
|
163
|
+
|
|
164
|
+
if (json) {
|
|
165
|
+
// Pass --json through to wheat init
|
|
166
|
+
if (hasClaims && hasCompilation) {
|
|
167
|
+
console.log(JSON.stringify({ status: 'exists', directory: cwd, hasClaims, hasCompilation }));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (hasClaims) {
|
|
171
|
+
delegate('wheat', ['compile', '--json', ...args]);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const { detect } = require('./doctor');
|
|
175
|
+
const wheatInfo = detect('@grainulation/wheat');
|
|
176
|
+
if (!wheatInfo) {
|
|
177
|
+
console.log(JSON.stringify({ status: 'missing', tool: 'wheat', install: 'npm install -g @grainulation/wheat' }));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
delegate('wheat', ['init', '--json', ...args]);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log(' \x1b[1;33mgrainulation init\x1b[0m');
|
|
186
|
+
console.log('');
|
|
187
|
+
|
|
188
|
+
// Context detection
|
|
189
|
+
console.log(' \x1b[2mDetected context:\x1b[0m');
|
|
190
|
+
console.log(` Directory ${cwd}`);
|
|
191
|
+
console.log(` Git repo ${hasGit ? 'yes' : 'no'}`);
|
|
192
|
+
console.log(` package.json ${hasPkg ? 'yes' : 'no'}`);
|
|
193
|
+
console.log(` claims.json ${hasClaims ? 'yes (existing sprint)' : 'no'}`);
|
|
194
|
+
console.log('');
|
|
195
|
+
|
|
196
|
+
if (hasClaims && hasCompilation) {
|
|
197
|
+
console.log(' An active sprint already exists in this directory.');
|
|
198
|
+
console.log(' To continue, use: grainulation wheat compile');
|
|
199
|
+
console.log(' To start fresh, remove claims.json first.');
|
|
200
|
+
console.log('');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (hasClaims) {
|
|
205
|
+
console.log(' Found claims.json but no compilation. Routing to wheat compile.');
|
|
206
|
+
console.log('');
|
|
207
|
+
delegate('wheat', ['compile', ...args]);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if wheat is available before delegating
|
|
212
|
+
const { detect } = require('./doctor');
|
|
213
|
+
const wheatInfo = detect('@grainulation/wheat');
|
|
214
|
+
if (!wheatInfo) {
|
|
215
|
+
console.log(' wheat is not installed. Install it first:');
|
|
216
|
+
console.log(' npm install -g @grainulation/wheat');
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(' Or run interactively:');
|
|
219
|
+
console.log(' grainulation setup');
|
|
220
|
+
console.log('');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Route to wheat init
|
|
225
|
+
console.log(' Routing to wheat init...');
|
|
226
|
+
console.log('');
|
|
227
|
+
delegate('wheat', ['init', ...args]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Cross-tool status: which tools are running, active sprints, etc.
|
|
232
|
+
*/
|
|
233
|
+
function statusData() {
|
|
234
|
+
const cwd = process.cwd();
|
|
235
|
+
const { detect } = require('./doctor');
|
|
236
|
+
const installable = getInstallable();
|
|
237
|
+
|
|
238
|
+
const tools = [];
|
|
239
|
+
for (const tool of installable) {
|
|
240
|
+
const result = detect(tool.package);
|
|
241
|
+
tools.push({
|
|
242
|
+
name: tool.name,
|
|
243
|
+
package: tool.package,
|
|
244
|
+
installed: !!result,
|
|
245
|
+
version: result ? result.version : null,
|
|
246
|
+
method: result ? result.method : null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const hasClaims = existsSync(path.join(cwd, 'claims.json'));
|
|
251
|
+
const hasCompilation = existsSync(path.join(cwd, 'compilation.json'));
|
|
252
|
+
let sprint = null;
|
|
253
|
+
|
|
254
|
+
if (hasClaims) {
|
|
255
|
+
try {
|
|
256
|
+
const claimsRaw = require('node:fs').readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
|
|
257
|
+
const claims = JSON.parse(claimsRaw);
|
|
258
|
+
const claimList = Array.isArray(claims) ? claims : (claims.claims || []);
|
|
259
|
+
const byType = {};
|
|
260
|
+
for (const c of claimList) {
|
|
261
|
+
const t = c.type || 'unknown';
|
|
262
|
+
byType[t] = (byType[t] || 0) + 1;
|
|
263
|
+
}
|
|
264
|
+
sprint = { claims: claimList.length, byType, compiled: hasCompilation };
|
|
265
|
+
} catch {
|
|
266
|
+
sprint = { error: 'claims.json found but could not be parsed' };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let farmerPidValue = null;
|
|
271
|
+
const farmerPidPath = path.join(cwd, 'dashboard', '.farmer.pid');
|
|
272
|
+
const farmerPidAlt = path.join(cwd, '.farmer.pid');
|
|
273
|
+
for (const pidFile of [farmerPidPath, farmerPidAlt]) {
|
|
274
|
+
if (existsSync(pidFile)) {
|
|
275
|
+
try {
|
|
276
|
+
const pid = require('node:fs').readFileSync(pidFile, 'utf-8').trim();
|
|
277
|
+
process.kill(Number(pid), 0);
|
|
278
|
+
farmerPidValue = Number(pid);
|
|
279
|
+
} catch {
|
|
280
|
+
// not running
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
directory: cwd,
|
|
288
|
+
tools,
|
|
289
|
+
sprint,
|
|
290
|
+
services: { farmer: farmerPidValue },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function status(opts) {
|
|
295
|
+
if (opts && opts.json) {
|
|
296
|
+
console.log(JSON.stringify(statusData()));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const cwd = process.cwd();
|
|
301
|
+
const { TOOLS } = require('./ecosystem');
|
|
302
|
+
const { detect } = require('./doctor');
|
|
303
|
+
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log(' \x1b[1;33mgrainulation status\x1b[0m');
|
|
306
|
+
console.log('');
|
|
307
|
+
|
|
308
|
+
// Installed tools
|
|
309
|
+
console.log(' \x1b[2mInstalled tools:\x1b[0m');
|
|
310
|
+
const installable = getInstallable();
|
|
311
|
+
let installedCount = 0;
|
|
312
|
+
for (const tool of installable) {
|
|
313
|
+
const result = detect(tool.package);
|
|
314
|
+
if (result) {
|
|
315
|
+
installedCount++;
|
|
316
|
+
console.log(` \x1b[32m+\x1b[0m ${tool.name.padEnd(12)} v${result.version} \x1b[2m(${result.method})\x1b[0m`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (installedCount === 0) {
|
|
320
|
+
console.log(' \x1b[2m(none)\x1b[0m');
|
|
321
|
+
}
|
|
322
|
+
console.log('');
|
|
323
|
+
|
|
324
|
+
// Active sprint detection
|
|
325
|
+
console.log(' \x1b[2mCurrent directory:\x1b[0m');
|
|
326
|
+
console.log(` ${cwd}`);
|
|
327
|
+
console.log('');
|
|
328
|
+
|
|
329
|
+
const hasClaims = existsSync(path.join(cwd, 'claims.json'));
|
|
330
|
+
const hasCompilation = existsSync(path.join(cwd, 'compilation.json'));
|
|
331
|
+
|
|
332
|
+
if (hasClaims) {
|
|
333
|
+
try {
|
|
334
|
+
const claimsRaw = require('node:fs').readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
|
|
335
|
+
const claims = JSON.parse(claimsRaw);
|
|
336
|
+
const claimList = Array.isArray(claims) ? claims : (claims.claims || []);
|
|
337
|
+
const total = claimList.length;
|
|
338
|
+
const byType = {};
|
|
339
|
+
for (const c of claimList) {
|
|
340
|
+
const t = c.type || 'unknown';
|
|
341
|
+
byType[t] = (byType[t] || 0) + 1;
|
|
342
|
+
}
|
|
343
|
+
console.log(' \x1b[2mActive sprint:\x1b[0m');
|
|
344
|
+
console.log(` Claims ${total}`);
|
|
345
|
+
const typeStr = Object.entries(byType)
|
|
346
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
347
|
+
.join(', ');
|
|
348
|
+
if (typeStr) {
|
|
349
|
+
console.log(` Breakdown ${typeStr}`);
|
|
350
|
+
}
|
|
351
|
+
if (hasCompilation) {
|
|
352
|
+
console.log(' Compiled yes');
|
|
353
|
+
} else {
|
|
354
|
+
console.log(' Compiled no (run: grainulation wheat compile)');
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
console.log(' \x1b[2mActive sprint:\x1b[0m');
|
|
358
|
+
console.log(' claims.json found but could not be parsed');
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
console.log(' \x1b[2mNo active sprint in this directory.\x1b[0m');
|
|
362
|
+
console.log(' Start one with: grainulation init');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check for running farmer (look for .farmer.pid)
|
|
366
|
+
const farmerPid = path.join(cwd, 'dashboard', '.farmer.pid');
|
|
367
|
+
const farmerPidAlt = path.join(cwd, '.farmer.pid');
|
|
368
|
+
let farmerRunning = false;
|
|
369
|
+
for (const pidFile of [farmerPid, farmerPidAlt]) {
|
|
370
|
+
if (existsSync(pidFile)) {
|
|
371
|
+
try {
|
|
372
|
+
const pid = require('node:fs').readFileSync(pidFile, 'utf-8').trim();
|
|
373
|
+
// Check if process is still running
|
|
374
|
+
process.kill(Number(pid), 0);
|
|
375
|
+
farmerRunning = true;
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(' \x1b[2mRunning services:\x1b[0m');
|
|
378
|
+
console.log(` farmer pid ${pid}`);
|
|
379
|
+
} catch {
|
|
380
|
+
// PID file exists but process is not running
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!farmerRunning) {
|
|
387
|
+
console.log('');
|
|
388
|
+
console.log(' \x1b[2mNo running services detected.\x1b[0m');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log('');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function route(args, opts) {
|
|
395
|
+
const command = args[0];
|
|
396
|
+
const rest = args.slice(1);
|
|
397
|
+
const json = opts && opts.json;
|
|
398
|
+
|
|
399
|
+
// No args — show overview
|
|
400
|
+
if (!command) {
|
|
401
|
+
if (json) {
|
|
402
|
+
console.log(JSON.stringify(overviewData()));
|
|
403
|
+
} else {
|
|
404
|
+
console.log(overview());
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Built-in commands
|
|
410
|
+
if (command === 'doctor') {
|
|
411
|
+
require('./doctor').run({ json });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (command === 'setup') {
|
|
415
|
+
require('./setup').run();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (command === 'init') {
|
|
419
|
+
init(rest, { json });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (command === 'status') {
|
|
423
|
+
status({ json });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Process management
|
|
428
|
+
if (command === 'up') {
|
|
429
|
+
pmUp(rest, { json });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (command === 'down') {
|
|
433
|
+
pmDown(rest, { json });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (command === 'ps') {
|
|
437
|
+
pmPs({ json });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
441
|
+
if (json) {
|
|
442
|
+
console.log(JSON.stringify(overviewData()));
|
|
443
|
+
} else {
|
|
444
|
+
console.log(overview());
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (command === '--version' || command === '-v') {
|
|
449
|
+
const pkg = require('../package.json');
|
|
450
|
+
if (json) {
|
|
451
|
+
console.log(JSON.stringify({ version: pkg.version }));
|
|
452
|
+
} else {
|
|
453
|
+
console.log(`grainulation v${pkg.version}`);
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Delegate to a tool — pass --json through
|
|
459
|
+
if (DELEGATE_COMMANDS.has(command)) {
|
|
460
|
+
const delegateArgs = json ? ['--json', ...rest] : rest;
|
|
461
|
+
delegate(command, delegateArgs);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Unknown
|
|
466
|
+
if (json) {
|
|
467
|
+
console.log(JSON.stringify({ error: `unknown command: ${command}` }));
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
console.error(`\x1b[31mgrainulation: unknown command: ${command}\x1b[0m`);
|
|
471
|
+
console.log(overview());
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// --- Process management commands ---
|
|
476
|
+
|
|
477
|
+
function pmUp(args, opts) {
|
|
478
|
+
const pm = require('./pm');
|
|
479
|
+
const toolNames = args.filter(a => !a.startsWith('-'));
|
|
480
|
+
const results = pm.up(toolNames.length > 0 ? toolNames : undefined);
|
|
481
|
+
|
|
482
|
+
if (opts && opts.json) {
|
|
483
|
+
console.log(JSON.stringify(results));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log('');
|
|
488
|
+
console.log(' \x1b[1;33mgrainulation up\x1b[0m');
|
|
489
|
+
console.log('');
|
|
490
|
+
|
|
491
|
+
for (const r of results) {
|
|
492
|
+
if (r.error) {
|
|
493
|
+
console.log(` \x1b[31mx\x1b[0m ${r.name.padEnd(12)} ${r.error}`);
|
|
494
|
+
} else if (r.alreadyRunning) {
|
|
495
|
+
console.log(` \x1b[33m~\x1b[0m ${r.name.padEnd(12)} already running (pid ${r.pid}, port ${r.port})`);
|
|
496
|
+
} else {
|
|
497
|
+
console.log(` \x1b[32m+\x1b[0m ${r.name.padEnd(12)} started (pid ${r.pid}, port ${r.port})`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
console.log('');
|
|
502
|
+
|
|
503
|
+
// Wait a moment then probe health
|
|
504
|
+
setTimeout(async () => {
|
|
505
|
+
const statuses = await pm.ps();
|
|
506
|
+
const running = statuses.filter(s => s.alive);
|
|
507
|
+
if (running.length > 0) {
|
|
508
|
+
console.log(' \x1b[2mHealth check:\x1b[0m');
|
|
509
|
+
for (const s of running) {
|
|
510
|
+
console.log(` \x1b[32m+\x1b[0m ${s.name.padEnd(12)} :${s.port} (${s.latencyMs}ms)`);
|
|
511
|
+
}
|
|
512
|
+
console.log('');
|
|
513
|
+
}
|
|
514
|
+
}, 2000);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function pmDown(args, opts) {
|
|
518
|
+
const pm = require('./pm');
|
|
519
|
+
const toolNames = args.filter(a => !a.startsWith('-'));
|
|
520
|
+
const results = pm.down(toolNames.length > 0 ? toolNames : undefined);
|
|
521
|
+
|
|
522
|
+
if (opts && opts.json) {
|
|
523
|
+
console.log(JSON.stringify(results));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
console.log('');
|
|
528
|
+
console.log(' \x1b[1;33mgrainulation down\x1b[0m');
|
|
529
|
+
console.log('');
|
|
530
|
+
|
|
531
|
+
let stoppedAny = false;
|
|
532
|
+
for (const r of results) {
|
|
533
|
+
if (r.stopped) {
|
|
534
|
+
stoppedAny = true;
|
|
535
|
+
console.log(` \x1b[31m-\x1b[0m ${r.name.padEnd(12)} stopped (pid ${r.pid})`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!stoppedAny) {
|
|
540
|
+
console.log(' \x1b[2mNo running tools to stop.\x1b[0m');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
console.log('');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function pmPs(opts) {
|
|
547
|
+
const pm = require('./pm');
|
|
548
|
+
const statuses = await pm.ps();
|
|
549
|
+
|
|
550
|
+
if (opts && opts.json) {
|
|
551
|
+
console.log(JSON.stringify(statuses));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log(' \x1b[1;33mgrainulation ps\x1b[0m');
|
|
557
|
+
console.log('');
|
|
558
|
+
|
|
559
|
+
const running = statuses.filter(s => s.alive);
|
|
560
|
+
const stopped = statuses.filter(s => !s.alive);
|
|
561
|
+
|
|
562
|
+
if (running.length > 0) {
|
|
563
|
+
console.log(' \x1b[2mRunning:\x1b[0m');
|
|
564
|
+
for (const s of running) {
|
|
565
|
+
const pidStr = s.pid ? `pid ${s.pid}` : 'unknown pid';
|
|
566
|
+
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}`);
|
|
568
|
+
}
|
|
569
|
+
console.log('');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (stopped.length > 0) {
|
|
573
|
+
console.log(' \x1b[2mStopped:\x1b[0m');
|
|
574
|
+
for (const s of stopped) {
|
|
575
|
+
console.log(` \x1b[2m- ${s.name.padEnd(12)} :${s.port}\x1b[0m`);
|
|
576
|
+
}
|
|
577
|
+
console.log('');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (running.length === 0) {
|
|
581
|
+
console.log(' \x1b[2mNo tools running. Start with: grainulation up\x1b[0m');
|
|
582
|
+
console.log('');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
module.exports = { route, overview, overviewData, isInstalled, delegate, init, status, statusData, pmUp, pmDown, pmPs };
|