@grafema/cli 0.3.24 → 0.3.28

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.
Files changed (48) hide show
  1. package/README.md +59 -45
  2. package/dist/cli.js +10 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/analyzeAction.d.ts.map +1 -1
  5. package/dist/commands/analyzeAction.js +134 -3
  6. package/dist/commands/analyzeAction.js.map +1 -1
  7. package/dist/commands/doctor/checks.d.ts.map +1 -1
  8. package/dist/commands/doctor/checks.js +7 -3
  9. package/dist/commands/doctor/checks.js.map +1 -1
  10. package/dist/commands/export.d.ts +15 -0
  11. package/dist/commands/export.d.ts.map +1 -0
  12. package/dist/commands/export.js +88 -0
  13. package/dist/commands/export.js.map +1 -0
  14. package/dist/commands/exportAction.d.ts +35 -0
  15. package/dist/commands/exportAction.d.ts.map +1 -0
  16. package/dist/commands/exportAction.js +58 -0
  17. package/dist/commands/exportAction.js.map +1 -0
  18. package/dist/commands/features.d.ts +13 -0
  19. package/dist/commands/features.d.ts.map +1 -0
  20. package/dist/commands/features.js +69 -0
  21. package/dist/commands/features.js.map +1 -0
  22. package/dist/commands/featuresAction.d.ts +82 -0
  23. package/dist/commands/featuresAction.d.ts.map +1 -0
  24. package/dist/commands/featuresAction.js +139 -0
  25. package/dist/commands/featuresAction.js.map +1 -0
  26. package/dist/commands/start.d.ts +12 -0
  27. package/dist/commands/start.d.ts.map +1 -0
  28. package/dist/commands/start.js +294 -0
  29. package/dist/commands/start.js.map +1 -0
  30. package/dist/commands/trace.d.ts.map +1 -1
  31. package/dist/commands/trace.js +50 -30
  32. package/dist/commands/trace.js.map +1 -1
  33. package/dist/commands/upgrade.d.ts +3 -0
  34. package/dist/commands/upgrade.d.ts.map +1 -0
  35. package/dist/commands/upgrade.js +279 -0
  36. package/dist/commands/upgrade.js.map +1 -0
  37. package/package.json +8 -8
  38. package/src/cli.ts +11 -0
  39. package/src/commands/analyzeAction.ts +135 -2
  40. package/src/commands/doctor/checks.ts +4 -3
  41. package/src/commands/explore.tsx +29 -2
  42. package/src/commands/export.ts +102 -0
  43. package/src/commands/exportAction.ts +107 -0
  44. package/src/commands/features.ts +88 -0
  45. package/src/commands/featuresAction.ts +218 -0
  46. package/src/commands/start.ts +303 -0
  47. package/src/commands/trace.ts +49 -29
  48. package/src/commands/upgrade.ts +310 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * `grafema start` — unified command to bring up the entire Grafema stack.
3
+ *
4
+ * Starts RFDB server (with HTTP) and prints connection info.
5
+ * Foreground by default; `--background` detaches.
6
+ *
7
+ * `grafema stop` — graceful shutdown of everything.
8
+ */
9
+
10
+ import { Command } from 'commander';
11
+ import { resolve, join, dirname } from 'path';
12
+ import { existsSync, mkdirSync, unlinkSync, readFileSync } from 'fs';
13
+ import { spawn, type ChildProcess } from 'child_process';
14
+ import { setTimeout as sleep } from 'timers/promises';
15
+ import { RFDBClient, findRfdbBinary, loadConfig, startRfdbServer } from '@grafema/util';
16
+ import { exitWithError } from '../utils/errorFormatter.js';
17
+
18
+ interface StartOptions {
19
+ project: string;
20
+ binary?: string;
21
+ background?: boolean;
22
+ httpPort?: string;
23
+ }
24
+
25
+ function getProjectPaths(projectPath: string) {
26
+ const grafemaDir = join(projectPath, '.grafema');
27
+ const socketPath = join(grafemaDir, 'rfdb.sock');
28
+ const dbPath = join(grafemaDir, 'graph.rfdb');
29
+ const pidPath = join(grafemaDir, 'rfdb.pid');
30
+ const httpPortFile = join(grafemaDir, 'rfdb-http.port');
31
+ const logFile = join(grafemaDir, 'rfdb.log');
32
+ return { grafemaDir, socketPath, dbPath, pidPath, httpPortFile, logFile };
33
+ }
34
+
35
+ function resolveBinaryPath(projectPath: string, explicitBinary?: string): string | null {
36
+ if (explicitBinary) {
37
+ return findRfdbBinary({ explicitPath: explicitBinary });
38
+ }
39
+ try {
40
+ const config = loadConfig(projectPath);
41
+ const serverConfig = (config as unknown as { server?: { binaryPath?: string } }).server;
42
+ if (serverConfig?.binaryPath) {
43
+ return findRfdbBinary({ explicitPath: serverConfig.binaryPath });
44
+ }
45
+ } catch { /* continue */ }
46
+ return findRfdbBinary();
47
+ }
48
+
49
+ async function isServerRunning(socketPath: string): Promise<{ running: boolean; version?: string }> {
50
+ if (!existsSync(socketPath)) return { running: false };
51
+ const client = new RFDBClient(socketPath, 'cli');
52
+ client.on('error', () => {});
53
+ try {
54
+ await client.connect();
55
+ const version = await client.ping();
56
+ await client.close();
57
+ return { running: true, version: version || undefined };
58
+ } catch {
59
+ return { running: false };
60
+ }
61
+ }
62
+
63
+ async function waitForHttpPort(httpPortFile: string, timeoutMs: number): Promise<number | null> {
64
+ const start = Date.now();
65
+ while (Date.now() - start < timeoutMs) {
66
+ if (existsSync(httpPortFile)) {
67
+ try {
68
+ const port = parseInt(readFileSync(httpPortFile, 'utf-8').trim(), 10);
69
+ if (port > 0) return port;
70
+ } catch { /* retry */ }
71
+ }
72
+ await sleep(100);
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function printBanner(socketPath: string, httpPort: number | null, pid: number | null, version: string | null, logFile: string | null) {
78
+ console.log('');
79
+ console.log(' Grafema is running');
80
+ console.log('');
81
+ console.log(` RFDB socket : ${socketPath}`);
82
+ if (httpPort) {
83
+ console.log(` HTTP : http://localhost:${httpPort}`);
84
+ console.log(` UI : http://localhost:${httpPort}/ui`);
85
+ }
86
+ if (version) {
87
+ console.log(` Version : ${version}`);
88
+ }
89
+ if (pid) {
90
+ console.log(` PID : ${pid}`);
91
+ }
92
+ if (logFile) {
93
+ console.log(` Log : ${logFile}`);
94
+ }
95
+ console.log('');
96
+ }
97
+
98
+ // ─── grafema start ───────────────────────────────────────────────────────────
99
+
100
+ export const startCommand = new Command('start')
101
+ .description('Start Grafema (RFDB server + HTTP)')
102
+ .option('-p, --project <path>', 'Project path', '.')
103
+ .option('-b, --binary <path>', 'Path to rfdb-server binary')
104
+ .option('--background', 'Run in background (detached)')
105
+ .option('--http-port <port>', 'HTTP port (0 = auto-assign)', '0')
106
+ .action(async (options: StartOptions) => {
107
+ const projectPath = resolve(options.project);
108
+ const { grafemaDir, socketPath, dbPath, pidPath, httpPortFile, logFile } = getProjectPaths(projectPath);
109
+
110
+ if (!existsSync(grafemaDir)) {
111
+ mkdirSync(grafemaDir, { recursive: true });
112
+ }
113
+
114
+ // Already running?
115
+ const status = await isServerRunning(socketPath);
116
+ if (status.running) {
117
+ const httpPort = existsSync(httpPortFile)
118
+ ? parseInt(readFileSync(httpPortFile, 'utf-8').trim(), 10) || null
119
+ : null;
120
+
121
+ let pid: number | null = null;
122
+ if (existsSync(pidPath)) {
123
+ try { pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10); } catch { /* ignore */ }
124
+ }
125
+
126
+ printBanner(socketPath, httpPort, pid, status.version || null, existsSync(logFile) ? logFile : null);
127
+ console.log(' Already running. Use `grafema stop` to shut down.');
128
+ return;
129
+ }
130
+
131
+ const binaryPath = resolveBinaryPath(projectPath, options.binary);
132
+ if (!binaryPath) {
133
+ exitWithError('RFDB server binary not found', [
134
+ 'Install: npm install @grafema/rfdb',
135
+ 'Or build: cargo build --release -p rfdb',
136
+ 'Or specify: grafema start --binary /path/to/rfdb-server',
137
+ ]);
138
+ }
139
+
140
+ const httpPort = options.httpPort ?? '0';
141
+ const dataDir = dirname(socketPath);
142
+
143
+ // ─── Background mode ───────────────────────────────────────────────
144
+
145
+ if (options.background) {
146
+ // Remove stale HTTP port lockfile so we don't read a previous port
147
+ if (existsSync(httpPortFile)) {
148
+ try { unlinkSync(httpPortFile); } catch { /* ignore */ }
149
+ }
150
+
151
+ const serverProcess = await startRfdbServer({
152
+ dbPath,
153
+ socketPath,
154
+ binaryPath,
155
+ pidPath,
156
+ waitTimeoutMs: 30_000,
157
+ extraArgs: ['--http-port', httpPort],
158
+ });
159
+
160
+ if (serverProcess === null) {
161
+ console.log('Server already running (detected via PID file)');
162
+ return;
163
+ }
164
+
165
+ const verifyStatus = await isServerRunning(socketPath);
166
+ if (!verifyStatus.running) {
167
+ exitWithError('Server started but not responding', [
168
+ 'Check server logs for errors',
169
+ ]);
170
+ }
171
+
172
+ const actualHttpPort = await waitForHttpPort(httpPortFile, 15_000);
173
+
174
+ printBanner(socketPath, actualHttpPort, serverProcess.pid ?? null, verifyStatus.version || null, logFile);
175
+ return;
176
+ }
177
+
178
+ // ─── Foreground mode (default) ─────────────────────────────────────
179
+
180
+ if (existsSync(socketPath)) {
181
+ try { unlinkSync(socketPath); } catch { /* ignore */ }
182
+ }
183
+ if (existsSync(httpPortFile)) {
184
+ try { unlinkSync(httpPortFile); } catch { /* ignore */ }
185
+ }
186
+
187
+ const args = [dbPath, '--socket', socketPath, '--data-dir', dataDir, '--http-port', httpPort];
188
+
189
+ const child: ChildProcess = spawn(binaryPath, args, {
190
+ stdio: ['ignore', 'inherit', 'pipe'],
191
+ });
192
+
193
+ // Buffer stderr until banner is printed, then forward
194
+ let bannerPrinted = false;
195
+ const stderrLines: string[] = [];
196
+
197
+ child.stderr?.setEncoding('utf-8');
198
+ child.stderr?.on('data', (chunk: string) => {
199
+ for (const line of chunk.split('\n')) {
200
+ if (!line) continue;
201
+ if (bannerPrinted) {
202
+ process.stderr.write(line + '\n');
203
+ } else {
204
+ stderrLines.push(line);
205
+ }
206
+ }
207
+ });
208
+
209
+ const shutdown = () => {
210
+ process.stderr.write('\nStopping Grafema...\n');
211
+ child.kill('SIGTERM');
212
+ };
213
+ process.on('SIGINT', shutdown);
214
+ process.on('SIGTERM', shutdown);
215
+
216
+ // Wait for socket
217
+ const deadline = Date.now() + 30_000;
218
+ while (!existsSync(socketPath) && Date.now() < deadline) {
219
+ await sleep(100);
220
+ if (child.exitCode !== null) {
221
+ for (const line of stderrLines) process.stderr.write(line + '\n');
222
+ exitWithError('Server exited before becoming ready', [
223
+ 'Check the error output above',
224
+ ]);
225
+ }
226
+ }
227
+
228
+ if (!existsSync(socketPath)) {
229
+ child.kill('SIGTERM');
230
+ exitWithError('Server failed to start within 30 seconds');
231
+ }
232
+
233
+ // Wait for HTTP port lockfile (warmup may take a while for large graphs)
234
+ const actualHttpPort = await waitForHttpPort(httpPortFile, 30_000);
235
+
236
+ const version = await (async () => {
237
+ try {
238
+ const s = await isServerRunning(socketPath);
239
+ return s.version || null;
240
+ } catch { return null; }
241
+ })();
242
+
243
+ printBanner(socketPath, actualHttpPort, child.pid ?? null, version, null);
244
+ console.log(' Press Ctrl+C to stop\n');
245
+
246
+ // Flush buffered stderr
247
+ for (const line of stderrLines) process.stderr.write(line + '\n');
248
+ bannerPrinted = true;
249
+
250
+ // Keep process alive
251
+ child.on('close', (code) => {
252
+ if (existsSync(socketPath)) {
253
+ try { unlinkSync(socketPath); } catch { /* ignore */ }
254
+ }
255
+ if (existsSync(pidPath)) {
256
+ try { unlinkSync(pidPath); } catch { /* ignore */ }
257
+ }
258
+ process.exit(code ?? 0);
259
+ });
260
+ });
261
+
262
+ // ─── grafema stop ────────────────────────────────────────────────────────────
263
+
264
+ export const stopCommand = new Command('stop')
265
+ .description('Stop Grafema')
266
+ .option('-p, --project <path>', 'Project path', '.')
267
+ .action(async (options: { project: string }) => {
268
+ const projectPath = resolve(options.project);
269
+ const { socketPath, pidPath } = getProjectPaths(projectPath);
270
+
271
+ const status = await isServerRunning(socketPath);
272
+ if (!status.running) {
273
+ console.log('Grafema is not running');
274
+ if (existsSync(socketPath)) {
275
+ try { unlinkSync(socketPath); } catch { /* ignore */ }
276
+ }
277
+ if (existsSync(pidPath)) {
278
+ try { unlinkSync(pidPath); } catch { /* ignore */ }
279
+ }
280
+ return;
281
+ }
282
+
283
+ console.log('Stopping Grafema...');
284
+
285
+ const client = new RFDBClient(socketPath, 'cli');
286
+ client.on('error', () => {});
287
+ try {
288
+ await client.connect();
289
+ await client.shutdown();
290
+ } catch { /* expected */ }
291
+
292
+ let attempts = 0;
293
+ while (existsSync(socketPath) && attempts < 30) {
294
+ await sleep(100);
295
+ attempts++;
296
+ }
297
+
298
+ if (existsSync(pidPath)) {
299
+ try { unlinkSync(pidPath); } catch { /* ignore */ }
300
+ }
301
+
302
+ console.log('Grafema stopped');
303
+ });
@@ -130,14 +130,20 @@ Examples:
130
130
  const { varName, scopeName } = parseTracePattern(pattern);
131
131
  const maxDepth = parseInt(options.depth, 10);
132
132
 
133
- console.log(`Tracing ${varName}${scopeName ? ` from ${scopeName}` : ''}...`);
134
- console.log('');
133
+ if (!options.json) {
134
+ console.log(`Tracing ${varName}${scopeName ? ` from ${scopeName}` : ''}...`);
135
+ console.log('');
136
+ }
135
137
 
136
138
  // Find starting variable(s)
137
139
  const variables = await findVariables(backend, varName, scopeName);
138
140
 
139
141
  if (variables.length === 0) {
140
- console.log(`No variable "${varName}" found${scopeName ? ` in ${scopeName}` : ''}`);
142
+ if (options.json) {
143
+ console.log(JSON.stringify({ variables: [] }, null, 2));
144
+ } else {
145
+ console.log(`No variable "${varName}" found${scopeName ? ` in ${scopeName}` : ''}`);
146
+ }
141
147
  return;
142
148
  }
143
149
 
@@ -145,47 +151,61 @@ Examples:
145
151
  const dfDb = backend as unknown as DataflowBackend;
146
152
 
147
153
  // Trace each variable using shared BFS
148
- for (const variable of variables) {
149
- console.log(formatNodeDisplay(variable, { projectPath }));
150
- console.log('');
154
+ const jsonEntries: object[] = [];
151
155
 
156
+ for (const variable of variables) {
152
157
  const results = await traceDataflow(dfDb, variable.id, {
153
158
  direction: 'both',
154
159
  maxDepth,
155
160
  });
156
161
 
157
- const narrative = renderTraceNarrative(results, variable.name || variable.id, {
158
- detail: options.detail || 'normal',
159
- hintStyle: 'cli',
160
- });
161
- console.log(narrative);
162
-
163
- // Show value domain if available
164
162
  const sources = await getValueSources(backend, variable.id);
165
- if (sources.length > 0) {
163
+
164
+ if (options.json) {
165
+ jsonEntries.push({
166
+ id: variable.id,
167
+ type: variable.type,
168
+ name: variable.name,
169
+ file: variable.file,
170
+ line: variable.line,
171
+ dataflow: results,
172
+ possibleValues: sources,
173
+ });
174
+ } else {
175
+ console.log(formatNodeDisplay(variable, { projectPath }));
166
176
  console.log('');
167
- console.log('Possible values:');
168
- for (const src of sources) {
169
- if (src.type === 'LITERAL' && src.value !== undefined) {
170
- console.log(` • ${JSON.stringify(src.value)} (literal)`);
171
- } else if (src.type === 'PARAMETER') {
172
- console.log(` • <parameter ${src.name}> (runtime input)`);
173
- } else if (src.type === 'CALL') {
174
- console.log(` • <return from ${src.name || 'call'}> (computed)`);
175
- } else {
176
- console.log(` • <${src.type.toLowerCase()}> ${src.name || ''}`);
177
+
178
+ const narrative = renderTraceNarrative(results, variable.name || variable.id, {
179
+ detail: options.detail || 'normal',
180
+ hintStyle: 'cli',
181
+ });
182
+ console.log(narrative);
183
+
184
+ if (sources.length > 0) {
185
+ console.log('');
186
+ console.log('Possible values:');
187
+ for (const src of sources) {
188
+ if (src.type === 'LITERAL' && src.value !== undefined) {
189
+ console.log(` • ${JSON.stringify(src.value)} (literal)`);
190
+ } else if (src.type === 'PARAMETER') {
191
+ console.log(` • <parameter ${src.name}> (runtime input)`);
192
+ } else if (src.type === 'CALL') {
193
+ console.log(` • <return from ${src.name || 'call'}> (computed)`);
194
+ } else {
195
+ console.log(` • <${src.type.toLowerCase()}> ${src.name || ''}`);
196
+ }
177
197
  }
178
198
  }
179
- }
180
199
 
181
- if (variables.length > 1) {
182
- console.log('');
183
- console.log('---');
200
+ if (variables.length > 1) {
201
+ console.log('');
202
+ console.log('---');
203
+ }
184
204
  }
185
205
  }
186
206
 
187
207
  if (options.json) {
188
- // TODO: structured JSON output
208
+ console.log(JSON.stringify({ variables: jsonEntries }, null, 2));
189
209
  }
190
210
 
191
211
  } finally {