@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.
@@ -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
- console.error(chalk_1.default.red(`\n❌ Error: Graph file not found at ${graphFilePath}`));
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
- platformTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.magenta(tableName)));
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
- console.log(chalk_1.default.yellow.bold('\n🔥 BLAST RADIUS ANALYSIS'));
96
- console.log(chalk_1.default.gray(''.repeat(80)));
97
- console.log(chalk_1.default.gray('Target: ') + chalk_1.default.white(targetTableName) + (isBaseTable ? chalk_1.default.yellow(' [Base Table]') : ''));
98
- if (!isBaseTable) {
99
- console.log(chalk_1.default.gray('File: ') + chalk_1.default.gray(targetNode.source.file));
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
- console.log(chalk_1.default.green('\n✅ Zero dependencies detected in local project.\n'));
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 output = displayed.map(tn => ({
129
- artifact: tn.node.name,
130
- type: registry.getDisplayGroupName(tn.node.table).toLowerCase(),
131
- file: tn.node.source.file,
132
- hop: tn.hop,
133
- path: tn.path,
134
- via: tn.viaEdge.via ?? tn.viaEdge.type,
135
- edgeType: tn.viaEdge.type,
136
- state: tn.node.action,
137
- }));
138
- console.log(JSON.stringify(output, null, 2));
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
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
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
- console.error(chalk_1.default.red('\n❌ Error:'), sdkDetectionResult.error);
39
- console.log(chalk_1.default.yellow('\n💡 Make sure you\'re in a ServiceNow Fluent project directory'));
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 (!options.quiet) {
75
- console.log(chalk_1.default.green('\n✅ Analysis complete'));
76
- console.log(chalk_1.default.gray(' Location: ') + chalk_1.default.white(outputFilePath));
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
- // Step 6: Visual Lineage Map
81
- if (options.display && !options.quiet) {
82
- (0, display_1.displayLineageMap)(lineageGraph, workspaceSDK.registry);
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 (error instanceof Error) {
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 or --quiet flag)
19
- const showBanner = !process.argv.includes('--no-banner') && !process.argv.includes('--quiet');
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
@@ -1,2 +1,3 @@
1
1
  export * from './extractor';
2
2
  export * from './types/graphs';
3
+ export * from './blast/traversal';
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yesprasad/fluent-graph",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Dependency visibility and impact analysis for ServiceNow Fluent SDK projects",
5
5
  "bin": {
6
6
  "fluent-graph": "./bin/fluent-graph.js"