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/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.0.0')
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 (default), json, csv, pdf, excel')
63
- .option('-j, --json', 'Output as JSON (legacy, same as --format json)')
64
- .option('--fields <fields>', 'Comma-separated list of fields to include')
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 AVAILABLE_FIELDS = ['ID', 'Name', 'Created'];
68
- const listOpts = parseListOptions(options);
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 (boardsList.length === 0) {
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
- return;
78
- }
79
-
80
- const rows = boardsList.map((b: any) => ({
81
- ID: b.id,
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.log(formatTableWithFields(rows, fields));
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 (default), json, csv, pdf, excel')
166
- .option('-j, --json', 'Output as JSON (legacy, same as --format json)')
167
- .option('--fields <fields>', 'Comma-separated list of fields to include (use "help" or "all")')
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 AVAILABLE_FIELDS = [...JOB_AVAILABLE_FIELDS];
171
- const listOpts = parseListOptions(options);
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 (jobsList.length === 0) {
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
- return;
192
- }
193
-
194
- const rows = jobsList.map(j => ({
195
- ID: j.id,
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.log(formatTableWithFields(rows, fields));
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 (default), json, csv, pdf, excel')
272
- .option('-d, --days <days>', 'Filter to last N days (e.g. 7 for past week)')
273
- .option('-w, --week', 'Filter to last 7 days (legacy, same as --days 7)')
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 AVAILABLE_FIELDS = [
280
- 'ID',
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
- const apiOpts: { since?: Date; types?: string[] } = {};
303
- if (listOpts.days) {
304
- apiOpts.since = new Date(Date.now() - listOpts.days * 24 * 60 * 60 * 1000);
305
- }
306
- if (listOpts.types) {
307
- apiOpts.types = listOpts.types;
308
- }
309
-
310
- const actions = await api.actions.listByBoardFlat(boardId, apiOpts);
311
-
312
- if (actions.length === 0) {
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
- return;
315
- }
316
-
317
- const rows = actions.map(a => ({
318
- ID: a.id,
319
- Date: new Date(a.date || a.createdAt).toISOString().substring(0, 16),
320
- Created: new Date(a.createdAt).toISOString().substring(0, 16),
321
- Updated: a.updatedAt ? new Date(a.updatedAt).toISOString().substring(0, 16) : '',
322
- Type: a.actionType,
323
- Company: a.data?.company?.name ?? '',
324
- CompanyId: a.data?._company ?? '',
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.log(formatTableWithFields(rows, fields));
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,Address'];
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();