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