@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.
- package/README.md +59 -45
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyzeAction.d.ts.map +1 -1
- package/dist/commands/analyzeAction.js +134 -3
- package/dist/commands/analyzeAction.js.map +1 -1
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +7 -3
- package/dist/commands/doctor/checks.js.map +1 -1
- package/dist/commands/export.d.ts +15 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +88 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/exportAction.d.ts +35 -0
- package/dist/commands/exportAction.d.ts.map +1 -0
- package/dist/commands/exportAction.js +58 -0
- package/dist/commands/exportAction.js.map +1 -0
- package/dist/commands/features.d.ts +13 -0
- package/dist/commands/features.d.ts.map +1 -0
- package/dist/commands/features.js +69 -0
- package/dist/commands/features.js.map +1 -0
- package/dist/commands/featuresAction.d.ts +82 -0
- package/dist/commands/featuresAction.d.ts.map +1 -0
- package/dist/commands/featuresAction.js +139 -0
- package/dist/commands/featuresAction.js.map +1 -0
- package/dist/commands/start.d.ts +12 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +294 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +50 -30
- package/dist/commands/trace.js.map +1 -1
- package/dist/commands/upgrade.d.ts +3 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +279 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/package.json +8 -8
- package/src/cli.ts +11 -0
- package/src/commands/analyzeAction.ts +135 -2
- package/src/commands/doctor/checks.ts +4 -3
- package/src/commands/explore.tsx +29 -2
- package/src/commands/export.ts +102 -0
- package/src/commands/exportAction.ts +107 -0
- package/src/commands/features.ts +88 -0
- package/src/commands/featuresAction.ts +218 -0
- package/src/commands/start.ts +303 -0
- package/src/commands/trace.ts +49 -29
- 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
|
+
});
|
package/src/commands/trace.ts
CHANGED
|
@@ -130,14 +130,20 @@ Examples:
|
|
|
130
130
|
const { varName, scopeName } = parseTracePattern(pattern);
|
|
131
131
|
const maxDepth = parseInt(options.depth, 10);
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
if (variables.length > 1) {
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log('---');
|
|
203
|
+
}
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
|
|
187
207
|
if (options.json) {
|
|
188
|
-
|
|
208
|
+
console.log(JSON.stringify({ variables: jsonEntries }, null, 2));
|
|
189
209
|
}
|
|
190
210
|
|
|
191
211
|
} finally {
|