@yesprasad/fluent-graph 0.1.0

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.
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.blastCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const cli_table3_1 = __importDefault(require("cli-table3"));
45
+ exports.blastCommand = new commander_1.Command('blast')
46
+ .description('Analyze blast radius for a target table')
47
+ .argument('<target>', 'Target table name (e.g., incident, x_bank_account)')
48
+ .option('-g, --graph <path>', 'Path to graph JSON file', 'fluent-graph.json')
49
+ .option('-f, --format <type>', 'Output format: table, json, csv', 'table')
50
+ .option('--include-deleted', 'Include deleted artifacts in the analysis')
51
+ .option('--no-banner', 'Skip banner (inherited)')
52
+ .action(async (targetTableName, options) => {
53
+ try {
54
+ const graphFilePath = path.isAbsolute(options.graph)
55
+ ? options.graph
56
+ : path.join(process.cwd(), options.graph);
57
+ if (!fs.existsSync(graphFilePath)) {
58
+ console.error(chalk_1.default.red(`\n❌ Error: Graph file not found at ${graphFilePath}`));
59
+ process.exit(1);
60
+ }
61
+ const lineageGraph = JSON.parse(fs.readFileSync(graphFilePath, 'utf8'));
62
+ // STEP 1: Determine target node (real node or shadow for platform)
63
+ const targetNode = lineageGraph.nodes.find(node => node.name === targetTableName);
64
+ const targetNodeId = targetNode ? targetNode.id : `platform:${targetTableName}`;
65
+ const isBaseTable = !targetNode;
66
+ // STEP 2: Check if target is referenced at all
67
+ const isTargetReferenced = lineageGraph.edges.some(edge => edge.to === targetNodeId || edge.targetTable === targetTableName);
68
+ if (!targetNode && !isTargetReferenced) {
69
+ console.log(chalk_1.default.red(`\n❌ Target "${targetTableName}" not found or referenced`));
70
+ console.log(chalk_1.default.yellow('\n💡 Available custom tables:'));
71
+ const customTables = lineageGraph.nodes
72
+ .filter(node => node.table === 'sys_db_object' && node.action !== 'DELETE')
73
+ .map(node => node.name)
74
+ .sort();
75
+ customTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.white(tableName)));
76
+ console.log(chalk_1.default.yellow('\n💡 Referenced platform tables:'));
77
+ const platformTables = [...new Set(lineageGraph.edges
78
+ .filter(edge => edge.to.startsWith('platform:'))
79
+ .map(edge => edge.to.split(':')[1]))].sort();
80
+ platformTables.forEach(tableName => console.log(chalk_1.default.gray(' • ') + chalk_1.default.magenta(tableName)));
81
+ process.exit(1);
82
+ }
83
+ // STEP 3: Display header
84
+ console.log(chalk_1.default.yellow.bold('\n🔥 BLAST RADIUS ANALYSIS'));
85
+ console.log(chalk_1.default.gray('═'.repeat(80)));
86
+ console.log(chalk_1.default.gray('Target: ') + chalk_1.default.white(targetTableName) + (isBaseTable ? chalk_1.default.yellow(' [Base Table]') : ''));
87
+ if (!isBaseTable) {
88
+ console.log(chalk_1.default.gray('File: ') + chalk_1.default.gray(targetNode.source.file));
89
+ }
90
+ console.log(chalk_1.default.gray('═'.repeat(80)));
91
+ // STEP 4: Collect impacted artifacts from edges
92
+ const impactedArtifacts = lineageGraph.edges
93
+ .filter(edge => edge.to === targetNodeId || edge.targetTable === targetTableName)
94
+ .map(edge => {
95
+ const sourceNode = lineageGraph.nodes.find(node => node.id === edge.from);
96
+ if (!sourceNode || (!options.includeDeleted && sourceNode.action === 'DELETE'))
97
+ return null;
98
+ // Friendly type mapping
99
+ let artifactType = 'unknown';
100
+ switch (sourceNode.table) {
101
+ case 'sys_script_client':
102
+ artifactType = 'client_script';
103
+ break;
104
+ case 'sys_script':
105
+ artifactType = 'business_rule';
106
+ break;
107
+ case 'sys_security_acl':
108
+ artifactType = 'acl';
109
+ break;
110
+ case 'sys_ui_action':
111
+ artifactType = 'ui_action';
112
+ break;
113
+ case 'sys_script_include':
114
+ artifactType = 'script_include';
115
+ break;
116
+ case 'sys_dictionary':
117
+ artifactType = 'field';
118
+ break; // For schema/FK
119
+ default: artifactType = sourceNode.table.replace('sys_', '').replace('_', ' ');
120
+ }
121
+ // Reason based on edge type
122
+ const impactReason = edge.type === 'schema_relationship'
123
+ ? `FK reference via ${edge.via || 'field'}`
124
+ : `${sourceNode.attributes?.type || sourceNode.attributes?.when || edge.via || 'attachment'}`;
125
+ return {
126
+ artifact: sourceNode.name,
127
+ type: artifactType,
128
+ file: sourceNode.source.file,
129
+ reason: impactReason,
130
+ state: sourceNode.action
131
+ };
132
+ })
133
+ .filter((artifact) => artifact !== null);
134
+ if (impactedArtifacts.length === 0) {
135
+ console.log(chalk_1.default.green('\n✅ Zero dependencies detected in local project.\n'));
136
+ return;
137
+ }
138
+ // STEP 5: Output based on format
139
+ switch (options.format) {
140
+ case 'json':
141
+ console.log(JSON.stringify(impactedArtifacts, null, 2));
142
+ break;
143
+ case 'csv':
144
+ console.log('Artifact,Type,File,Reason,State');
145
+ impactedArtifacts.forEach(impactedArtifact => {
146
+ console.log(`"${impactedArtifact.artifact}","${impactedArtifact.type}","${impactedArtifact.file}","${impactedArtifact.reason}","${impactedArtifact.state}"`);
147
+ });
148
+ break;
149
+ case 'table':
150
+ default:
151
+ const table = new cli_table3_1.default({
152
+ head: [
153
+ chalk_1.default.cyan('Artifact'),
154
+ chalk_1.default.cyan('Type'),
155
+ chalk_1.default.cyan('File'),
156
+ chalk_1.default.cyan('Reason'),
157
+ chalk_1.default.cyan('State')
158
+ ],
159
+ wordWrap: true,
160
+ style: { head: [], border: ['gray'] }
161
+ });
162
+ impactedArtifacts.forEach(impactedArtifact => {
163
+ const typeIcon = impactedArtifact.type === 'field' ? '🔗'
164
+ : impactedArtifact.type === 'client_script' ? '⚡'
165
+ : impactedArtifact.type === 'business_rule' ? '🔧'
166
+ : impactedArtifact.type === 'acl' ? '🔒'
167
+ : '';
168
+ const stateColor = impactedArtifact.state === 'DELETE' ? chalk_1.default.red : chalk_1.default.green;
169
+ table.push([
170
+ chalk_1.default.white(impactedArtifact.artifact),
171
+ `${typeIcon} ${chalk_1.default.gray(impactedArtifact.type)}`,
172
+ chalk_1.default.gray(impactedArtifact.file),
173
+ impactedArtifact.reason,
174
+ stateColor(impactedArtifact.state)
175
+ ]);
176
+ });
177
+ console.log('\n' + table.toString());
178
+ // STEP 6: Impact Summary
179
+ console.log(chalk_1.default.yellow('\n📊 IMPACT SUMMARY'));
180
+ const riskLevel = impactedArtifacts.length < 3 ? 'LOW'
181
+ : impactedArtifacts.length < 8 ? 'MEDIUM'
182
+ : 'HIGH';
183
+ const riskColor = riskLevel === 'LOW' ? chalk_1.default.green
184
+ : riskLevel === 'MEDIUM' ? chalk_1.default.yellow
185
+ : chalk_1.default.red;
186
+ console.log(chalk_1.default.gray('Total Impacted: ') + chalk_1.default.cyan(impactedArtifacts.length));
187
+ console.log(chalk_1.default.gray('Risk Level: ') + riskColor.bold(riskLevel));
188
+ console.log(chalk_1.default.gray('─'.repeat(80)) + '\n');
189
+ if (riskLevel === 'HIGH') {
190
+ console.log(chalk_1.default.yellow('⚠️ High impact - Review carefully'));
191
+ }
192
+ else if (riskLevel === 'MEDIUM') {
193
+ console.log(chalk_1.default.yellow('💡 Moderate impact'));
194
+ }
195
+ break;
196
+ }
197
+ }
198
+ catch (error) {
199
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
200
+ process.exit(1);
201
+ }
202
+ });
@@ -0,0 +1,6 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * The Extract Command: Orchestrates the transformation of a ServiceNow Fluent
4
+ * project into a formal directed graph.
5
+ */
6
+ export declare const extractCommand: Command;
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const ora_1 = __importDefault(require("ora"));
11
+ const detector_1 = require("../../sdk/detector");
12
+ const loader_1 = require("../../sdk/loader");
13
+ const extractor_1 = require("../../extractor");
14
+ const writer_1 = require("../../output/writer");
15
+ const display_1 = require("../utils/display");
16
+ // /**
17
+ // * The Analyze Command: Orchestrates the transformation of a ServiceNow Fluent
18
+ // * project into a formal directed graph.
19
+ // */
20
+ // export const extractCommand = new Command('analyze')
21
+ // .description('Analyze and Extract dependency graph from current project')
22
+ // .option('-o, --output <path>', 'Output file path', 'fluent-graph.json')
23
+ // .option('-q, --quiet', 'Suppress console output')
24
+ // .option('--no-display', 'Skip visual display')
25
+ // .option('--no-banner', 'Skip banner (inherited)')
26
+ // .action(async (options) => {
27
+ // try {
28
+ // const cwd = process.cwd();
29
+ // let spinner: any;
30
+ // // Step 1: Detect ServiceNow SDK Environment
31
+ // if (!options.quiet) {
32
+ // spinner = ora('Detecting ServiceNow SDK...').start();
33
+ // }
34
+ // const sdk = await detectSDK(cwd);
35
+ // if (!sdk.found) {
36
+ // if (spinner) spinner.fail('SDK not found');
37
+ // console.error(chalk.red('\n❌ Error:'), sdk.error);
38
+ // console.log(chalk.yellow('\n💡 Make sure you\'re in a ServiceNow Fluent project directory'));
39
+ // process.exit(1);
40
+ // }
41
+ // if (spinner) {
42
+ // spinner.succeed(`✅ SDK detected at ${chalk.gray(sdk.sdkPath)}`);
43
+ // }
44
+ // // Step 2: Dynamically Load Workspace SDK
45
+ // if (!options.quiet) {
46
+ // spinner = ora('Loading workspace SDK...').start();
47
+ // }
48
+ // const { sdkapi } = loadWorkspaceSDK(sdk.sdkPath!);
49
+ // if (spinner) {
50
+ // spinner.succeed('Workspace SDK loaded');
51
+ // }
52
+ // // Step 3: Instantiate the Project Proxy
53
+ // if (!options.quiet) {
54
+ // spinner = ora('Loading project configuration...').start();
55
+ // }
56
+ // const project = new sdkapi.Project({ rootDir: cwd });
57
+ // if (spinner) {
58
+ // spinner.succeed('Project loaded');
59
+ // }
60
+ // // Step 4: Extract Lineage Graph
61
+ // // We pass the full project object, allowing the extractor to
62
+ // // access nodes, edges, and metadata (projectRoot, scope, etc.)
63
+ // if (!options.quiet) {
64
+ // spinner = ora('Building dependency graph...').start();
65
+ // }
66
+ // const graph = await extractLineageGraph(project);
67
+ // if (spinner) {
68
+ // spinner.succeed(`Graph built: ${chalk.cyan(graph.nodes.length)} nodes, ${chalk.cyan(graph.edges.length)} edges`);
69
+ // }
70
+ // // Step 5: Persist the Graph (The "Iterable" result)
71
+ // const outputPath = path.isAbsolute(options.output)
72
+ // ? options.output
73
+ // : path.join(cwd, options.output);
74
+ // writeGraph(graph, outputPath);
75
+ // if (!options.quiet) {
76
+ // console.log(chalk.green('\n✅ Analysis complete'));
77
+ // console.log(chalk.gray(' Location: ') + chalk.white(outputPath));
78
+ // console.log(chalk.gray(' Complexity: ') + chalk.cyan((graph.edges.length / graph.nodes.length).toFixed(2)) + chalk.gray(' edges/node'));
79
+ // console.log(chalk.gray(' Generated at: ') + chalk.gray(new Date().toLocaleTimeString()));
80
+ // }
81
+ // // Step 6: Visual Lineage Map
82
+ // // "Artifact-Agnostic" display logic
83
+ // if (options.display && !options.quiet) {
84
+ // displayLineageMap(graph);
85
+ // }
86
+ // } catch (err: any) {
87
+ // console.error(chalk.red('\n❌ Error:'), err.message);
88
+ // if (process.env.DEBUG) {
89
+ // console.error(chalk.gray('\nStack trace:'));
90
+ // console.error(err.stack);
91
+ // }
92
+ // process.exit(1);
93
+ // }
94
+ // });
95
+ /**
96
+ * The Extract Command: Orchestrates the transformation of a ServiceNow Fluent
97
+ * project into a formal directed graph.
98
+ */
99
+ exports.extractCommand = new commander_1.Command('analyze')
100
+ .description('Analyze and extract dependency graph from the current project')
101
+ .option('-o, --output <path>', 'Output file path', 'fluent-graph.json')
102
+ .option('-q, --quiet', 'Suppress console output')
103
+ .option('--no-display', 'Skip visual display')
104
+ .option('--no-banner', 'Skip banner (inherited)')
105
+ .action(async (options) => {
106
+ let spinner;
107
+ try {
108
+ const currentWorkingDir = process.cwd();
109
+ // Step 1: Detect ServiceNow SDK Environment
110
+ if (!options.quiet) {
111
+ spinner = (0, ora_1.default)('Detecting ServiceNow SDK...').start();
112
+ }
113
+ const sdkDetectionResult = await (0, detector_1.detectSDK)(currentWorkingDir);
114
+ if (!sdkDetectionResult.found) {
115
+ if (spinner)
116
+ spinner.fail('SDK not found');
117
+ console.error(chalk_1.default.red('\n❌ Error:'), sdkDetectionResult.error);
118
+ console.log(chalk_1.default.yellow('\n💡 Make sure you\'re in a ServiceNow Fluent project directory'));
119
+ process.exit(1);
120
+ }
121
+ if (spinner) {
122
+ spinner.succeed(`✅ SDK detected at ${chalk_1.default.gray(sdkDetectionResult.sdkPath)}`);
123
+ }
124
+ // Step 2: Dynamically Load Workspace SDK
125
+ if (!options.quiet) {
126
+ spinner = (0, ora_1.default)('Loading workspace SDK...').start();
127
+ }
128
+ const workspaceSDK = (0, loader_1.loadWorkspaceSDK)(sdkDetectionResult.sdkPath);
129
+ if (spinner) {
130
+ spinner.succeed('Workspace SDK loaded');
131
+ }
132
+ // Step 3: Instantiate the Project Proxy
133
+ if (!options.quiet) {
134
+ spinner = (0, ora_1.default)('Loading project configuration...').start();
135
+ }
136
+ const projectInstance = new workspaceSDK.sdkapi.Project({ rootDir: currentWorkingDir });
137
+ if (spinner) {
138
+ spinner.succeed('Project loaded');
139
+ }
140
+ // Step 4: Extract Lineage Graph
141
+ if (!options.quiet) {
142
+ spinner = (0, ora_1.default)('Building dependency graph...').start();
143
+ }
144
+ const lineageGraph = await (0, extractor_1.extractLineageGraph)(projectInstance);
145
+ if (spinner) {
146
+ spinner.succeed(`Graph built: ${chalk_1.default.cyan(lineageGraph.nodes.length)} nodes, ${chalk_1.default.cyan(lineageGraph.edges.length)} edges`);
147
+ }
148
+ // Step 5: Persist the Graph
149
+ const outputFilePath = path_1.default.isAbsolute(options.output)
150
+ ? options.output
151
+ : path_1.default.join(currentWorkingDir, options.output);
152
+ (0, writer_1.writeGraph)(lineageGraph, outputFilePath);
153
+ if (!options.quiet) {
154
+ console.log(chalk_1.default.green('\n✅ Analysis complete'));
155
+ console.log(chalk_1.default.gray(' Location: ') + chalk_1.default.white(outputFilePath));
156
+ 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'));
157
+ console.log(chalk_1.default.gray(' Generated at: ') + chalk_1.default.gray(new Date().toLocaleTimeString()));
158
+ }
159
+ // Step 6: Visual Lineage Map
160
+ if (options.display && !options.quiet) {
161
+ (0, display_1.displayLineageMap)(lineageGraph);
162
+ }
163
+ }
164
+ catch (error) {
165
+ if (error instanceof Error) {
166
+ console.error(chalk_1.default.red('\n❌ Error:'), error.message);
167
+ if (process.env.DEBUG) {
168
+ console.error(chalk_1.default.gray('\nStack trace:'));
169
+ console.error(error.stack);
170
+ }
171
+ }
172
+ else {
173
+ console.error(chalk_1.default.red('\n❌ Unknown error occurred'));
174
+ console.error(error);
175
+ }
176
+ process.exit(1);
177
+ }
178
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // import { Command } from 'commander';
4
+ // import chalk from 'chalk';
5
+ // import { extractCommand } from './commands/extract';
6
+ // import { blastCommand } from './commands/blast';
7
+ // import { displayBanner } from './utils/banner';
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ // const program = new Command();
13
+ // // Show banner on first run (unless --no-banner flag)
14
+ // const showBanner = !process.argv.includes('--no-banner') && !process.argv.includes('--quiet');
15
+ // if (showBanner) {
16
+ // displayBanner();
17
+ // }
18
+ // program
19
+ // .name('fluent-graph')
20
+ // .description('Dependency/ Lineage graph extraction & Blast-radius/ impact analysis for ServiceNow Fluent Artifacts')
21
+ // .version('1.0.0')
22
+ // .option('--no-banner', 'Suppress ASCII banner');
23
+ // // Add commands
24
+ // program.addCommand(extractCommand);
25
+ // program.addCommand(blastCommand);
26
+ // // Default behavior: show help if no command provided
27
+ // if (process.argv.length === 2) {
28
+ // program.help();
29
+ // }
30
+ // program.parse(process.argv);
31
+ const commander_1 = require("commander");
32
+ const path_1 = __importDefault(require("path"));
33
+ const fs_1 = __importDefault(require("fs"));
34
+ const extract_1 = require("./commands/extract");
35
+ const blast_1 = require("./commands/blast");
36
+ const banner_1 = require("./utils/banner");
37
+ // ===== 1. Load package.json dynamically =====
38
+ const packageJsonPath = path_1.default.join(__dirname, '../../package.json');
39
+ const packageJsonContent = fs_1.default.readFileSync(packageJsonPath, 'utf-8');
40
+ const packageJson = JSON.parse(packageJsonContent);
41
+ // ===== 2. Initialize CLI program =====
42
+ const program = new commander_1.Command();
43
+ // Show banner on first run (unless --no-banner or --quiet flag)
44
+ const showBanner = !process.argv.includes('--no-banner') && !process.argv.includes('--quiet');
45
+ if (showBanner) {
46
+ (0, banner_1.displayBanner)();
47
+ }
48
+ // Use dynamic name, description, version
49
+ program
50
+ .name(packageJson.name)
51
+ .description(packageJson.description)
52
+ .version(packageJson.version)
53
+ .option('--no-banner', 'Suppress ASCII banner');
54
+ // ===== 3. Register commands =====
55
+ program.addCommand(extract_1.extractCommand);
56
+ program.addCommand(blast_1.blastCommand);
57
+ // ===== 4. Default: show help if no command =====
58
+ if (process.argv.length === 2) {
59
+ program.help();
60
+ }
61
+ program.parse(process.argv);
@@ -0,0 +1,2 @@
1
+ export declare function displayBanner(): void;
2
+ export declare function displayVersion(): void;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.displayBanner = displayBanner;
7
+ exports.displayVersion = displayVersion;
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ function displayBanner() {
10
+ const banner = `
11
+ ███████╗██╗ ██╗ ██╗███████╗███╗ ██╗████████╗ ██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
12
+ ██╔════╝██║ ██║ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██║ ██║
13
+ █████╗ ██║ ██║ ██║█████╗ ██╔██╗ ██║ ██║ ██║ ███╗██████╔╝███████║██████╔╝███████║
14
+ ██╔══╝ ██║ ██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██╔══██╗██╔══██║██╔════╝██╔══██║
15
+ ██║ ███████╗╚██████╔╝███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ██║██║ ██║██║ ██║ ██║
16
+ ╚═╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
17
+ `;
18
+ console.log(chalk_1.default.cyan(banner));
19
+ console.log(chalk_1.default.gray(' Dependency/ Lineage graph extraction & Blast-radius/ impact analysis for ServiceNow Fluent Artifacts'));
20
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────────────────────\n'));
21
+ }
22
+ function displayVersion() {
23
+ console.log(chalk_1.default.cyan.bold('Fluent Graph') + chalk_1.default.gray(' v0.1.0'));
24
+ console.log(chalk_1.default.gray('Dependency visibility for ServiceNow Fluent projects\n'));
25
+ }
@@ -0,0 +1,2 @@
1
+ import { LineageGraph } from '../../types/graphs';
2
+ export declare function displayLineageMap(graph: LineageGraph): void;
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.displayLineageMap = displayLineageMap;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const cli_table3_1 = __importDefault(require("cli-table3"));
9
+ function displayLineageMap(graph) {
10
+ console.log(chalk_1.default.yellow.bold('\n📊 LINEAGE MAP'));
11
+ console.log(chalk_1.default.gray('═'.repeat(80)));
12
+ // ===== SECTION 1: DATA SCHEMA =====
13
+ const dataTables = graph.nodes.filter(node => node.table === 'sys_db_object' && node.action !== 'DELETE');
14
+ if (dataTables.length > 0) {
15
+ console.log(chalk_1.default.cyan.bold('\n📦 DATA SCHEMA'));
16
+ const table = new cli_table3_1.default({
17
+ head: [
18
+ chalk_1.default.cyan('Table Name'),
19
+ chalk_1.default.cyan('Status'),
20
+ chalk_1.default.cyan('Source File')
21
+ ],
22
+ wordWrap: true,
23
+ style: { head: [], border: ['gray'] }
24
+ });
25
+ dataTables.forEach(tableNode => {
26
+ const statusLabel = tableNode.action === 'DELETE' ? chalk_1.default.red('DELETE') : chalk_1.default.green('ACTIVE');
27
+ table.push([
28
+ chalk_1.default.white(tableNode.name),
29
+ statusLabel,
30
+ chalk_1.default.gray(tableNode.source.file)
31
+ ]);
32
+ });
33
+ console.log(table.toString());
34
+ }
35
+ // ===== SECTION 2: SCHEMA RELATIONSHIPS =====
36
+ const schemaEdges = graph.edges.filter(edge => edge.type === 'schema_relationship');
37
+ if (schemaEdges.length > 0) {
38
+ console.log(chalk_1.default.cyan.bold('\n🔗 SCHEMA RELATIONSHIPS (Reference Fields)'));
39
+ const table = new cli_table3_1.default({
40
+ head: [
41
+ chalk_1.default.cyan('Field'),
42
+ chalk_1.default.cyan('From Table'),
43
+ chalk_1.default.cyan('Target Table')
44
+ ],
45
+ wordWrap: true,
46
+ style: { head: [], border: ['gray'] }
47
+ });
48
+ schemaEdges.forEach(edge => {
49
+ const fromNode = graph.nodes.find(node => node.id === edge.from);
50
+ const fieldName = fromNode?.attributes?.element || edge.via || 'unknown';
51
+ const fromTableName = fromNode?.attributes?.name || 'unknown';
52
+ const targetTableName = edge.to.startsWith('platform:')
53
+ ? edge.to.split(':')[1]
54
+ : graph.nodes.find(node => node.id === edge.to)?.name || edge.to;
55
+ table.push([chalk_1.default.white(fieldName), chalk_1.default.gray(fromTableName), chalk_1.default.magenta(targetTableName)]);
56
+ });
57
+ console.log(table.toString());
58
+ }
59
+ // ===== SECTION 3: LOGIC ATTACHMENTS =====
60
+ const logicArtifacts = graph.nodes.filter(node => ['sys_script_client', 'sys_ui_action', 'sys_script', 'sys_script_include', 'sys_ui_policy'].includes(node.table) &&
61
+ node.action !== 'DELETE' &&
62
+ node.source.file);
63
+ if (logicArtifacts.length > 0) {
64
+ console.log(chalk_1.default.cyan.bold('\n⚙️ LOGIC ATTACHMENTS (Scripts & Actions)'));
65
+ const table = new cli_table3_1.default({
66
+ head: [
67
+ chalk_1.default.cyan('Artifact Name'),
68
+ chalk_1.default.cyan('Target Table'),
69
+ chalk_1.default.cyan('Type'),
70
+ chalk_1.default.cyan('Artifact Type')
71
+ ],
72
+ wordWrap: true,
73
+ style: { head: [], border: ['gray'] }
74
+ });
75
+ logicArtifacts.forEach(logicNode => {
76
+ const dependencyEdge = graph.edges.find(edge => edge.from === logicNode.id && edge.type === 'dependency');
77
+ const targetTable = dependencyEdge
78
+ ? (dependencyEdge.to.startsWith('platform:')
79
+ ? dependencyEdge.to.split(':')[1]
80
+ : graph.nodes.find(node => node.id === dependencyEdge.to)?.name || dependencyEdge.to)
81
+ : '[global]';
82
+ const typeLabel = logicNode.attributes?.type ? `[${logicNode.attributes.type}]` : '[action]';
83
+ const artifactType = (() => {
84
+ switch (logicNode.table) {
85
+ case 'sys_script_client': return 'client script';
86
+ case 'sys_script': return 'script';
87
+ case 'sys_ui_action': return 'ui action';
88
+ case 'sys_script_include': return 'script include';
89
+ case 'sys_ui_policy': return 'ui policy';
90
+ default: return logicNode.table;
91
+ }
92
+ })();
93
+ table.push([
94
+ chalk_1.default.white(logicNode.name),
95
+ chalk_1.default.gray(targetTable),
96
+ chalk_1.default.gray(typeLabel),
97
+ artifactType
98
+ ]);
99
+ });
100
+ console.log(table.toString());
101
+ }
102
+ // ===== SECTION 4: SECURITY & ACCESS (ACLs) =====
103
+ const aclNodes = graph.nodes.filter(node => node.table === 'sys_security_acl' && node.action !== 'DELETE');
104
+ if (aclNodes.length > 0) {
105
+ console.log(chalk_1.default.cyan.bold('\n🔒 SECURITY & ACCESS (ACLs)'));
106
+ const table = new cli_table3_1.default({
107
+ head: [
108
+ chalk_1.default.cyan('ACL'),
109
+ chalk_1.default.cyan('Level'),
110
+ chalk_1.default.cyan('Operation'),
111
+ chalk_1.default.cyan('Roles'),
112
+ chalk_1.default.cyan('Target Table')
113
+ ],
114
+ wordWrap: true,
115
+ style: { head: [], border: ['gray'] }
116
+ });
117
+ aclNodes.forEach(aclNode => {
118
+ const aclEdge = graph.edges.find(edge => edge.from === aclNode.id && edge.type === 'dependency');
119
+ const targetTable = aclEdge?.targetTable || (aclEdge?.to.startsWith('platform:') ? aclEdge.to.split(':')[1] : 'unknown');
120
+ const [nameTable, nameField] = aclNode.name.includes('.') ? aclNode.name.split('.') : [aclNode.name, '*'];
121
+ const isFieldLevel = nameField !== '*' && nameField !== undefined;
122
+ const level = isFieldLevel ? 'Field-level' : 'Table-level';
123
+ const operation = (aclNode.attributes?.operation || 'read').toUpperCase();
124
+ const roleEdges = graph.edges.filter(edge => edge.from === aclNode.id && edge.type === 'composition');
125
+ const roleNames = roleEdges
126
+ .map(edge => {
127
+ const roleNode = graph.nodes.find(node => node.id === edge.to);
128
+ return roleNode?.attributes?.name || roleNode?.name || 'unknown';
129
+ })
130
+ .filter(Boolean)
131
+ .sort();
132
+ const rolesDisplay = roleNames.length > 0 ? roleNames.join(', ') : chalk_1.default.gray('No roles specified');
133
+ const aclLabel = `${operation} on ${nameTable}${isFieldLevel ? `.${nameField}` : ''}`;
134
+ table.push([
135
+ chalk_1.default.yellow(aclLabel),
136
+ chalk_1.default.gray(level),
137
+ chalk_1.default.gray(operation),
138
+ rolesDisplay,
139
+ chalk_1.default.magenta(targetTable)
140
+ ]);
141
+ });
142
+ console.log(table.toString());
143
+ }
144
+ // ===== SECTION 5: PROJECT INSIGHTS =====
145
+ console.log(chalk_1.default.cyan.bold('\n🧠 PROJECT INSIGHTS'));
146
+ const platformDeps = graph.edges.filter(edge => edge.to.startsWith('platform:')).length;
147
+ const internalDeps = graph.edges.filter(edge => !edge.to.startsWith('platform:')).length;
148
+ const userArtifactTables = [
149
+ 'sys_db_object',
150
+ 'sys_script_client',
151
+ 'sys_script',
152
+ 'sys_business_rule',
153
+ 'sys_ui_action',
154
+ 'sys_security_acl',
155
+ 'sys_script_include'
156
+ ];
157
+ const userArtifacts = graph.nodes.filter(node => node.action !== 'DELETE' &&
158
+ node.source.file &&
159
+ !node.id.startsWith('platform:') &&
160
+ userArtifactTables.includes(node.table));
161
+ console.log(` • Total User Artifacts: ${chalk_1.default.cyan(userArtifacts.length)}`);
162
+ const countsByType = userArtifacts.reduce((acc, node) => {
163
+ acc[node.table] = (acc[node.table] || 0) + 1;
164
+ return acc;
165
+ }, {});
166
+ console.log(` • User Artifacts Breakdown: ${JSON.stringify(countsByType, null, 2)}`);
167
+ console.log(` • Platform Dependencies: ${chalk_1.default.magenta(platformDeps)}`);
168
+ console.log(` • Internal References: ${chalk_1.default.white(internalDeps)}`);
169
+ console.log(` • Total Artifacts: ${chalk_1.default.cyan(graph.nodes.length)}`);
170
+ console.log(` • Total Relationships: ${chalk_1.default.cyan(graph.edges.length)}`);
171
+ console.log(chalk_1.default.gray('═'.repeat(80)) + '\n');
172
+ }