huntr-cli 1.0.9

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.
Files changed (117) hide show
  1. package/.env.example +7 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  5. package/.github/labels.json +92 -0
  6. package/.github/pull_request_template.md +64 -0
  7. package/.github/workflows/ci.yml +87 -0
  8. package/.github/workflows/labels.yml +27 -0
  9. package/.github/workflows/manual-publish.yml +105 -0
  10. package/.github/workflows/publish.yml +57 -0
  11. package/.github/workflows/release.yml +124 -0
  12. package/.github/workflows/security-audit.yml +44 -0
  13. package/.husky/pre-commit +12 -0
  14. package/.husky/pre-push +27 -0
  15. package/.lintstagedrc.json +3 -0
  16. package/AGENTS.md +449 -0
  17. package/CHANGELOG.md +38 -0
  18. package/CHANGES.md +259 -0
  19. package/LICENSE +15 -0
  20. package/PUBLISHING.md +191 -0
  21. package/README.md +385 -0
  22. package/ROADMAP.md +158 -0
  23. package/SETUP-COMPLETE.md +446 -0
  24. package/WORKFLOW-SUMMARY.md +368 -0
  25. package/completions/_huntr +168 -0
  26. package/completions/huntr.1 +266 -0
  27. package/completions/huntr.bash +91 -0
  28. package/dist/api/client.d.ts +14 -0
  29. package/dist/api/client.d.ts.map +1 -0
  30. package/dist/api/client.js +74 -0
  31. package/dist/api/client.js.map +1 -0
  32. package/dist/api/personal/activities.d.ts +20 -0
  33. package/dist/api/personal/activities.d.ts.map +1 -0
  34. package/dist/api/personal/activities.js +50 -0
  35. package/dist/api/personal/activities.js.map +1 -0
  36. package/dist/api/personal/boards.d.ts +9 -0
  37. package/dist/api/personal/boards.d.ts.map +1 -0
  38. package/dist/api/personal/boards.js +16 -0
  39. package/dist/api/personal/boards.js.map +1 -0
  40. package/dist/api/personal/index.d.ts +17 -0
  41. package/dist/api/personal/index.d.ts.map +1 -0
  42. package/dist/api/personal/index.js +37 -0
  43. package/dist/api/personal/index.js.map +1 -0
  44. package/dist/api/personal/jobs.d.ts +13 -0
  45. package/dist/api/personal/jobs.d.ts.map +1 -0
  46. package/dist/api/personal/jobs.js +31 -0
  47. package/dist/api/personal/jobs.js.map +1 -0
  48. package/dist/api/personal/user.d.ts +8 -0
  49. package/dist/api/personal/user.d.ts.map +1 -0
  50. package/dist/api/personal/user.js +13 -0
  51. package/dist/api/personal/user.js.map +1 -0
  52. package/dist/cli.d.ts +3 -0
  53. package/dist/cli.d.ts.map +1 -0
  54. package/dist/cli.js +501 -0
  55. package/dist/cli.js.map +1 -0
  56. package/dist/commands/capture-session.d.ts +10 -0
  57. package/dist/commands/capture-session.d.ts.map +1 -0
  58. package/dist/commands/capture-session.js +478 -0
  59. package/dist/commands/capture-session.js.map +1 -0
  60. package/dist/config/clerk-session-manager.d.ts +44 -0
  61. package/dist/config/clerk-session-manager.d.ts.map +1 -0
  62. package/dist/config/clerk-session-manager.js +232 -0
  63. package/dist/config/clerk-session-manager.js.map +1 -0
  64. package/dist/config/config-manager.d.ts +15 -0
  65. package/dist/config/config-manager.d.ts.map +1 -0
  66. package/dist/config/config-manager.js +51 -0
  67. package/dist/config/config-manager.js.map +1 -0
  68. package/dist/config/keychain-manager.d.ts +6 -0
  69. package/dist/config/keychain-manager.d.ts.map +1 -0
  70. package/dist/config/keychain-manager.js +37 -0
  71. package/dist/config/keychain-manager.js.map +1 -0
  72. package/dist/config/token-capture.d.ts +11 -0
  73. package/dist/config/token-capture.d.ts.map +1 -0
  74. package/dist/config/token-capture.js +252 -0
  75. package/dist/config/token-capture.js.map +1 -0
  76. package/dist/config/token-manager.d.ts +38 -0
  77. package/dist/config/token-manager.d.ts.map +1 -0
  78. package/dist/config/token-manager.js +153 -0
  79. package/dist/config/token-manager.js.map +1 -0
  80. package/dist/lib/list-options.d.ts +69 -0
  81. package/dist/lib/list-options.d.ts.map +1 -0
  82. package/dist/lib/list-options.js +299 -0
  83. package/dist/lib/list-options.js.map +1 -0
  84. package/dist/types/personal.d.ts +113 -0
  85. package/dist/types/personal.d.ts.map +1 -0
  86. package/dist/types/personal.js +4 -0
  87. package/dist/types/personal.js.map +1 -0
  88. package/docs/AUTOMATIC-PUBLISHING.md +520 -0
  89. package/docs/CHANGELOG-AUTOMATION.md +418 -0
  90. package/docs/CI-CD-SETUP.md +582 -0
  91. package/docs/DEV-SETUP.md +512 -0
  92. package/docs/ENHANCEMENT-PLAN.md +204 -0
  93. package/docs/ENTITY-TYPES.md +462 -0
  94. package/docs/GITHUB-ACTIONS-GUIDE.md +367 -0
  95. package/docs/NPM-PUBLISHING.md +324 -0
  96. package/docs/OUTPUT-EXAMPLES.md +414 -0
  97. package/docs/OUTPUT-FORMATS.md +299 -0
  98. package/docs/TESTING.md +216 -0
  99. package/eslint.config.js +68 -0
  100. package/package.json +64 -0
  101. package/src/api/client.ts +88 -0
  102. package/src/api/personal/activities.ts +66 -0
  103. package/src/api/personal/boards.ts +14 -0
  104. package/src/api/personal/index.ts +25 -0
  105. package/src/api/personal/jobs.ts +33 -0
  106. package/src/api/personal/user.ts +10 -0
  107. package/src/cli.ts +487 -0
  108. package/src/commands/capture-session.ts +582 -0
  109. package/src/config/clerk-session-manager.ts +263 -0
  110. package/src/config/config-manager.ts +56 -0
  111. package/src/config/keychain-manager.ts +30 -0
  112. package/src/config/token-capture.ts +233 -0
  113. package/src/config/token-manager.ts +139 -0
  114. package/src/lib/list-options.ts +370 -0
  115. package/src/types/personal.ts +114 -0
  116. package/tests/example.test.ts +130 -0
  117. package/tsconfig.json +19 -0
@@ -0,0 +1,139 @@
1
+ import { password } from '@inquirer/prompts';
2
+ import { ConfigManager } from './config-manager';
3
+ import { KeychainManager } from './keychain-manager';
4
+ import { ClerkSessionManager } from './clerk-session-manager';
5
+ import type { TokenProvider } from '../api/client';
6
+
7
+ export interface TokenOptions {
8
+ token?: string;
9
+ usePrompt?: boolean;
10
+ }
11
+
12
+ export class TokenManager {
13
+ private configManager: ConfigManager;
14
+ private keychainManager: KeychainManager;
15
+ readonly clerkSession: ClerkSessionManager;
16
+
17
+ constructor() {
18
+ this.configManager = new ConfigManager();
19
+ this.keychainManager = new KeychainManager();
20
+ this.clerkSession = new ClerkSessionManager();
21
+ }
22
+
23
+ /**
24
+ * Returns a TokenProvider for use with HuntrPersonalApi.
25
+ *
26
+ * Resolution order:
27
+ * 1. CLI --token argument (static, passed via options)
28
+ * 2. HUNTR_API_TOKEN env var (static)
29
+ * 3. Stored Clerk session cookie → auto-refresh on every call
30
+ * 4. Static token from config file or keychain
31
+ * 5. Interactive prompt
32
+ */
33
+ async getTokenProvider(options: TokenOptions = {}): Promise<TokenProvider> {
34
+ // 1. CLI argument — static token
35
+ if (options.token) {
36
+ return options.token;
37
+ }
38
+
39
+ // 2. Environment variable — static token
40
+ const envToken = process.env.HUNTR_API_TOKEN;
41
+ if (envToken) {
42
+ return envToken;
43
+ }
44
+
45
+ // 3. Clerk session — dynamic refresh
46
+ if (await this.clerkSession.hasSession()) {
47
+ return () => this.clerkSession.getFreshToken();
48
+ }
49
+
50
+ // 4. Static token from config or keychain
51
+ const configToken = this.configManager.getToken();
52
+ if (configToken) return configToken;
53
+
54
+ const keychainToken = await this.keychainManager.getToken();
55
+ if (keychainToken) return keychainToken;
56
+
57
+ // 5. Interactive prompt — static token (short-lived, but usable)
58
+ if (options.usePrompt !== false) {
59
+ const promptedToken = await password({
60
+ message: 'Enter your Huntr API token:',
61
+ mask: '*',
62
+ });
63
+
64
+ if (promptedToken) {
65
+ const saveChoice = await this.promptSaveLocation();
66
+ if (saveChoice !== 'none') {
67
+ await this.saveToken(promptedToken, saveChoice);
68
+ }
69
+ return promptedToken;
70
+ }
71
+ }
72
+
73
+ throw new Error(
74
+ 'No Huntr credentials found. Options:\n' +
75
+ ' • Clerk session (recommended): huntr config set-session <__session-cookie>\n' +
76
+ ' • Static token: huntr config set-token <token> [--keychain]\n' +
77
+ ' • CLI flag: huntr --token <token> <command>\n' +
78
+ ' • Environment: HUNTR_API_TOKEN=<token> huntr <command>',
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Legacy helper — returns a resolved static token string.
84
+ * Use getTokenProvider() for new code so session refresh works.
85
+ */
86
+ async getToken(options: TokenOptions = {}): Promise<string> {
87
+ if (options.token) return options.token;
88
+ const envToken = process.env.HUNTR_API_TOKEN;
89
+ if (envToken) return envToken;
90
+ const configToken = this.configManager.getToken();
91
+ if (configToken) return configToken;
92
+ const keychainToken = await this.keychainManager.getToken();
93
+ if (keychainToken) return keychainToken;
94
+ throw new Error('No static token found. Try huntr config set-token or set-session.');
95
+ }
96
+
97
+ private async promptSaveLocation(): Promise<'config' | 'keychain' | 'none'> {
98
+ const { select } = await import('@inquirer/prompts');
99
+ return await select({
100
+ message: 'Where would you like to save this token?',
101
+ choices: [
102
+ { name: 'Save to config file (~/.huntr/config.json)', value: 'config' as const },
103
+ { name: 'Save to macOS Keychain (secure)', value: 'keychain' as const },
104
+ { name: 'Do not save (enter each time)', value: 'none' as const },
105
+ ],
106
+ }) as 'config' | 'keychain' | 'none';
107
+ }
108
+
109
+ async saveToken(token: string, location: 'config' | 'keychain'): Promise<void> {
110
+ if (location === 'config') {
111
+ this.configManager.setToken(token);
112
+ } else {
113
+ await this.keychainManager.setToken(token);
114
+ }
115
+ }
116
+
117
+ async clearToken(location?: 'config' | 'keychain' | 'all'): Promise<void> {
118
+ if (!location || location === 'all' || location === 'config') {
119
+ this.configManager.clearToken();
120
+ }
121
+ if (!location || location === 'all' || location === 'keychain') {
122
+ await this.keychainManager.deleteToken();
123
+ }
124
+ }
125
+
126
+ async showTokenSources(): Promise<{
127
+ env: boolean;
128
+ config: boolean;
129
+ keychain: boolean;
130
+ clerkSession: boolean;
131
+ }> {
132
+ return {
133
+ env: !!process.env.HUNTR_API_TOKEN,
134
+ config: !!this.configManager.getToken(),
135
+ keychain: !!(await this.keychainManager.getToken()),
136
+ clerkSession: await this.clerkSession.hasSession(),
137
+ };
138
+ }
139
+ }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Shared CLI options for list commands (boards, jobs, activities).
3
+ * Provides consistent flag parsing and validation across all list subcommands.
4
+ */
5
+
6
+ export type OutputFormat = 'table' | 'json' | 'csv' | 'pdf' | 'excel';
7
+
8
+ export interface ListOptions {
9
+ /** Output format: table (default), json, csv, pdf, or excel */
10
+ format: OutputFormat;
11
+ /** Number of days to filter to (activities only, default: all) */
12
+ days?: number;
13
+ /** Action types to filter (activities only, comma-separated) */
14
+ types?: string[];
15
+ /** Selected fields to include in output (comma-separated) */
16
+ fields?: string[];
17
+ }
18
+
19
+ /**
20
+ * Parses common list options from CLI arguments.
21
+ * Supports aliases: -f/--format, -d/--days, --json (legacy compatibility), --fields.
22
+ */
23
+ export function parseListOptions(opts: Record<string, unknown>): ListOptions {
24
+ let format: OutputFormat = 'table';
25
+
26
+ // Handle legacy --json flag
27
+ if (opts.json) {
28
+ format = 'json';
29
+ }
30
+ // Handle new --format flag (takes precedence over --json)
31
+ if (opts.format) {
32
+ const fmt = String(opts.format).toLowerCase();
33
+ if (!['table', 'json', 'csv', 'pdf', 'excel'].includes(fmt)) {
34
+ throw new Error(`Invalid format: ${fmt}. Must be table, json, csv, pdf, or excel.`);
35
+ }
36
+ format = fmt as OutputFormat;
37
+ }
38
+
39
+ let days: number | undefined;
40
+ if (opts.days) {
41
+ const d = parseInt(String(opts.days), 10);
42
+ if (isNaN(d) || d < 1 || d > 365) {
43
+ throw new Error('Days must be a number between 1 and 365');
44
+ }
45
+ days = d;
46
+ }
47
+
48
+ // Legacy --week flag for activities (maps to 7 days)
49
+ if (opts.week && !days) {
50
+ days = 7;
51
+ }
52
+
53
+ let types: string[] | undefined;
54
+ if (opts.types) {
55
+ types = String(opts.types)
56
+ .split(',')
57
+ .map(t => t.trim())
58
+ .filter(t => t.length > 0);
59
+ }
60
+
61
+ let fields: string[] | undefined;
62
+ if (opts.fields) {
63
+ fields = String(opts.fields)
64
+ .split(',')
65
+ .map(f => f.trim())
66
+ .filter(f => f.length > 0);
67
+ }
68
+
69
+ return { format, days, types, fields };
70
+ }
71
+
72
+ /**
73
+ * Formats table output with consistent column widths.
74
+ */
75
+ export function formatTable<T extends Record<string, unknown>>(rows: T[]): string {
76
+ if (rows.length === 0) return '';
77
+
78
+ const keys = Object.keys(rows[0]);
79
+ const colWidths: Record<string, number> = {};
80
+
81
+ // Calculate column widths
82
+ for (const key of keys) {
83
+ colWidths[key] = key.length;
84
+ for (const row of rows) {
85
+ const val = String(row[key] ?? '');
86
+ colWidths[key] = Math.max(colWidths[key], val.length);
87
+ }
88
+ }
89
+
90
+ // Format header
91
+ const header = keys.map(k => k.padEnd(colWidths[k])).join(' ');
92
+ const divider = keys.map(k => '─'.repeat(colWidths[k])).join(' ');
93
+
94
+ // Format rows
95
+ const lines = [header, divider];
96
+ for (const row of rows) {
97
+ const line = keys.map(k => String(row[k] ?? '').padEnd(colWidths[k])).join(' ');
98
+ lines.push(line);
99
+ }
100
+
101
+ return lines.join('\n');
102
+ }
103
+
104
+ /**
105
+ * Converts array of objects to CSV with proper escaping.
106
+ */
107
+ export function formatCsv<T extends Record<string, unknown>>(rows: T[], headers?: string[]): string {
108
+ if (rows.length === 0) return headers ? headers.join(',') : '';
109
+
110
+ const keys = headers || Object.keys(rows[0]);
111
+ const lines = [keys.map(escapeCsvField).join(',')];
112
+
113
+ for (const row of rows) {
114
+ const values = keys.map(k => {
115
+ const val = row[k];
116
+ return escapeCsvField(String(val ?? ''));
117
+ });
118
+ lines.push(values.join(','));
119
+ }
120
+
121
+ return lines.join('\n');
122
+ }
123
+
124
+ function escapeCsvField(field: string): string {
125
+ if (field.includes(',') || field.includes('"') || field.includes('\n')) {
126
+ return `"${field.replace(/"/g, '""')}"`;
127
+ }
128
+ return field;
129
+ }
130
+
131
+ /**
132
+ * Validates and filters fields based on available fields.
133
+ * If no fields are requested, returns all available fields.
134
+ * Throws an error if requested fields don't exist.
135
+ */
136
+ export function validateFields(availableFields: string[], requestedFields?: string[]): string[] {
137
+ if (!requestedFields || requestedFields.length === 0) {
138
+ return availableFields;
139
+ }
140
+
141
+ const invalid = requestedFields.filter(f => !availableFields.includes(f));
142
+ if (invalid.length > 0) {
143
+ const invalidStr = invalid.join(', ');
144
+ const availableStr = availableFields.join(', ');
145
+ throw new Error(
146
+ `Unknown field(s): ${invalidStr}\nAvailable fields: ${availableStr}`,
147
+ );
148
+ }
149
+
150
+ return requestedFields;
151
+ }
152
+
153
+ /**
154
+ * Formats table output with specific fields and consistent column widths.
155
+ * @param rows Data rows
156
+ * @param headers Field names to include (in order)
157
+ */
158
+ export function formatTableWithFields<T extends Record<string, unknown>>(
159
+ rows: T[],
160
+ headers: string[],
161
+ ): string {
162
+ if (rows.length === 0) return '';
163
+
164
+ const colWidths: Record<string, number> = {};
165
+
166
+ // Calculate column widths
167
+ for (const key of headers) {
168
+ colWidths[key] = key.length;
169
+ for (const row of rows) {
170
+ const val = String(row[key] ?? '');
171
+ colWidths[key] = Math.max(colWidths[key], val.length);
172
+ }
173
+ }
174
+
175
+ // Format header
176
+ const header = headers.map(k => k.padEnd(colWidths[k])).join(' ');
177
+ const divider = headers.map(k => '─'.repeat(colWidths[k])).join(' ');
178
+
179
+ // Format rows
180
+ const lines = [header, divider];
181
+ for (const row of rows) {
182
+ const line = headers.map(k => String(row[k] ?? '').padEnd(colWidths[k])).join(' ');
183
+ lines.push(line);
184
+ }
185
+
186
+ return lines.join('\n');
187
+ }
188
+
189
+ /**
190
+ * Formats CSV output with specific fields.
191
+ * @param rows Data rows
192
+ * @param headers Field names to include (in order)
193
+ */
194
+ export function formatCsvWithFields<T extends Record<string, unknown>>(
195
+ rows: T[],
196
+ headers: string[],
197
+ ): string {
198
+ if (rows.length === 0) return headers.join(',');
199
+
200
+ const lines = [headers.map(escapeCsvField).join(',')];
201
+
202
+ for (const row of rows) {
203
+ const values = headers.map(h => {
204
+ const val = row[h];
205
+ return escapeCsvField(String(val ?? ''));
206
+ });
207
+ lines.push(values.join(','));
208
+ }
209
+
210
+ return lines.join('\n');
211
+ }
212
+
213
+ /**
214
+ * Formats JSON output with specific fields.
215
+ * @param rows Data rows
216
+ * @param headers Field names to include (in order)
217
+ */
218
+ export function formatJsonWithFields<T extends Record<string, unknown>>(
219
+ rows: T[],
220
+ headers: string[],
221
+ ): string {
222
+ const filtered = rows.map(row => {
223
+ const obj: Record<string, unknown> = {};
224
+ for (const header of headers) {
225
+ obj[header] = row[header];
226
+ }
227
+ return obj;
228
+ });
229
+ return JSON.stringify(filtered, null, 2);
230
+ }
231
+
232
+ /**
233
+ * Formats PDF output with specific fields.
234
+ * Requires pdfkit to be installed.
235
+ * @param rows Data rows
236
+ * @param headers Field names to include (in order)
237
+ * @param title Optional title for the PDF
238
+ */
239
+ export function formatPdf<T extends Record<string, unknown>>(
240
+ rows: T[],
241
+ headers: string[],
242
+ title?: string,
243
+ ): Buffer {
244
+ // Dynamically import pdfkit to avoid hard dependency at load time
245
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
246
+ const PDFDocument = require('pdfkit');
247
+
248
+ const doc = new PDFDocument({ margin: 40, size: 'letter' });
249
+ const chunks: Buffer[] = [];
250
+
251
+ // Collect output
252
+ doc.on('data', (chunk: Buffer) => chunks.push(chunk));
253
+
254
+ // Title
255
+ if (title) {
256
+ doc.fontSize(16).font('Helvetica-Bold').text(title, { underline: true });
257
+ doc.moveDown();
258
+ }
259
+
260
+ // Metadata
261
+ const now = new Date();
262
+ doc
263
+ .fontSize(10)
264
+ .font('Helvetica')
265
+ .text(`Generated: ${now.toISOString()}`, { lineGap: 5 });
266
+ doc.moveDown();
267
+
268
+ // Table header
269
+ const colWidth = (doc.page.width - 80) / headers.length;
270
+ const yStart = doc.y;
271
+
272
+ // Header background
273
+ doc.rect(40, yStart, doc.page.width - 80, 25).fill('#e8e8e8');
274
+
275
+ // Header text
276
+ doc.fontSize(10).font('Helvetica-Bold').fillColor('black');
277
+ headers.forEach((header, i) => {
278
+ doc.text(header, 40 + i * colWidth + 5, yStart + 5, {
279
+ width: colWidth - 10,
280
+ ellipsis: true,
281
+ });
282
+ });
283
+
284
+ doc.moveDown(1.5);
285
+
286
+ // Data rows
287
+ doc.font('Helvetica').fontSize(9);
288
+ const rowHeight = 20;
289
+
290
+ for (const row of rows) {
291
+ const yRow = doc.y;
292
+
293
+ // Alternate row background
294
+ if (rows.indexOf(row) % 2 === 0) {
295
+ doc.rect(40, yRow, doc.page.width - 80, rowHeight).fill('#f5f5f5');
296
+ doc.fillColor('black');
297
+ }
298
+
299
+ headers.forEach((header, i) => {
300
+ const val = String(row[header] ?? '');
301
+ doc.text(val, 40 + i * colWidth + 5, yRow + 3, {
302
+ width: colWidth - 10,
303
+ ellipsis: true,
304
+ });
305
+ });
306
+
307
+ doc.moveDown(1.2);
308
+ }
309
+
310
+ // Footer
311
+ doc.fontSize(9).font('Helvetica').text('huntr-cli', { align: 'center' });
312
+
313
+ doc.end();
314
+
315
+ return Buffer.concat(chunks);
316
+ }
317
+
318
+ /**
319
+ * Formats Excel output with specific fields.
320
+ * Requires exceljs to be installed.
321
+ * @param rows Data rows
322
+ * @param headers Field names to include (in order)
323
+ * @param title Optional sheet title
324
+ */
325
+ export async function formatExcel<T extends Record<string, unknown>>(
326
+ rows: T[],
327
+ headers: string[],
328
+ title?: string,
329
+ ): Promise<Buffer> {
330
+ // Dynamically import exceljs to avoid hard dependency at load time
331
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
332
+ const ExcelJS = require('exceljs');
333
+
334
+ const workbook = new ExcelJS.Workbook();
335
+ const worksheet = workbook.addWorksheet(title || 'Data', {
336
+ pageSetup: { paperSize: 9, orientation: 'landscape' },
337
+ });
338
+
339
+ // Add header row
340
+ worksheet.addRow(headers);
341
+ const headerRow = worksheet.getRow(1);
342
+ headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
343
+ headerRow.fill = {
344
+ type: 'pattern',
345
+ pattern: 'solid',
346
+ fgColor: { argb: 'FF4472C4' },
347
+ };
348
+ headerRow.alignment = { horizontal: 'center', vertical: 'center' };
349
+
350
+ // Add data rows
351
+ for (const row of rows) {
352
+ const values = headers.map(h => row[h] ?? '');
353
+ worksheet.addRow(values);
354
+ }
355
+
356
+ // Auto-adjust column widths
357
+ headers.forEach((header, i) => {
358
+ let maxLen = header.length;
359
+ for (const row of rows) {
360
+ const val = String(row[header] ?? '');
361
+ maxLen = Math.max(maxLen, val.length);
362
+ }
363
+ const col = worksheet.getColumn(i + 1);
364
+ col.width = Math.min(maxLen + 2, 50); // Cap at 50 chars
365
+ });
366
+
367
+ // Generate buffer
368
+ const buffer = await workbook.xlsx.writeBuffer();
369
+ return buffer as Buffer;
370
+ }
@@ -0,0 +1,114 @@
1
+ // Personal API types (for /api endpoints with Clerk auth)
2
+
3
+ export interface Board {
4
+ id: string;
5
+ _id: string;
6
+ name?: string;
7
+ createdAt: string;
8
+ updatedAt?: string;
9
+ lists?: BoardList[];
10
+ }
11
+
12
+ export interface BoardList {
13
+ id: string;
14
+ _id: string;
15
+ name: string;
16
+ order?: number;
17
+ }
18
+
19
+ // Company as returned inside job action data
20
+ export interface JobCompany {
21
+ _id: string;
22
+ id: string;
23
+ name: string;
24
+ color?: string;
25
+ }
26
+
27
+ // Job as returned by /api/board/{boardId}/jobs — keyed object map
28
+ export interface PersonalJob {
29
+ _id: string;
30
+ id: string;
31
+ title: string;
32
+ url?: string;
33
+ rootDomain?: string;
34
+ htmlDescription?: string;
35
+ _company: string; // company ID ref
36
+ _list?: string;
37
+ _board: string;
38
+ _activities?: string[];
39
+ _notes?: string[];
40
+ salary?: {
41
+ min?: number;
42
+ max?: number;
43
+ currency?: string;
44
+ };
45
+ location?: {
46
+ address?: string;
47
+ name?: string;
48
+ url?: string;
49
+ lat?: string;
50
+ lng?: string;
51
+ };
52
+ createdAt: string;
53
+ updatedAt?: string;
54
+ lastMovedAt?: string;
55
+ }
56
+
57
+ // Jobs response is an object map { [jobId]: PersonalJob }
58
+ export interface PersonalJobsResponse {
59
+ jobs: Record<string, PersonalJob>;
60
+ }
61
+
62
+ // Action as returned by /api/board/{boardId}/actions — keyed object map
63
+ export interface PersonalAction {
64
+ _id: string;
65
+ id: string;
66
+ actionType: string;
67
+ date: string;
68
+ createdAt: string;
69
+ updatedAt?: string;
70
+ data: {
71
+ _job?: string;
72
+ _company?: string;
73
+ _board?: string;
74
+ _fromList?: string;
75
+ _toList?: string;
76
+ note?: ActionNote | null;
77
+ job?: { _id: string; id: string; title: string };
78
+ company?: JobCompany;
79
+ fromList?: { _id: string; id: string; name: string } | null;
80
+ toList?: { _id: string; id: string; name: string } | null;
81
+ activity?: unknown;
82
+ activityCategory?: unknown;
83
+ contact?: unknown;
84
+ };
85
+ }
86
+
87
+ export interface ActionNote {
88
+ _id: string;
89
+ id: string;
90
+ text: string;
91
+ }
92
+
93
+ export interface PersonalActivity {
94
+ id: string;
95
+ _id: string;
96
+ title?: string;
97
+ note?: string;
98
+ completed: boolean;
99
+ completedAt?: string;
100
+ createdAt: string;
101
+ jobId?: string;
102
+ boardId?: string;
103
+ }
104
+
105
+ export interface UserProfile {
106
+ id: string;
107
+ _id?: string;
108
+ email: string;
109
+ givenName?: string;
110
+ familyName?: string;
111
+ firstName?: string;
112
+ lastName?: string;
113
+ createdAt: string;
114
+ }