huntr-cli 1.1.0 → 1.2.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/README.md +23 -0
- package/dist/cli.js +312 -357
- package/dist/cli.js.map +4 -4
- package/package.json +1 -1
- package/src/cli.ts +394 -217
package/src/cli.ts
CHANGED
|
@@ -1,23 +1,148 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { Command } from 'commander';
|
|
3
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
4
4
|
import { HuntrPersonalApi } from './api/personal';
|
|
5
5
|
import { TokenManager } from './config/token-manager';
|
|
6
6
|
import { ClerkSessionManager } from './config/clerk-session-manager';
|
|
7
7
|
import { captureSession, checkCdpSession } from './commands/capture-session';
|
|
8
|
-
import {
|
|
9
|
-
parseListOptions,
|
|
10
|
-
validateFields,
|
|
11
|
-
formatTableWithFields,
|
|
12
|
-
formatCsvWithFields,
|
|
13
|
-
formatJsonWithFields,
|
|
14
|
-
formatPdf,
|
|
15
|
-
formatExcel,
|
|
16
|
-
} from './lib/list-options';
|
|
17
8
|
|
|
18
9
|
const program = new Command();
|
|
19
10
|
const tokenManager = new TokenManager();
|
|
20
11
|
|
|
12
|
+
type OutputFormat = 'json' | 'table' | 'csv';
|
|
13
|
+
|
|
14
|
+
type SharedListOptions = {
|
|
15
|
+
format?: OutputFormat;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
days?: number;
|
|
18
|
+
since?: Date;
|
|
19
|
+
until?: Date;
|
|
20
|
+
limit?: number;
|
|
21
|
+
week?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function parsePositiveInt(value: string, flagName: string): number {
|
|
25
|
+
const parsed = Number.parseInt(value, 10);
|
|
26
|
+
if (!Number.isFinite(parsed) || Number.isNaN(parsed) || parsed <= 0) {
|
|
27
|
+
throw new InvalidArgumentError(`${flagName} must be a positive integer.`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseDaysOption(value: string): number {
|
|
33
|
+
return parsePositiveInt(value, '--days');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseLimitOption(value: string): number {
|
|
37
|
+
return parsePositiveInt(value, '--limit');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseFormatOption(value: string): OutputFormat {
|
|
41
|
+
const normalized = value.toLowerCase();
|
|
42
|
+
if (normalized === 'json' || normalized === 'table' || normalized === 'csv') {
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
throw new InvalidArgumentError('--format must be one of: json, table, csv.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseDateOption(value: string): Date {
|
|
49
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
50
|
+
throw new InvalidArgumentError('Date must be in YYYY-MM-DD format.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [yearStr, monthStr, dayStr] = value.split('-');
|
|
54
|
+
const year = Number(yearStr);
|
|
55
|
+
const month = Number(monthStr);
|
|
56
|
+
const day = Number(dayStr);
|
|
57
|
+
const date = new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0));
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
date.getUTCFullYear() !== year ||
|
|
61
|
+
date.getUTCMonth() !== month - 1 ||
|
|
62
|
+
date.getUTCDate() !== day
|
|
63
|
+
) {
|
|
64
|
+
throw new InvalidArgumentError(`Invalid calendar date: ${value}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return date;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveOutputFormat(options: SharedListOptions): OutputFormat {
|
|
71
|
+
if (options.json) {
|
|
72
|
+
if (options.format && options.format !== 'json') {
|
|
73
|
+
throw new Error(`--json cannot be combined with --format ${options.format}.`);
|
|
74
|
+
}
|
|
75
|
+
return 'json';
|
|
76
|
+
}
|
|
77
|
+
return options.format ?? 'json';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveDateRange(options: SharedListOptions): { since?: Date; until?: Date } {
|
|
81
|
+
let days = options.days;
|
|
82
|
+
if (options.week) {
|
|
83
|
+
if (options.days || options.since || options.until) {
|
|
84
|
+
throw new Error('--week cannot be combined with --days, --since, or --until.');
|
|
85
|
+
}
|
|
86
|
+
days = 7;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (days && (options.since || options.until)) {
|
|
90
|
+
throw new Error('--days cannot be combined with --since or --until.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const since = days ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) : options.since;
|
|
94
|
+
const until = options.until
|
|
95
|
+
? new Date(Date.UTC(
|
|
96
|
+
options.until.getUTCFullYear(),
|
|
97
|
+
options.until.getUTCMonth(),
|
|
98
|
+
options.until.getUTCDate(),
|
|
99
|
+
23, 59, 59, 999,
|
|
100
|
+
))
|
|
101
|
+
: undefined;
|
|
102
|
+
|
|
103
|
+
if (since && until && since.getTime() > until.getTime()) {
|
|
104
|
+
throw new Error('--since must be earlier than or equal to --until.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { since, until };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function filterByDateRange<T>(
|
|
111
|
+
items: T[],
|
|
112
|
+
getDate: (item: T) => string | undefined,
|
|
113
|
+
range: { since?: Date; until?: Date },
|
|
114
|
+
): T[] {
|
|
115
|
+
return items.filter(item => {
|
|
116
|
+
const raw = getDate(item);
|
|
117
|
+
if (!raw) return true;
|
|
118
|
+
|
|
119
|
+
const timestamp = new Date(raw).getTime();
|
|
120
|
+
if (Number.isNaN(timestamp)) return false;
|
|
121
|
+
if (range.since && timestamp < range.since.getTime()) return false;
|
|
122
|
+
if (range.until && timestamp > range.until.getTime()) return false;
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function applyLimit<T>(items: T[], limit?: number): T[] {
|
|
128
|
+
if (!limit) return items;
|
|
129
|
+
return items.slice(0, limit);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function csvCell(value: unknown): string {
|
|
133
|
+
const str = value == null ? '' : String(value);
|
|
134
|
+
if (/[",\n]/.test(str)) return `"${str.replace(/"/g, '""')}"`;
|
|
135
|
+
return str;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function printCsv(headers: string[], rows: unknown[][]): void {
|
|
139
|
+
const lines = [headers.join(',')];
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
lines.push(row.map(csvCell).join(','));
|
|
142
|
+
}
|
|
143
|
+
console.log(lines.join('\n'));
|
|
144
|
+
}
|
|
145
|
+
|
|
21
146
|
async function getApi(token?: string): Promise<HuntrPersonalApi> {
|
|
22
147
|
const provider = await tokenManager.getTokenProvider({ token });
|
|
23
148
|
return new HuntrPersonalApi(provider);
|
|
@@ -26,7 +151,7 @@ async function getApi(token?: string): Promise<HuntrPersonalApi> {
|
|
|
26
151
|
program
|
|
27
152
|
.name('huntr')
|
|
28
153
|
.description('CLI tool for Huntr')
|
|
29
|
-
.version('1.
|
|
154
|
+
.version('1.1.0')
|
|
30
155
|
.option('-t, --token <token>', 'API token (overrides all other sources)');
|
|
31
156
|
|
|
32
157
|
// ── me ───────────────────────────────────────────────────────────────────────
|
|
@@ -59,42 +184,38 @@ const boards = program.command('boards').description('Manage your boards');
|
|
|
59
184
|
boards
|
|
60
185
|
.command('list')
|
|
61
186
|
.description('List all your boards')
|
|
62
|
-
.option('-f, --format <format>', 'Output format: table
|
|
63
|
-
.option('-j, --json', 'Output as JSON (
|
|
64
|
-
.option('--
|
|
187
|
+
.option('-f, --format <format>', 'Output format: json | table | csv', parseFormatOption, 'json')
|
|
188
|
+
.option('-j, --json', 'Output as JSON (alias for --format json)')
|
|
189
|
+
.option('-d, --days <n>', 'Show only boards created in last N days', parseDaysOption)
|
|
190
|
+
.option('--since <date>', 'Show boards created since YYYY-MM-DD', parseDateOption)
|
|
191
|
+
.option('--until <date>', 'Show boards created until YYYY-MM-DD (inclusive)', parseDateOption)
|
|
192
|
+
.option('--limit <n>', 'Maximum rows to output', parseLimitOption)
|
|
65
193
|
.action(async (options, command) => {
|
|
66
194
|
try {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const fields = validateFields(AVAILABLE_FIELDS, listOpts.fields);
|
|
195
|
+
const format = resolveOutputFormat(options);
|
|
196
|
+
const range = resolveDateRange(options);
|
|
70
197
|
|
|
71
198
|
const api = await getApi(command.parent?.parent?.opts().token);
|
|
72
199
|
const response = await api.boards.list();
|
|
73
200
|
const boardsList = Array.isArray(response) ? response : (response as any).data ?? [];
|
|
201
|
+
const sorted = [...boardsList].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
202
|
+
const filtered = applyLimit(filterByDateRange(sorted, b => b.createdAt, range), options.limit);
|
|
74
203
|
|
|
75
|
-
if (
|
|
204
|
+
if (format === 'json') {
|
|
205
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
206
|
+
} else if (filtered.length === 0) {
|
|
76
207
|
console.log('No boards found.');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Name: b.name ?? 'N/A',
|
|
83
|
-
Created: new Date(b.createdAt).toLocaleDateString(),
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
if (listOpts.format === 'json') {
|
|
87
|
-
console.log(formatJsonWithFields(rows, fields));
|
|
88
|
-
} else if (listOpts.format === 'csv') {
|
|
89
|
-
console.log(formatCsvWithFields(rows, fields));
|
|
90
|
-
} else if (listOpts.format === 'pdf') {
|
|
91
|
-
const buffer = formatPdf(rows, fields, 'Boards List');
|
|
92
|
-
process.stdout.write(buffer);
|
|
93
|
-
} else if (listOpts.format === 'excel') {
|
|
94
|
-
const buffer = await formatExcel(rows, fields, 'Boards');
|
|
95
|
-
process.stdout.write(buffer);
|
|
208
|
+
} else if (format === 'csv') {
|
|
209
|
+
printCsv(
|
|
210
|
+
['id', 'name', 'created_at'],
|
|
211
|
+
filtered.map((b: any) => [b.id, b.name ?? '', b.createdAt ?? '']),
|
|
212
|
+
);
|
|
96
213
|
} else {
|
|
97
|
-
console.
|
|
214
|
+
console.table(filtered.map((b: any) => ({
|
|
215
|
+
ID: b.id,
|
|
216
|
+
Name: b.name ?? 'N/A',
|
|
217
|
+
Created: new Date(b.createdAt).toLocaleDateString(),
|
|
218
|
+
})));
|
|
98
219
|
}
|
|
99
220
|
} catch (error) {
|
|
100
221
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
@@ -132,99 +253,42 @@ boards
|
|
|
132
253
|
|
|
133
254
|
const jobs = program.command('jobs').description('Manage jobs on your boards');
|
|
134
255
|
|
|
135
|
-
const JOB_AVAILABLE_FIELDS: ReadonlyArray<string> = [
|
|
136
|
-
'ID', 'Title', 'URL', 'RootDomain', 'Description',
|
|
137
|
-
'CompanyId', 'ListId', 'BoardId',
|
|
138
|
-
'SalaryMin', 'SalaryMax', 'SalaryCurrency',
|
|
139
|
-
'LocationAddress', 'LocationName', 'LocationUrl', 'LocationLat', 'LocationLng',
|
|
140
|
-
'Created', 'Updated', 'LastMoved',
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
jobs
|
|
144
|
-
.command('fields')
|
|
145
|
-
.description('List available fields for jobs list')
|
|
146
|
-
.option('-j, --json', 'Output as JSON')
|
|
147
|
-
.action(async (options) => {
|
|
148
|
-
try {
|
|
149
|
-
const rows = JOB_AVAILABLE_FIELDS.map((f: string) => ({ Field: f }));
|
|
150
|
-
if (options.json) {
|
|
151
|
-
console.log(JSON.stringify(rows.map(r => r.Field), null, 2));
|
|
152
|
-
} else {
|
|
153
|
-
console.log(formatTableWithFields(rows, ['Field']));
|
|
154
|
-
}
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error('Error:', error instanceof Error ? error.message : error);
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
256
|
jobs
|
|
162
257
|
.command('list')
|
|
163
258
|
.description('List jobs on a board')
|
|
164
259
|
.argument('<board-id>', 'Board ID')
|
|
165
|
-
.option('-f, --format <format>', 'Output format: table
|
|
166
|
-
.option('-j, --json', 'Output as JSON (
|
|
167
|
-
.option('--
|
|
260
|
+
.option('-f, --format <format>', 'Output format: json | table | csv', parseFormatOption, 'json')
|
|
261
|
+
.option('-j, --json', 'Output as JSON (alias for --format json)')
|
|
262
|
+
.option('-d, --days <n>', 'Show only jobs created in last N days', parseDaysOption)
|
|
263
|
+
.option('--since <date>', 'Show jobs created since YYYY-MM-DD', parseDateOption)
|
|
264
|
+
.option('--until <date>', 'Show jobs created until YYYY-MM-DD (inclusive)', parseDateOption)
|
|
265
|
+
.option('--limit <n>', 'Maximum rows to output', parseLimitOption)
|
|
168
266
|
.action(async (boardId, options, command) => {
|
|
169
267
|
try {
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
// If user asked for field help, print and exit success
|
|
174
|
-
const wantsHelp = (listOpts.fields ?? []).some(f => /^(help|\?)$/i.test(f));
|
|
175
|
-
if (wantsHelp) {
|
|
176
|
-
const rows = AVAILABLE_FIELDS.map(f => ({ Field: f }));
|
|
177
|
-
console.log(formatTableWithFields(rows, ['Field']));
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const requested = listOpts.fields;
|
|
182
|
-
const fields = (requested && requested.length === 1 && /^(all)$/i.test(requested[0]))
|
|
183
|
-
? AVAILABLE_FIELDS.slice()
|
|
184
|
-
: validateFields(AVAILABLE_FIELDS, requested);
|
|
268
|
+
const format = resolveOutputFormat(options);
|
|
269
|
+
const range = resolveDateRange(options);
|
|
185
270
|
|
|
186
271
|
const api = await getApi(command.parent?.parent?.opts().token);
|
|
187
272
|
const jobsList = await api.jobs.listByBoardFlat(boardId);
|
|
273
|
+
const sorted = [...jobsList].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
274
|
+
const filtered = applyLimit(filterByDateRange(sorted, j => j.createdAt, range), options.limit);
|
|
188
275
|
|
|
189
|
-
if (
|
|
276
|
+
if (format === 'json') {
|
|
277
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
278
|
+
} else if (filtered.length === 0) {
|
|
190
279
|
console.log('No jobs found.');
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Title: j.title ?? '',
|
|
197
|
-
URL: j.url ?? '',
|
|
198
|
-
RootDomain: j.rootDomain ?? '',
|
|
199
|
-
Description: j.htmlDescription ?? '',
|
|
200
|
-
CompanyId: j._company ?? '',
|
|
201
|
-
ListId: j._list ?? '',
|
|
202
|
-
BoardId: j._board ?? '',
|
|
203
|
-
SalaryMin: j.salary?.min ?? '',
|
|
204
|
-
SalaryMax: j.salary?.max ?? '',
|
|
205
|
-
SalaryCurrency: j.salary?.currency ?? '',
|
|
206
|
-
LocationAddress: j.location?.address ?? '',
|
|
207
|
-
LocationName: j.location?.name ?? '',
|
|
208
|
-
LocationUrl: j.location?.url ?? '',
|
|
209
|
-
LocationLat: j.location?.lat ?? '',
|
|
210
|
-
LocationLng: j.location?.lng ?? '',
|
|
211
|
-
Created: new Date(j.createdAt).toLocaleDateString(),
|
|
212
|
-
Updated: j.updatedAt ? new Date(j.updatedAt).toLocaleDateString() : '',
|
|
213
|
-
LastMoved: j.lastMovedAt ? new Date(j.lastMovedAt).toLocaleDateString() : '',
|
|
214
|
-
}));
|
|
215
|
-
|
|
216
|
-
if (listOpts.format === 'json') {
|
|
217
|
-
console.log(formatJsonWithFields(rows, fields));
|
|
218
|
-
} else if (listOpts.format === 'csv') {
|
|
219
|
-
console.log(formatCsvWithFields(rows, fields));
|
|
220
|
-
} else if (listOpts.format === 'pdf') {
|
|
221
|
-
const buffer = formatPdf(rows, fields, 'Jobs List');
|
|
222
|
-
process.stdout.write(buffer);
|
|
223
|
-
} else if (listOpts.format === 'excel') {
|
|
224
|
-
const buffer = await formatExcel(rows, fields, 'Jobs');
|
|
225
|
-
process.stdout.write(buffer);
|
|
280
|
+
} else if (format === 'csv') {
|
|
281
|
+
printCsv(
|
|
282
|
+
['id', 'title', 'url', 'created_at'],
|
|
283
|
+
filtered.map(j => [j.id, j.title, j.url ?? '', j.createdAt]),
|
|
284
|
+
);
|
|
226
285
|
} else {
|
|
227
|
-
console.
|
|
286
|
+
console.table(filtered.map(j => ({
|
|
287
|
+
ID: j.id,
|
|
288
|
+
Title: j.title,
|
|
289
|
+
URL: j.url ?? 'N/A',
|
|
290
|
+
Created: new Date(j.createdAt).toLocaleDateString(),
|
|
291
|
+
})));
|
|
228
292
|
}
|
|
229
293
|
} catch (error) {
|
|
230
294
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
@@ -268,83 +332,53 @@ activities
|
|
|
268
332
|
.command('list')
|
|
269
333
|
.description('List actions for a board')
|
|
270
334
|
.argument('<board-id>', 'Board ID')
|
|
271
|
-
.option('-f, --format <format>', 'Output format: table
|
|
272
|
-
.option('-
|
|
273
|
-
.option('-
|
|
335
|
+
.option('-f, --format <format>', 'Output format: json | table | csv', parseFormatOption, 'json')
|
|
336
|
+
.option('-j, --json', 'Output as JSON (alias for --format json)')
|
|
337
|
+
.option('-d, --days <n>', 'Show only actions from last N days', parseDaysOption)
|
|
338
|
+
.option('--since <date>', 'Show actions since YYYY-MM-DD', parseDateOption)
|
|
339
|
+
.option('--until <date>', 'Show actions until YYYY-MM-DD (inclusive)', parseDateOption)
|
|
340
|
+
.option('--limit <n>', 'Maximum rows to output', parseLimitOption)
|
|
341
|
+
.option('-w, --week', 'Alias for --days 7')
|
|
274
342
|
.option('--types <types>', 'Comma-separated action types (e.g. JOB_MOVED,NOTE_CREATED)')
|
|
275
|
-
.option('--fields <fields>', 'Comma-separated list of fields to include')
|
|
276
|
-
.option('-j, --json', 'Output as JSON (legacy, same as --format json)')
|
|
277
343
|
.action(async (boardId, options, command) => {
|
|
278
344
|
try {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
'Date',
|
|
282
|
-
'Created',
|
|
283
|
-
'Updated',
|
|
284
|
-
'Type',
|
|
285
|
-
'Company',
|
|
286
|
-
'CompanyId',
|
|
287
|
-
'Job',
|
|
288
|
-
'JobId',
|
|
289
|
-
'FromStatus',
|
|
290
|
-
'FromStatusId',
|
|
291
|
-
'ToStatus',
|
|
292
|
-
'ToStatusId',
|
|
293
|
-
'Note',
|
|
294
|
-
'NoteId',
|
|
295
|
-
'BoardId',
|
|
296
|
-
];
|
|
297
|
-
const listOpts = parseListOptions(options);
|
|
298
|
-
const fields = validateFields(AVAILABLE_FIELDS, listOpts.fields);
|
|
345
|
+
const format = resolveOutputFormat(options);
|
|
346
|
+
const range = resolveDateRange(options);
|
|
299
347
|
|
|
300
348
|
const api = await getApi(command.parent?.parent?.opts().token);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
349
|
+
const opts: { since?: Date; types?: string[] } = {};
|
|
350
|
+
if (range.since) opts.since = range.since;
|
|
351
|
+
if (options.types) opts.types = options.types.split(',').map((t: string) => t.trim()).filter(Boolean);
|
|
352
|
+
|
|
353
|
+
const actionsRaw = await api.actions.listByBoardFlat(boardId, opts);
|
|
354
|
+
const actions = applyLimit(
|
|
355
|
+
filterByDateRange(actionsRaw, a => a.date || a.createdAt, range),
|
|
356
|
+
options.limit,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if (format === 'json') {
|
|
360
|
+
console.log(JSON.stringify(actions, null, 2));
|
|
361
|
+
} else if (actions.length === 0) {
|
|
313
362
|
console.log('No activities found.');
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
Job: (a.data?.job?.title ?? '').substring(0, 40),
|
|
326
|
-
JobId: a.data?._job ?? '',
|
|
327
|
-
FromStatus: a.data?.fromList?.name ?? '',
|
|
328
|
-
FromStatusId: a.data?._fromList ?? '',
|
|
329
|
-
ToStatus: a.data?.toList?.name ?? '',
|
|
330
|
-
ToStatusId: a.data?._toList ?? '',
|
|
331
|
-
Note: a.data?.note?.text ?? '',
|
|
332
|
-
NoteId: a.data?.note?.id ?? '',
|
|
333
|
-
BoardId: a.data?._board ?? '',
|
|
334
|
-
}));
|
|
335
|
-
|
|
336
|
-
if (listOpts.format === 'json') {
|
|
337
|
-
console.log(formatJsonWithFields(rows, fields));
|
|
338
|
-
} else if (listOpts.format === 'csv') {
|
|
339
|
-
console.log(formatCsvWithFields(rows, fields));
|
|
340
|
-
} else if (listOpts.format === 'pdf') {
|
|
341
|
-
const buffer = formatPdf(rows, fields, 'Activities List');
|
|
342
|
-
process.stdout.write(buffer);
|
|
343
|
-
} else if (listOpts.format === 'excel') {
|
|
344
|
-
const buffer = await formatExcel(rows, fields, 'Activities');
|
|
345
|
-
process.stdout.write(buffer);
|
|
363
|
+
} else if (format === 'csv') {
|
|
364
|
+
printCsv(
|
|
365
|
+
['date', 'type', 'company', 'job', 'status'],
|
|
366
|
+
actions.map(a => [
|
|
367
|
+
new Date(a.date || a.createdAt).toISOString(),
|
|
368
|
+
a.actionType,
|
|
369
|
+
a.data?.company?.name ?? '',
|
|
370
|
+
a.data?.job?.title ?? '',
|
|
371
|
+
a.data?.toList?.name ?? '',
|
|
372
|
+
]),
|
|
373
|
+
);
|
|
346
374
|
} else {
|
|
347
|
-
console.
|
|
375
|
+
console.table(actions.map(a => ({
|
|
376
|
+
Date: new Date(a.date || a.createdAt).toISOString().substring(0, 16),
|
|
377
|
+
Type: a.actionType,
|
|
378
|
+
Company: a.data?.company?.name ?? '',
|
|
379
|
+
Job: (a.data?.job?.title ?? '').substring(0, 40),
|
|
380
|
+
Status: a.data?.toList?.name ?? '',
|
|
381
|
+
})));
|
|
348
382
|
}
|
|
349
383
|
} catch (error) {
|
|
350
384
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
@@ -352,32 +386,6 @@ activities
|
|
|
352
386
|
}
|
|
353
387
|
});
|
|
354
388
|
|
|
355
|
-
activities
|
|
356
|
-
.command('fields')
|
|
357
|
-
.description('Show available fields for activities list command')
|
|
358
|
-
.action(() => {
|
|
359
|
-
const AVAILABLE_FIELDS = [
|
|
360
|
-
'ID',
|
|
361
|
-
'Date',
|
|
362
|
-
'Created',
|
|
363
|
-
'Updated',
|
|
364
|
-
'Type',
|
|
365
|
-
'Company',
|
|
366
|
-
'CompanyId',
|
|
367
|
-
'Job',
|
|
368
|
-
'JobId',
|
|
369
|
-
'FromStatus',
|
|
370
|
-
'FromStatusId',
|
|
371
|
-
'ToStatus',
|
|
372
|
-
'ToStatusId',
|
|
373
|
-
'Note',
|
|
374
|
-
'NoteId',
|
|
375
|
-
'BoardId',
|
|
376
|
-
];
|
|
377
|
-
const rows = AVAILABLE_FIELDS.map(f => ({ Field: f }));
|
|
378
|
-
console.log(formatTableWithFields(rows, ['Field']));
|
|
379
|
-
});
|
|
380
|
-
|
|
381
389
|
activities
|
|
382
390
|
.command('week-csv')
|
|
383
391
|
.description('Export last 7 days of activity as CSV')
|
|
@@ -386,7 +394,7 @@ activities
|
|
|
386
394
|
try {
|
|
387
395
|
const api = await getApi(command.parent?.parent?.opts().token);
|
|
388
396
|
const rows = await api.actions.weekSummary(boardId);
|
|
389
|
-
const lines = ['Date,Action,Company,Job Title,Status,Job URL
|
|
397
|
+
const lines = ['Date,Action,Company,Job Title,Status,Job URL'];
|
|
390
398
|
for (const r of rows) {
|
|
391
399
|
lines.push([
|
|
392
400
|
r.date,
|
|
@@ -395,7 +403,6 @@ activities
|
|
|
395
403
|
`"${r.jobTitle.replace(/"/g, '""')}"`,
|
|
396
404
|
r.status,
|
|
397
405
|
r.url,
|
|
398
|
-
`"${r.address.replace(/"/g, '""')}"`,
|
|
399
406
|
].join(','));
|
|
400
407
|
}
|
|
401
408
|
console.log(lines.join('\n'));
|
|
@@ -592,4 +599,174 @@ config
|
|
|
592
599
|
}
|
|
593
600
|
});
|
|
594
601
|
|
|
602
|
+
// ── completions ──────────────────────────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
program
|
|
605
|
+
.command('completions')
|
|
606
|
+
.description('Generate shell completion script')
|
|
607
|
+
.argument('<shell>', 'Shell to generate completions for: bash | zsh | fish')
|
|
608
|
+
.addHelpText('after', `
|
|
609
|
+
Examples:
|
|
610
|
+
# bash
|
|
611
|
+
huntr completions bash >> ~/.bash_completion
|
|
612
|
+
source ~/.bash_completion
|
|
613
|
+
|
|
614
|
+
# zsh (oh-my-zsh or fpath)
|
|
615
|
+
huntr completions zsh > "$HOME/.zsh/completions/_huntr"
|
|
616
|
+
# then add $HOME/.zsh/completions to your fpath in ~/.zshrc
|
|
617
|
+
|
|
618
|
+
# fish
|
|
619
|
+
huntr completions fish > ~/.config/fish/completions/huntr.fish
|
|
620
|
+
`)
|
|
621
|
+
.action((shell: string) => {
|
|
622
|
+
switch (shell) {
|
|
623
|
+
case 'bash':
|
|
624
|
+
/* eslint-disable no-useless-escape */
|
|
625
|
+
console.log(`# huntr bash completion
|
|
626
|
+
# Add to ~/.bash_completion or ~/.bashrc:
|
|
627
|
+
# source <(huntr completions bash)
|
|
628
|
+
|
|
629
|
+
_huntr_completions() {
|
|
630
|
+
local cur prev words cword
|
|
631
|
+
_init_completion || return
|
|
632
|
+
|
|
633
|
+
local top_commands="me boards jobs activities config completions"
|
|
634
|
+
local boards_commands="list get"
|
|
635
|
+
local jobs_commands="list get"
|
|
636
|
+
local activities_commands="list week-csv"
|
|
637
|
+
local config_commands="set-token capture-session check-cdp set-session test-session show-token clear-token clear-session"
|
|
638
|
+
|
|
639
|
+
case "\${words[1]}" in
|
|
640
|
+
boards)
|
|
641
|
+
COMPREPLY=( \$(compgen -W "\${boards_commands}" -- "\${cur}") )
|
|
642
|
+
return ;;
|
|
643
|
+
jobs)
|
|
644
|
+
COMPREPLY=( \$(compgen -W "\${jobs_commands}" -- "\${cur}") )
|
|
645
|
+
return ;;
|
|
646
|
+
activities)
|
|
647
|
+
COMPREPLY=( \$(compgen -W "\${activities_commands}" -- "\${cur}") )
|
|
648
|
+
return ;;
|
|
649
|
+
config)
|
|
650
|
+
COMPREPLY=( \$(compgen -W "\${config_commands}" -- "\${cur}") )
|
|
651
|
+
return ;;
|
|
652
|
+
completions)
|
|
653
|
+
COMPREPLY=( \$(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
654
|
+
return ;;
|
|
655
|
+
esac
|
|
656
|
+
|
|
657
|
+
COMPREPLY=( \$(compgen -W "\${top_commands}" -- "\${cur}") )
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
complete -F _huntr_completions huntr
|
|
661
|
+
`);
|
|
662
|
+
/* eslint-enable no-useless-escape */
|
|
663
|
+
break;
|
|
664
|
+
|
|
665
|
+
case 'zsh':
|
|
666
|
+
/* eslint-disable no-useless-escape */
|
|
667
|
+
console.log(`#compdef huntr
|
|
668
|
+
# huntr zsh completion
|
|
669
|
+
# Save to a directory in your fpath, e.g. ~/.zsh/completions/_huntr
|
|
670
|
+
|
|
671
|
+
_huntr() {
|
|
672
|
+
local -a top_commands boards_commands jobs_commands activities_commands config_commands
|
|
673
|
+
top_commands=(
|
|
674
|
+
'me:Show your user profile'
|
|
675
|
+
'boards:Manage your boards'
|
|
676
|
+
'jobs:Manage jobs on your boards'
|
|
677
|
+
'activities:View your board activity log'
|
|
678
|
+
'config:Manage CLI configuration'
|
|
679
|
+
'completions:Generate shell completion script'
|
|
680
|
+
)
|
|
681
|
+
boards_commands=('list:List all your boards' 'get:Get details of a specific board')
|
|
682
|
+
jobs_commands=('list:List jobs on a board' 'get:Get details of a specific job')
|
|
683
|
+
activities_commands=('list:List actions for a board' 'week-csv:Export last 7 days of activity as CSV')
|
|
684
|
+
config_commands=(
|
|
685
|
+
'set-token:Save API token'
|
|
686
|
+
'capture-session:Capture Clerk session from browser'
|
|
687
|
+
'check-cdp:Check Chrome DevTools connectivity'
|
|
688
|
+
'set-session:Save Clerk session cookie'
|
|
689
|
+
'test-session:Test stored Clerk session'
|
|
690
|
+
'show-token:Show configured auth sources'
|
|
691
|
+
'clear-token:Remove saved API token'
|
|
692
|
+
'clear-session:Remove saved Clerk session'
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
local state
|
|
696
|
+
_arguments -C \\
|
|
697
|
+
'(-t --token)'{-t,--token}'[API token]:token:' \\
|
|
698
|
+
'1: :->command' \\
|
|
699
|
+
'*: :->args' && return 0
|
|
700
|
+
|
|
701
|
+
case \$state in
|
|
702
|
+
command) _describe 'command' top_commands ;;
|
|
703
|
+
args)
|
|
704
|
+
case \$words[2] in
|
|
705
|
+
boards) _describe 'boards command' boards_commands ;;
|
|
706
|
+
jobs) _describe 'jobs command' jobs_commands ;;
|
|
707
|
+
activities) _describe 'activities command' activities_commands ;;
|
|
708
|
+
config) _describe 'config command' config_commands ;;
|
|
709
|
+
completions) _values 'shell' bash zsh fish ;;
|
|
710
|
+
esac ;;
|
|
711
|
+
esac
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
_huntr "\$@"
|
|
715
|
+
`);
|
|
716
|
+
/* eslint-enable no-useless-escape */
|
|
717
|
+
break;
|
|
718
|
+
|
|
719
|
+
case 'fish':
|
|
720
|
+
console.log(`# huntr fish completion
|
|
721
|
+
# Save to ~/.config/fish/completions/huntr.fish
|
|
722
|
+
|
|
723
|
+
set -l top_commands me boards jobs activities config completions
|
|
724
|
+
|
|
725
|
+
# Disable file completions globally
|
|
726
|
+
complete -c huntr -f
|
|
727
|
+
|
|
728
|
+
# Top-level commands
|
|
729
|
+
complete -c huntr -n "__fish_use_subcommand" -a me -d "Show your user profile"
|
|
730
|
+
complete -c huntr -n "__fish_use_subcommand" -a boards -d "Manage your boards"
|
|
731
|
+
complete -c huntr -n "__fish_use_subcommand" -a jobs -d "Manage jobs on your boards"
|
|
732
|
+
complete -c huntr -n "__fish_use_subcommand" -a activities -d "View your board activity log"
|
|
733
|
+
complete -c huntr -n "__fish_use_subcommand" -a config -d "Manage CLI configuration"
|
|
734
|
+
complete -c huntr -n "__fish_use_subcommand" -a completions -d "Generate shell completion script"
|
|
735
|
+
|
|
736
|
+
# Global flag
|
|
737
|
+
complete -c huntr -s t -l token -d "API token (overrides all other sources)" -r
|
|
738
|
+
|
|
739
|
+
# boards subcommands
|
|
740
|
+
complete -c huntr -n "__fish_seen_subcommand_from boards" -a list -d "List all your boards"
|
|
741
|
+
complete -c huntr -n "__fish_seen_subcommand_from boards" -a get -d "Get details of a specific board"
|
|
742
|
+
|
|
743
|
+
# jobs subcommands
|
|
744
|
+
complete -c huntr -n "__fish_seen_subcommand_from jobs" -a list -d "List jobs on a board"
|
|
745
|
+
complete -c huntr -n "__fish_seen_subcommand_from jobs" -a get -d "Get details of a specific job"
|
|
746
|
+
|
|
747
|
+
# activities subcommands
|
|
748
|
+
complete -c huntr -n "__fish_seen_subcommand_from activities" -a list -d "List actions for a board"
|
|
749
|
+
complete -c huntr -n "__fish_seen_subcommand_from activities" -a week-csv -d "Export last 7 days as CSV"
|
|
750
|
+
|
|
751
|
+
# config subcommands
|
|
752
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a set-token -d "Save API token"
|
|
753
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a capture-session -d "Capture Clerk session from browser"
|
|
754
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a check-cdp -d "Check Chrome DevTools connectivity"
|
|
755
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a set-session -d "Save Clerk session cookie"
|
|
756
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a test-session -d "Test stored Clerk session"
|
|
757
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a show-token -d "Show configured auth sources"
|
|
758
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a clear-token -d "Remove saved API token"
|
|
759
|
+
complete -c huntr -n "__fish_seen_subcommand_from config" -a clear-session -d "Remove saved Clerk session"
|
|
760
|
+
|
|
761
|
+
# completions shell argument
|
|
762
|
+
complete -c huntr -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"
|
|
763
|
+
`);
|
|
764
|
+
break;
|
|
765
|
+
|
|
766
|
+
default:
|
|
767
|
+
console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
595
772
|
program.parse();
|