@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.
- package/LICENSE +36 -0
- package/README.md +447 -0
- package/bin/fluent-graph.js +9 -0
- package/dist/cli/commands/blast.d.ts +2 -0
- package/dist/cli/commands/blast.js +202 -0
- package/dist/cli/commands/extract.d.ts +6 -0
- package/dist/cli/commands/extract.js +178 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +61 -0
- package/dist/cli/utils/banner.d.ts +2 -0
- package/dist/cli/utils/banner.js +25 -0
- package/dist/cli/utils/display.d.ts +2 -0
- package/dist/cli/utils/display.js +172 -0
- package/dist/extractor/edges.d.ts +11 -0
- package/dist/extractor/edges.js +120 -0
- package/dist/extractor/index.d.ts +2 -0
- package/dist/extractor/index.js +24 -0
- package/dist/extractor/metadata.d.ts +6 -0
- package/dist/extractor/metadata.js +37 -0
- package/dist/extractor/nodes.d.ts +2 -0
- package/dist/extractor/nodes.js +64 -0
- package/dist/graph/extractor.d.ts +6 -0
- package/dist/graph/extractor.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/output/writer.d.ts +9 -0
- package/dist/output/writer.js +28 -0
- package/dist/sdk/detector.d.ts +6 -0
- package/dist/sdk/detector.js +32 -0
- package/dist/sdk/loader.d.ts +5 -0
- package/dist/sdk/loader.js +15 -0
- package/dist/types/graphs.d.ts +35 -0
- package/dist/types/graphs.js +2 -0
- package/package.json +53 -0
|
@@ -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,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,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,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,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
|
+
}
|