intelwatch 1.2.0 → 1.3.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.
@@ -1,124 +1,200 @@
1
1
  import { writeFileSync } from 'fs';
2
- import { join } from 'path';
2
+ import { resolve } from 'path';
3
+ import * as XLSX from 'xlsx';
4
+ import { isPro, requirePro, getLimits, applyFreeLimit, printPaywallAndExit } from '../license.js';
5
+
6
+ // ── JSON Export ──────────────────────────────────────────────────────────────
3
7
 
4
8
  /**
5
- * Export data to JSON format
9
+ * Export data to JSON format.
10
+ * @param {any} data
11
+ * @param {string|null} outputPath
12
+ * @returns {string} Status message
6
13
  */
7
14
  export function exportToJSON(data, outputPath = null) {
8
15
  const jsonStr = JSON.stringify(data, null, 2);
9
-
16
+
10
17
  if (outputPath) {
11
- writeFileSync(outputPath, jsonStr, 'utf8');
18
+ writeFileSync(resolve(outputPath), jsonStr, 'utf8');
12
19
  return `Exported to ${outputPath}`;
13
20
  }
14
-
15
- // Print to console if no path specified
21
+
16
22
  console.log(jsonStr);
17
23
  return 'JSON output printed to console';
18
24
  }
19
25
 
26
+ // ── CSV Export ────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Escape a value for CSV (RFC 4180 compliant).
30
+ * @param {any} value
31
+ * @returns {string}
32
+ */
33
+ function escapeCSV(value) {
34
+ if (value == null) return '';
35
+ let str = typeof value === 'object'
36
+ ? (Array.isArray(value) ? value.join('; ') : JSON.stringify(value))
37
+ : String(value);
38
+ // Always quote if contains comma, double-quote, newline, or leading/trailing whitespace
39
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r') || str !== str.trim()) {
40
+ str = `"${str.replace(/"/g, '""')}"`;
41
+ }
42
+ return str;
43
+ }
44
+
20
45
  /**
21
- * Export data to CSV format
22
- * Supports both flat objects and nested structures
46
+ * Export an array of objects to CSV.
47
+ * @param {Array<object>} data
48
+ * @param {string|null} outputPath
49
+ * @param {{ headers?: string[], separator?: string }} options
50
+ * @returns {string} Status message
23
51
  */
24
52
  export function exportToCSV(data, outputPath = null, options = {}) {
25
53
  if (!Array.isArray(data)) {
26
54
  throw new Error('CSV export requires an array of objects');
27
55
  }
28
-
56
+
57
+ const sep = options.separator || ',';
58
+
29
59
  if (data.length === 0) {
30
- const emptyCSV = options.headers ? options.headers.join(',') + '\n' : '';
60
+ const empty = options.headers ? options.headers.join(sep) + '\n' : '';
31
61
  if (outputPath) {
32
- writeFileSync(outputPath, emptyCSV, 'utf8');
62
+ writeFileSync(resolve(outputPath), empty, 'utf8');
33
63
  return `Empty CSV exported to ${outputPath}`;
34
64
  }
35
- console.log(emptyCSV);
65
+ console.log(empty);
36
66
  return 'Empty CSV output';
37
67
  }
38
68
 
39
- // Auto-detect headers from first object if not provided
40
69
  const headers = options.headers || Object.keys(data[0]);
41
-
42
- // CSV header row
43
- const csvRows = [headers.join(',')];
44
-
45
- // CSV data rows
70
+ const rows = [headers.join(sep)];
71
+
46
72
  for (const item of data) {
47
- const row = headers.map(header => {
48
- let value = item[header];
49
-
50
- // Handle nested objects/arrays
51
- if (typeof value === 'object' && value !== null) {
52
- if (Array.isArray(value)) {
53
- value = value.join('; ');
54
- } else {
55
- value = JSON.stringify(value);
56
- }
57
- }
58
-
59
- // Escape CSV values
60
- value = String(value || '');
61
- if (value.includes(',') || value.includes('"') || value.includes('\n')) {
62
- value = `"${value.replace(/"/g, '""')}"`;
63
- }
64
-
65
- return value;
66
- });
67
-
68
- csvRows.push(row.join(','));
73
+ const row = headers.map(h => escapeCSV(item[h]));
74
+ rows.push(row.join(sep));
69
75
  }
70
-
71
- const csvStr = csvRows.join('\n') + '\n';
72
-
76
+
77
+ const csvStr = rows.join('\n') + '\n';
78
+
73
79
  if (outputPath) {
74
- writeFileSync(outputPath, csvStr, 'utf8');
80
+ writeFileSync(resolve(outputPath), csvStr, 'utf8');
75
81
  return `CSV exported to ${outputPath}`;
76
82
  }
77
-
83
+
78
84
  console.log(csvStr);
79
85
  return 'CSV output printed to console';
80
86
  }
81
87
 
88
+ // ── XLS Export (xlsx format) ─────────────────────────────────────────────────
89
+
90
+ /**
91
+ * Export data to XLSX (Excel) format.
92
+ * @param {Array<object>} data — rows to export
93
+ * @param {string} outputPath — output file (.xlsx)
94
+ * @param {{ sheetName?: string, headers?: string[] }} options
95
+ * @returns {string} Status message
96
+ */
97
+ export function exportToXLS(data, outputPath, options = {}) {
98
+ if (!outputPath) throw new Error('XLS export requires an output path');
99
+ if (!Array.isArray(data)) throw new Error('XLS export requires an array of objects');
100
+
101
+ const sheetName = options.sheetName || 'Data';
102
+ const headers = options.headers || (data.length > 0 ? Object.keys(data[0]) : []);
103
+
104
+ // Build worksheet from array of arrays (header row + data rows)
105
+ const aoa = [headers];
106
+ for (const item of data) {
107
+ aoa.push(headers.map(h => {
108
+ const v = item[h];
109
+ if (v == null) return '';
110
+ if (Array.isArray(v)) return v.join('; ');
111
+ if (typeof v === 'object') return JSON.stringify(v);
112
+ return v;
113
+ }));
114
+ }
115
+
116
+ const wb = XLSX.utils.book_new();
117
+ const ws = XLSX.utils.aoa_to_sheet(aoa);
118
+
119
+ // Auto-size columns (approximate)
120
+ ws['!cols'] = headers.map((h, i) => {
121
+ let maxLen = h.length;
122
+ for (const row of aoa.slice(1)) {
123
+ const cellLen = String(row[i] || '').length;
124
+ if (cellLen > maxLen) maxLen = cellLen;
125
+ }
126
+ return { wch: Math.min(maxLen + 2, 60) };
127
+ });
128
+
129
+ XLSX.utils.book_append_sheet(wb, ws, sheetName);
130
+ XLSX.writeFile(wb, resolve(outputPath));
131
+ return `XLS exported to ${outputPath}`;
132
+ }
133
+
134
+ // ── PDF Export (via @recognity/pdf-report) ────────────────────────────────────
135
+
136
+ /**
137
+ * Export data to a PDF report.
138
+ * @param {object} data — structured report data
139
+ * @param {string} outputPath — output file (.pdf)
140
+ * @param {{ type?: string, title?: string, branding?: object }} options
141
+ * @returns {Promise<string>} Status message
142
+ */
143
+ export async function exportToPDF(data, outputPath, options = {}) {
144
+ if (!outputPath) throw new Error('PDF export requires an output path');
145
+
146
+ const { generatePDF } = await import('@recognity/pdf-report');
147
+ await generatePDF({
148
+ type: options.type || 'intel-report',
149
+ title: options.title || 'IntelWatch Report',
150
+ data,
151
+ output: resolve(outputPath),
152
+ branding: options.branding,
153
+ });
154
+
155
+ return `PDF exported to ${outputPath}`;
156
+ }
157
+
158
+ // ── Flatten Object ───────────────────────────────────────────────────────────
159
+
82
160
  /**
83
- * Flatten nested objects for CSV export
84
- * Example: { user: { name: 'John', age: 30 } } -> { 'user.name': 'John', 'user.age': 30 }
161
+ * Flatten nested objects for tabular export.
162
+ * { user: { name: 'Jo' } } { 'user.name': 'Jo' }
85
163
  */
86
164
  export function flattenObject(obj, prefix = '', result = {}) {
87
- for (const key in obj) {
88
- if (obj.hasOwnProperty(key)) {
89
- const newKey = prefix ? `${prefix}.${key}` : key;
90
-
91
- if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
92
- flattenObject(obj[key], newKey, result);
93
- } else {
94
- result[newKey] = obj[key];
95
- }
165
+ for (const key of Object.keys(obj)) {
166
+ const newKey = prefix ? `${prefix}.${key}` : key;
167
+ const val = obj[key];
168
+ if (val && typeof val === 'object' && !Array.isArray(val) && !(val instanceof Date)) {
169
+ flattenObject(val, newKey, result);
170
+ } else {
171
+ result[newKey] = val;
96
172
  }
97
173
  }
98
174
  return result;
99
175
  }
100
176
 
177
+ // ── Format for Export ────────────────────────────────────────────────────────
178
+
101
179
  /**
102
- * Format data for export based on command type
180
+ * Format data for export based on command type.
181
+ * @param {any} data
182
+ * @param {string} commandType
183
+ * @returns {Array<object>}
103
184
  */
104
185
  export function formatForExport(data, commandType) {
105
186
  switch (commandType) {
106
- case 'check':
107
- return formatCheckData(data);
108
- case 'digest':
109
- return formatDigestData(data);
110
- case 'report':
111
- return formatReportData(data);
112
- case 'profile':
113
- return formatProfileData(data);
114
- default:
115
- return data;
187
+ case 'check': return formatCheckData(data);
188
+ case 'digest': return formatDigestData(data);
189
+ case 'report': return formatReportData(data);
190
+ case 'profile': return formatProfileData(data);
191
+ case 'discover': return formatDiscoverData(data);
192
+ default: return Array.isArray(data) ? data : [data];
116
193
  }
117
194
  }
118
195
 
119
196
  function formatCheckData(data) {
120
197
  if (!Array.isArray(data)) return [data];
121
-
122
198
  return data.map(item => ({
123
199
  trackerId: item.id || item.trackerId,
124
200
  name: item.name,
@@ -129,41 +205,32 @@ function formatCheckData(data) {
129
205
  changes: Array.isArray(item.changes) ? item.changes.length : 0,
130
206
  techStack: Array.isArray(item.techStack) ? item.techStack.join('; ') : '',
131
207
  seoScore: item.seoScore || null,
132
- sentiment: item.sentiment || null
208
+ sentiment: item.sentiment || null,
133
209
  }));
134
210
  }
135
211
 
136
212
  function formatDigestData(data) {
137
213
  if (!Array.isArray(data)) return [data];
138
-
139
214
  return data.map(item => flattenObject({
140
- tracker: {
141
- id: item.trackerId,
142
- name: item.name,
143
- type: item.type
144
- },
215
+ tracker: { id: item.trackerId, name: item.name, type: item.type },
145
216
  changes: {
146
217
  total: item.changes?.length || 0,
147
218
  critical: item.changes?.filter(c => c.severity === 'critical').length || 0,
148
219
  major: item.changes?.filter(c => c.severity === 'major').length || 0,
149
- minor: item.changes?.filter(c => c.severity === 'minor').length || 0
220
+ minor: item.changes?.filter(c => c.severity === 'minor').length || 0,
150
221
  },
151
- summary: item.summary || ''
222
+ summary: item.summary || '',
152
223
  }));
153
224
  }
154
225
 
155
226
  function formatReportData(data) {
156
- // For reports, export the raw data structure
157
227
  return Array.isArray(data) ? data : [data];
158
228
  }
159
229
 
160
230
  function formatProfileData(data) {
161
231
  if (!data) return [];
162
-
163
232
  const profile = Array.isArray(data) ? data[0] : data;
164
-
165
- const flattened = {
166
- // Company identity
233
+ return [{
167
234
  siren: profile.siren,
168
235
  name: profile.name || profile.identity?.name,
169
236
  legalForm: profile.identity?.formeJuridique,
@@ -171,31 +238,86 @@ function formatProfileData(data) {
171
238
  nafLabel: profile.identity?.nafLabel,
172
239
  creationDate: profile.identity?.dateCreation,
173
240
  address: profile.identity?.adresse,
174
-
175
- // Financial data (latest year)
176
241
  revenue: profile.financialHistory?.[0]?.revenue,
177
242
  netIncome: profile.financialHistory?.[0]?.netIncome,
178
243
  employees: profile.financialHistory?.[0]?.employees,
179
244
  year: profile.financialHistory?.[0]?.year,
180
-
181
- // AI analysis
182
245
  executiveSummary: profile.executiveSummary,
183
246
  healthScore: profile.healthScore?.score,
184
247
  riskLevel: profile.riskAssessment?.overall,
185
-
186
- // Strengths (concatenated)
187
248
  strengths: profile.strengths?.map(s => s.text || s).join('; ') || '',
188
-
189
- // Weaknesses (concatenated)
190
249
  weaknesses: profile.weaknesses?.map(w => w.text || w).join('; ') || '',
191
-
192
- // Competitors (concatenated)
193
250
  competitors: profile.competitors?.map(c => c.name).join('; ') || '',
194
-
195
- // Group info
196
251
  subsidiariesCount: profile.subsidiaries?.length || 0,
197
- groupRevenue: profile.groupStructure?.consolidatedRevenue
198
- };
199
-
200
- return [flattened];
201
- }
252
+ groupRevenue: profile.groupStructure?.consolidatedRevenue,
253
+ }];
254
+ }
255
+
256
+ function formatDiscoverData(data) {
257
+ if (!Array.isArray(data)) return [data];
258
+ return data.map(item => ({
259
+ name: item.name || item.domain,
260
+ domain: item.domain,
261
+ url: item.url,
262
+ relevanceScore: item.relevanceScore || item.score,
263
+ description: item.description || item.snippet || '',
264
+ category: item.category || '',
265
+ }));
266
+ }
267
+
268
+ // ── Unified Export Handler ───────────────────────────────────────────────────
269
+
270
+ /**
271
+ * Handle --export flag for any command.
272
+ *
273
+ * Free tier: json, csv (capped at 50 rows)
274
+ * Pro tier: json, csv (unlimited), xls, pdf
275
+ *
276
+ * @param {string} format — 'json' | 'csv' | 'xls' | 'pdf'
277
+ * @param {any} data — data to export
278
+ * @param {{ output?: string, commandType?: string, pdfOptions?: object }} options
279
+ * @returns {Promise<string>} Status message
280
+ */
281
+ export async function handleExport(format, data, options = {}) {
282
+ const fmt = format.toLowerCase();
283
+ const limits = getLimits();
284
+
285
+ // Gate Pro-only formats — clean paywall exit
286
+ if (['xls', 'xlsx', 'excel', 'pdf'].includes(fmt)) {
287
+ if (!isPro()) {
288
+ printPaywallAndExit(`Export to ${fmt.toUpperCase()}`);
289
+ }
290
+ }
291
+
292
+ let formatted = options.commandType
293
+ ? formatForExport(data, options.commandType)
294
+ : (Array.isArray(data) ? data : [data]);
295
+
296
+ // Apply Free-tier row cap on CSV
297
+ if (fmt === 'csv' && Array.isArray(formatted)) {
298
+ formatted = applyFreeLimit(formatted, limits.csvMaxRows, 'CSV export rows');
299
+ }
300
+
301
+ switch (fmt) {
302
+ case 'json': {
303
+ const outPath = options.output?.replace(/\.[^.]+$/, '.json') || null;
304
+ return exportToJSON(formatted, outPath);
305
+ }
306
+ case 'csv': {
307
+ const outPath = options.output?.replace(/\.[^.]+$/, '.csv') || null;
308
+ return exportToCSV(formatted, outPath);
309
+ }
310
+ case 'xls':
311
+ case 'xlsx':
312
+ case 'excel': {
313
+ const outPath = options.output?.replace(/\.[^.]+$/, '.xlsx') || 'export.xlsx';
314
+ return exportToXLS(formatted, outPath);
315
+ }
316
+ case 'pdf': {
317
+ const outPath = options.output?.replace(/\.[^.]+$/, '.pdf') || 'export.pdf';
318
+ return exportToPDF(options.pdfData || data, outPath, options.pdfOptions || {});
319
+ }
320
+ default:
321
+ throw new Error(`Unsupported export format: ${format}. Use json, csv, xls, or pdf.`);
322
+ }
323
+ }