@yesprasad/fluent-graph 0.2.1 → 0.2.2
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/dist/cli/commands/blast.js +74 -25
- package/dist/cli/commands/extract.js +41 -15
- package/dist/cli/index.js +15 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ exports.blastCommand = new commander_1.Command('blast')
|
|
|
56
56
|
.option('--include-deleted', 'Include deleted artifacts in the analysis')
|
|
57
57
|
.option('--no-banner', 'Skip banner (inherited)')
|
|
58
58
|
.action(async (targetTableName, options) => {
|
|
59
|
+
const isJson = options.format === 'json';
|
|
59
60
|
try {
|
|
60
61
|
const graphFilePath = path.isAbsolute(options.graph)
|
|
61
62
|
? options.graph
|
|
@@ -67,7 +68,12 @@ exports.blastCommand = new commander_1.Command('blast')
|
|
|
67
68
|
registry.load(sdkDetection.sdkPath);
|
|
68
69
|
}
|
|
69
70
|
if (!fs.existsSync(graphFilePath)) {
|
|
70
|
-
|
|
71
|
+
if (isJson) {
|
|
72
|
+
console.log(JSON.stringify({ error: `Graph file not found at ${graphFilePath}`, code: 'GRAPH_NOT_FOUND' }));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.error(chalk_1.default.red(`\n❌ Error: Graph file not found at ${graphFilePath}`));
|
|
76
|
+
}
|
|
71
77
|
process.exit(1);
|
|
72
78
|
}
|
|
73
79
|
const lineageGraph = JSON.parse(fs.readFileSync(graphFilePath, 'utf8'));
|
|
@@ -77,28 +83,40 @@ exports.blastCommand = new commander_1.Command('blast')
|
|
|
77
83
|
const isBaseTable = !targetNode;
|
|
78
84
|
const isTargetReferenced = lineageGraph.edges.some(edge => edge.to === targetNodeId || edge.targetTable === targetTableName);
|
|
79
85
|
if (!targetNode && !isTargetReferenced) {
|
|
80
|
-
console.log(chalk_1.default.red(`\n❌ Target "${targetTableName}" not found or referenced`));
|
|
81
|
-
console.log(chalk_1.default.yellow('\n💡 Available custom tables:'));
|
|
82
86
|
const customTables = lineageGraph.nodes
|
|
83
87
|
.filter(node => registry.getPluginName(node.table) === 'TablePlugin' && !registry.isChild(node.table) && node.action !== 'DELETE')
|
|
84
88
|
.map(node => node.name)
|
|
85
89
|
.sort();
|
|
86
|
-
customTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.white(tableName)));
|
|
87
|
-
console.log(chalk_1.default.yellow('\n💡 Referenced platform tables:'));
|
|
88
90
|
const platformTables = [...new Set(lineageGraph.edges
|
|
89
91
|
.filter(edge => edge.to.startsWith('platform:'))
|
|
90
92
|
.map(edge => edge.to.split(':')[1]))].sort();
|
|
91
|
-
|
|
93
|
+
if (isJson) {
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
|
+
error: `Target "${targetTableName}" not found or referenced`,
|
|
96
|
+
code: 'TARGET_NOT_FOUND',
|
|
97
|
+
availableCustomTables: customTables,
|
|
98
|
+
availablePlatformTables: platformTables,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log(chalk_1.default.red(`\n❌ Target "${targetTableName}" not found or referenced`));
|
|
103
|
+
console.log(chalk_1.default.yellow('\n💡 Available custom tables:'));
|
|
104
|
+
customTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.white(tableName)));
|
|
105
|
+
console.log(chalk_1.default.yellow('\n💡 Referenced platform tables:'));
|
|
106
|
+
platformTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.magenta(tableName)));
|
|
107
|
+
}
|
|
92
108
|
process.exit(1);
|
|
93
109
|
}
|
|
94
110
|
// ── STEP 2: Display header ───────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
if (!isJson) {
|
|
112
|
+
console.log(chalk_1.default.yellow.bold('\n🔥 BLAST RADIUS ANALYSIS'));
|
|
113
|
+
console.log(chalk_1.default.gray('═'.repeat(80)));
|
|
114
|
+
console.log(chalk_1.default.gray('Target: ') + chalk_1.default.white(targetTableName) + (isBaseTable ? chalk_1.default.yellow(' [Base Table]') : ''));
|
|
115
|
+
if (!isBaseTable) {
|
|
116
|
+
console.log(chalk_1.default.gray('File: ') + chalk_1.default.gray(targetNode.source.file));
|
|
117
|
+
}
|
|
118
|
+
console.log(chalk_1.default.gray('═'.repeat(80)));
|
|
100
119
|
}
|
|
101
|
-
console.log(chalk_1.default.gray('═'.repeat(80)));
|
|
102
120
|
// ── STEP 3: BFS traversal ────────────────────────────────────────────────
|
|
103
121
|
const maxDepth = options.directOnly ? 1 : Math.max(1, parseInt(options.depth, 10) || 10);
|
|
104
122
|
const traversal = (0, traversal_1.traverseBlastRadius)(lineageGraph, targetTableName, maxDepth);
|
|
@@ -119,23 +137,49 @@ exports.blastCommand = new commander_1.Command('blast')
|
|
|
119
137
|
const directImpact = displayed.filter(tn => tn.hop === 1);
|
|
120
138
|
const transitiveImpact = displayed.filter(tn => tn.hop > 1);
|
|
121
139
|
if (displayed.length === 0) {
|
|
122
|
-
|
|
140
|
+
if (isJson) {
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
target: targetTableName,
|
|
143
|
+
isBaseTable,
|
|
144
|
+
totalImpacted: 0,
|
|
145
|
+
direct: 0,
|
|
146
|
+
transitive: 0,
|
|
147
|
+
maxHopReached: 0,
|
|
148
|
+
cyclesPrevented: traversal.cyclesPrevented,
|
|
149
|
+
riskLevel: 'LOW',
|
|
150
|
+
impacts: [],
|
|
151
|
+
}, null, 2));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log(chalk_1.default.green('\n✅ Zero dependencies detected in local project.\n'));
|
|
155
|
+
}
|
|
123
156
|
return;
|
|
124
157
|
}
|
|
125
158
|
// ── STEP 5: Output ───────────────────────────────────────────────────────
|
|
126
159
|
switch (options.format) {
|
|
127
160
|
case 'json': {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
161
|
+
const totalCount = displayed.length;
|
|
162
|
+
const riskLevel = totalCount < 3 ? 'LOW' : totalCount < 8 ? 'MEDIUM' : 'HIGH';
|
|
163
|
+
console.log(JSON.stringify({
|
|
164
|
+
target: targetTableName,
|
|
165
|
+
isBaseTable,
|
|
166
|
+
totalImpacted: totalCount,
|
|
167
|
+
direct: directImpact.length,
|
|
168
|
+
transitive: transitiveImpact.length,
|
|
169
|
+
maxHopReached: traversal.maxHopReached,
|
|
170
|
+
cyclesPrevented: traversal.cyclesPrevented,
|
|
171
|
+
riskLevel,
|
|
172
|
+
impacts: displayed.map(tn => ({
|
|
173
|
+
artifact: tn.node.name,
|
|
174
|
+
type: registry.getDisplayGroupName(tn.node.table).toLowerCase(),
|
|
175
|
+
file: tn.node.source.file,
|
|
176
|
+
hop: tn.hop,
|
|
177
|
+
path: tn.path,
|
|
178
|
+
via: tn.viaEdge.via ?? tn.viaEdge.type,
|
|
179
|
+
edgeType: tn.viaEdge.type,
|
|
180
|
+
state: tn.node.action,
|
|
181
|
+
})),
|
|
182
|
+
}, null, 2));
|
|
139
183
|
break;
|
|
140
184
|
}
|
|
141
185
|
case 'csv': {
|
|
@@ -233,7 +277,12 @@ exports.blastCommand = new commander_1.Command('blast')
|
|
|
233
277
|
}
|
|
234
278
|
}
|
|
235
279
|
catch (error) {
|
|
236
|
-
|
|
280
|
+
if (isJson) {
|
|
281
|
+
console.log(JSON.stringify({ error: error.message ?? 'Unexpected error', code: 'UNEXPECTED_ERROR' }));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.error(chalk_1.default.red('\n❌ Error:'), error.message);
|
|
285
|
+
}
|
|
237
286
|
process.exit(1);
|
|
238
287
|
}
|
|
239
288
|
});
|
|
@@ -20,30 +20,37 @@ const display_1 = require("../utils/display");
|
|
|
20
20
|
exports.extractCommand = new commander_1.Command('analyze')
|
|
21
21
|
.description('Analyze and extract dependency graph from the current project')
|
|
22
22
|
.option('-o, --output <path>', 'Output file path', 'fluent-graph.json')
|
|
23
|
+
.option('-f, --format <type>', 'Output format: table, json', 'table')
|
|
23
24
|
.option('-q, --quiet', 'Suppress console output')
|
|
24
25
|
.option('--no-display', 'Skip visual display')
|
|
25
26
|
.option('--no-banner', 'Skip banner (inherited)')
|
|
26
27
|
.action(async (options) => {
|
|
28
|
+
const isJson = options.format === 'json';
|
|
27
29
|
let spinner;
|
|
28
30
|
try {
|
|
29
31
|
const currentWorkingDir = process.cwd();
|
|
30
32
|
// Step 1: Detect ServiceNow SDK Environment
|
|
31
|
-
if (!options.quiet) {
|
|
33
|
+
if (!options.quiet && !isJson) {
|
|
32
34
|
spinner = (0, ora_1.default)('Detecting ServiceNow SDK...').start();
|
|
33
35
|
}
|
|
34
36
|
const sdkDetectionResult = await (0, detector_1.detectSDK)(currentWorkingDir);
|
|
35
37
|
if (!sdkDetectionResult.found) {
|
|
36
38
|
if (spinner)
|
|
37
39
|
spinner.fail('SDK not found');
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
if (isJson) {
|
|
41
|
+
console.log(JSON.stringify({ error: sdkDetectionResult.error, code: 'SDK_NOT_FOUND' }));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.error(chalk_1.default.red('\n❌ Error:'), sdkDetectionResult.error);
|
|
45
|
+
console.log(chalk_1.default.yellow('\n💡 Make sure you\'re in a ServiceNow Fluent project directory'));
|
|
46
|
+
}
|
|
40
47
|
process.exit(1);
|
|
41
48
|
}
|
|
42
49
|
if (spinner) {
|
|
43
50
|
spinner.succeed(`✅ SDK detected at ${chalk_1.default.gray(sdkDetectionResult.sdkPath)}`);
|
|
44
51
|
}
|
|
45
52
|
// Step 2: Dynamically Load Workspace SDK
|
|
46
|
-
if (!options.quiet) {
|
|
53
|
+
if (!options.quiet && !isJson) {
|
|
47
54
|
spinner = (0, ora_1.default)('Loading workspace SDK...').start();
|
|
48
55
|
}
|
|
49
56
|
const workspaceSDK = (0, loader_1.loadWorkspaceSDK)(sdkDetectionResult.sdkPath);
|
|
@@ -51,7 +58,7 @@ exports.extractCommand = new commander_1.Command('analyze')
|
|
|
51
58
|
spinner.succeed('Workspace SDK loaded');
|
|
52
59
|
}
|
|
53
60
|
// Step 3: Instantiate the Project Proxy
|
|
54
|
-
if (!options.quiet) {
|
|
61
|
+
if (!options.quiet && !isJson) {
|
|
55
62
|
spinner = (0, ora_1.default)('Loading project configuration...').start();
|
|
56
63
|
}
|
|
57
64
|
const projectInstance = new workspaceSDK.sdkapi.Project({ rootDir: currentWorkingDir });
|
|
@@ -59,10 +66,19 @@ exports.extractCommand = new commander_1.Command('analyze')
|
|
|
59
66
|
spinner.succeed('Project loaded');
|
|
60
67
|
}
|
|
61
68
|
// Step 4: Extract Lineage Graph
|
|
62
|
-
if (!options.quiet) {
|
|
69
|
+
if (!options.quiet && !isJson) {
|
|
63
70
|
spinner = (0, ora_1.default)('Building dependency graph...').start();
|
|
64
71
|
}
|
|
72
|
+
// In JSON mode, redirect SDK diagnostic noise (e.g. "Record defined N times")
|
|
73
|
+
// from stdout to stderr so stdout stays pure JSON for agent/tool consumption.
|
|
74
|
+
let restoreStdout;
|
|
75
|
+
if (isJson) {
|
|
76
|
+
const original = process.stdout.write.bind(process.stdout);
|
|
77
|
+
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
78
|
+
restoreStdout = () => { process.stdout.write = original; };
|
|
79
|
+
}
|
|
65
80
|
const lineageGraph = await (0, extractor_1.extractLineageGraph)(projectInstance, workspaceSDK.registry);
|
|
81
|
+
restoreStdout?.();
|
|
66
82
|
if (spinner) {
|
|
67
83
|
spinner.succeed(`Graph built: ${chalk_1.default.cyan(lineageGraph.nodes.length)} nodes, ${chalk_1.default.cyan(lineageGraph.edges.length)} edges`);
|
|
68
84
|
}
|
|
@@ -71,19 +87,29 @@ exports.extractCommand = new commander_1.Command('analyze')
|
|
|
71
87
|
? options.output
|
|
72
88
|
: path_1.default.join(currentWorkingDir, options.output);
|
|
73
89
|
(0, writer_1.writeGraph)(lineageGraph, outputFilePath);
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
console.log(
|
|
77
|
-
console.log(chalk_1.default.gray(' Complexity: ') + chalk_1.default.cyan((lineageGraph.edges.length / lineageGraph.nodes.length).toFixed(2)) + chalk_1.default.gray(' edges/node'));
|
|
78
|
-
console.log(chalk_1.default.gray(' Generated at: ') + chalk_1.default.gray(new Date().toLocaleTimeString()));
|
|
90
|
+
if (isJson) {
|
|
91
|
+
// Pure JSON on stdout: the full lineage graph, ready for agent/tool consumption.
|
|
92
|
+
console.log(JSON.stringify(lineageGraph, null, 2));
|
|
79
93
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
else {
|
|
95
|
+
if (!options.quiet) {
|
|
96
|
+
console.log(chalk_1.default.green('\n✅ Analysis complete'));
|
|
97
|
+
console.log(chalk_1.default.gray(' Location: ') + chalk_1.default.white(outputFilePath));
|
|
98
|
+
console.log(chalk_1.default.gray(' Complexity: ') + chalk_1.default.cyan((lineageGraph.edges.length / lineageGraph.nodes.length).toFixed(2)) + chalk_1.default.gray(' edges/node'));
|
|
99
|
+
console.log(chalk_1.default.gray(' Generated at: ') + chalk_1.default.gray(new Date().toLocaleTimeString()));
|
|
100
|
+
}
|
|
101
|
+
// Step 6: Visual Lineage Map
|
|
102
|
+
if (options.display && !options.quiet) {
|
|
103
|
+
(0, display_1.displayLineageMap)(lineageGraph, workspaceSDK.registry);
|
|
104
|
+
}
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
107
|
catch (error) {
|
|
86
|
-
if (
|
|
108
|
+
if (isJson) {
|
|
109
|
+
const msg = error instanceof Error ? error.message : 'Unexpected error';
|
|
110
|
+
console.log(JSON.stringify({ error: msg, code: 'EXTRACTION_FAILED' }));
|
|
111
|
+
}
|
|
112
|
+
else if (error instanceof Error) {
|
|
87
113
|
console.error(chalk_1.default.red('\n❌ Error:'), error.message);
|
|
88
114
|
if (process.env.DEBUG) {
|
|
89
115
|
console.error(chalk_1.default.gray('\nStack trace:'));
|
package/dist/cli/index.js
CHANGED
|
@@ -15,8 +15,21 @@ const packageJsonContent = fs_1.default.readFileSync(packageJsonPath, 'utf-8');
|
|
|
15
15
|
const packageJson = JSON.parse(packageJsonContent);
|
|
16
16
|
// ===== 2. Initialize CLI program =====
|
|
17
17
|
const program = new commander_1.Command();
|
|
18
|
-
// Show banner on first run (unless --no-banner
|
|
19
|
-
|
|
18
|
+
// Show banner on first run (unless --no-banner / --quiet, or JSON output requested).
|
|
19
|
+
// JSON consumers (agents, scripts) need stdout to be pure JSON, so the banner is
|
|
20
|
+
// suppressed whenever `--format json` (or `-f json`) is present.
|
|
21
|
+
const wantsJsonOutput = (() => {
|
|
22
|
+
const argv = process.argv;
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a === '--format' || a === '-f')
|
|
26
|
+
return argv[i + 1] === 'json';
|
|
27
|
+
if (a === '--format=json' || a === '-f=json')
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
})();
|
|
32
|
+
const showBanner = !process.argv.includes('--no-banner') && !process.argv.includes('--quiet') && !wantsJsonOutput;
|
|
20
33
|
if (showBanner) {
|
|
21
34
|
(0, banner_1.displayBanner)();
|
|
22
35
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./extractor"), exports);
|
|
18
18
|
__exportStar(require("./types/graphs"), exports);
|
|
19
|
+
__exportStar(require("./blast/traversal"), exports);
|