zele 0.3.17 → 0.3.21

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 (64) hide show
  1. package/README.md +81 -12
  2. package/dist/api-utils.d.ts +10 -0
  3. package/dist/api-utils.js +14 -0
  4. package/dist/api-utils.js.map +1 -1
  5. package/dist/cli-types.d.ts +4 -0
  6. package/dist/cli-types.js +6 -0
  7. package/dist/cli-types.js.map +1 -0
  8. package/dist/cli.js +1 -5
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/attachment.d.ts +2 -2
  11. package/dist/commands/attachment.js.map +1 -1
  12. package/dist/commands/auth-cmd.d.ts +2 -2
  13. package/dist/commands/auth-cmd.js +58 -52
  14. package/dist/commands/auth-cmd.js.map +1 -1
  15. package/dist/commands/calendar.d.ts +2 -2
  16. package/dist/commands/calendar.js +13 -14
  17. package/dist/commands/calendar.js.map +1 -1
  18. package/dist/commands/draft.d.ts +2 -2
  19. package/dist/commands/draft.js +62 -15
  20. package/dist/commands/draft.js.map +1 -1
  21. package/dist/commands/filter.d.ts +2 -2
  22. package/dist/commands/filter.js.map +1 -1
  23. package/dist/commands/label.d.ts +2 -2
  24. package/dist/commands/label.js +5 -6
  25. package/dist/commands/label.js.map +1 -1
  26. package/dist/commands/mail-actions.d.ts +2 -2
  27. package/dist/commands/mail-actions.js +290 -1
  28. package/dist/commands/mail-actions.js.map +1 -1
  29. package/dist/commands/mail.d.ts +2 -2
  30. package/dist/commands/mail.js +50 -10
  31. package/dist/commands/mail.js.map +1 -1
  32. package/dist/commands/profile.d.ts +2 -2
  33. package/dist/commands/profile.js.map +1 -1
  34. package/dist/commands/watch.d.ts +2 -2
  35. package/dist/commands/watch.js +2 -2
  36. package/dist/commands/watch.js.map +1 -1
  37. package/dist/gmail-client.d.ts +59 -3
  38. package/dist/gmail-client.js +119 -5
  39. package/dist/gmail-client.js.map +1 -1
  40. package/dist/imap-smtp-client.d.ts +75 -4
  41. package/dist/imap-smtp-client.js +131 -7
  42. package/dist/imap-smtp-client.js.map +1 -1
  43. package/dist/unsubscribe.d.ts +76 -0
  44. package/dist/unsubscribe.js +224 -0
  45. package/dist/unsubscribe.js.map +1 -0
  46. package/package.json +3 -2
  47. package/skills/zele/SKILL.md +32 -124
  48. package/src/api-utils.ts +14 -0
  49. package/src/cli-types.ts +8 -0
  50. package/src/cli.ts +2 -7
  51. package/src/commands/attachment.ts +2 -2
  52. package/src/commands/auth-cmd.ts +66 -56
  53. package/src/commands/calendar.ts +15 -16
  54. package/src/commands/draft.ts +71 -17
  55. package/src/commands/filter.ts +2 -2
  56. package/src/commands/label.ts +7 -8
  57. package/src/commands/mail-actions.ts +315 -4
  58. package/src/commands/mail.ts +54 -12
  59. package/src/commands/profile.ts +2 -2
  60. package/src/commands/watch.ts +4 -4
  61. package/src/gmail-client.ts +193 -6
  62. package/src/imap-smtp-client.ts +186 -7
  63. package/src/unsubscribe.test.ts +487 -0
  64. package/src/unsubscribe.ts +255 -0
@@ -2,71 +2,83 @@
2
2
  // Manages authentication for zele (Google OAuth and IMAP/SMTP credentials).
3
3
  // Supports multiple accounts: login adds accounts, logout removes one.
4
4
 
5
- import type { Goke } from 'goke'
5
+ import type { ZeleCli } from '../cli-types.js'
6
6
  import { z } from 'zod'
7
7
  import pc from 'picocolors'
8
+ import * as clack from '@clack/prompts'
8
9
  import { login, loginImap, logout, listAccounts, getAuthStatuses } from '../auth.js'
9
10
  import { closePrisma } from '../db.js'
10
11
  import * as out from '../output.js'
11
12
  import { handleCommandError } from '../output.js'
12
13
 
13
- export function registerAuthCommands(cli: Goke) {
14
+ export function registerAuthCommands(cli: ZeleCli) {
14
15
  cli
15
16
  .command('login', 'Authenticate with Google (opens browser) or show IMAP/SMTP login instructions')
16
- .action(async () => {
17
- // In a TTY, ask if they want Google or Other
18
- if (process.stdin.isTTY) {
19
- const readline = await import('node:readline')
20
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
17
+ .option(
18
+ '--method <method>',
19
+ z.enum(['google', 'imap']).optional().describe('Authentication method (google or imap)'),
20
+ )
21
+ .action(async (options) => {
22
+ let method = options.method
21
23
 
22
- console.error(pc.bold('\nChoose authentication method:\n'))
23
- console.error(' ' + pc.cyan('1') + ' Google (opens browser for OAuth)')
24
- console.error(' ' + pc.cyan('2') + ' Other (IMAP/SMTP with password)\n')
24
+ if (!method) {
25
+ if (!process.stdin.isTTY) {
26
+ out.error('Run non-interactively with: zele login --method google|imap')
27
+ process.exit(1)
28
+ }
25
29
 
26
- const answer = await new Promise<string>((resolve) => {
27
- rl.question('Enter choice [1]: ', resolve)
30
+ const choice = await clack.select({
31
+ message: 'Choose authentication method',
32
+ options: [
33
+ { value: 'google', label: 'Google', hint: 'opens browser for OAuth' },
34
+ { value: 'imap', label: 'Other', hint: 'IMAP/SMTP with password' },
35
+ ],
28
36
  })
29
- rl.close()
30
-
31
- const choice = answer.trim() || '1'
32
-
33
- if (choice === '2') {
34
- console.error(pc.bold('\nTo add an IMAP/SMTP account, run:\n'))
35
- console.error(pc.dim(' # Fastmail'))
36
- console.error(` zele login imap \\`)
37
- console.error(` --email you@fastmail.com \\`)
38
- console.error(` --imap-host imap.fastmail.com --imap-port 993 \\`)
39
- console.error(` --smtp-host smtp.fastmail.com --smtp-port 465 \\`)
40
- console.error(` --password "your-app-password"`)
41
- console.error()
42
- console.error(pc.dim(' # Gmail (app password)'))
43
- console.error(` zele login imap \\`)
44
- console.error(` --email you@gmail.com \\`)
45
- console.error(` --imap-host imap.gmail.com --imap-port 993 \\`)
46
- console.error(` --smtp-host smtp.gmail.com --smtp-port 465 \\`)
47
- console.error(` --password "your-app-password"`)
48
- console.error()
49
- console.error(pc.dim(' # Outlook/Hotmail'))
50
- console.error(` zele login imap \\`)
51
- console.error(` --email you@outlook.com \\`)
52
- console.error(` --imap-host outlook.office365.com --imap-port 993 \\`)
53
- console.error(` --smtp-host smtp-mail.outlook.com --smtp-port 587 \\`)
54
- console.error(` --password "your-password"`)
55
- console.error()
56
- console.error(pc.dim(' # Generic (any IMAP/SMTP provider)'))
57
- console.error(` zele login imap \\`)
58
- console.error(` --email you@example.com \\`)
59
- console.error(` --imap-host imap.example.com --imap-port 993 \\`)
60
- console.error(` --smtp-host smtp.example.com --smtp-port 465 \\`)
61
- console.error(` --password "your-password"`)
62
- console.error()
63
- console.error(pc.dim('Omit --smtp-host for read-only (IMAP only, no sending).'))
64
- console.error(pc.dim('Use --imap-user/--smtp-user if the login username differs from your email.'))
65
- return
37
+
38
+ if (clack.isCancel(choice)) {
39
+ out.hint('Cancelled')
40
+ process.exit(0)
66
41
  }
42
+
43
+ method = choice
44
+ }
45
+
46
+ if (method === 'imap') {
47
+ console.error(pc.bold('\nTo add an IMAP/SMTP account, run:\n'))
48
+ console.error(pc.dim(' # Fastmail'))
49
+ console.error(` zele login imap \\`)
50
+ console.error(` --email you@fastmail.com \\`)
51
+ console.error(` --imap-host imap.fastmail.com --imap-port 993 \\`)
52
+ console.error(` --smtp-host smtp.fastmail.com --smtp-port 465 \\`)
53
+ console.error(` --password "your-app-password"`)
54
+ console.error()
55
+ console.error(pc.dim(' # Gmail (app password)'))
56
+ console.error(` zele login imap \\`)
57
+ console.error(` --email you@gmail.com \\`)
58
+ console.error(` --imap-host imap.gmail.com --imap-port 993 \\`)
59
+ console.error(` --smtp-host smtp.gmail.com --smtp-port 465 \\`)
60
+ console.error(` --password "your-app-password"`)
61
+ console.error()
62
+ console.error(pc.dim(' # Outlook/Hotmail'))
63
+ console.error(` zele login imap \\`)
64
+ console.error(` --email you@outlook.com \\`)
65
+ console.error(` --imap-host outlook.office365.com --imap-port 993 \\`)
66
+ console.error(` --smtp-host smtp-mail.outlook.com --smtp-port 587 \\`)
67
+ console.error(` --password "your-password"`)
68
+ console.error()
69
+ console.error(pc.dim(' # Generic (any IMAP/SMTP provider)'))
70
+ console.error(` zele login imap \\`)
71
+ console.error(` --email you@example.com \\`)
72
+ console.error(` --imap-host imap.example.com --imap-port 993 \\`)
73
+ console.error(` --smtp-host smtp.example.com --smtp-port 465 \\`)
74
+ console.error(` --password "your-password"`)
75
+ console.error()
76
+ console.error(pc.dim('Omit --smtp-host for read-only (IMAP only, no sending).'))
77
+ console.error(pc.dim('Use --imap-user/--smtp-user if the login username differs from your email.'))
78
+ return
67
79
  }
68
80
 
69
- // Default: Google OAuth flow
81
+ // Google OAuth flow
70
82
  const result = await login()
71
83
  if (result instanceof Error) handleCommandError(result)
72
84
  const { email } = result
@@ -162,14 +174,12 @@ export function registerAuthCommands(cli: Goke) {
162
174
  process.exit(1)
163
175
  }
164
176
 
165
- const readline = await import('node:readline')
166
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
167
- const answer = await new Promise<string>((resolve) => {
168
- rl.question(`Remove credentials for ${targetEmail}? [y/N] `, resolve)
177
+ const confirmed = await clack.confirm({
178
+ message: `Remove credentials for ${targetEmail}?`,
179
+ initialValue: false,
169
180
  })
170
- rl.close()
171
181
 
172
- if (answer.toLowerCase() !== 'y') {
182
+ if (clack.isCancel(confirmed) || !confirmed) {
173
183
  out.hint('Cancelled')
174
184
  return
175
185
  }
@@ -3,9 +3,9 @@
3
3
  // Cache is handled by the client — commands just call methods and use data.
4
4
  // Multi-account: list/events fetch all accounts concurrently and merge by start time.
5
5
 
6
- import type { Goke } from 'goke'
6
+ import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
- import readline from 'node:readline'
8
+ import * as clack from '@clack/prompts'
9
9
  import { getCalendarClients, getCalendarClient } from '../auth.js'
10
10
  import type { CalendarClient, CalendarEvent, CalendarListItem, EventListResult } from '../calendar-client.js'
11
11
  import { AuthError } from '../api-utils.js'
@@ -17,7 +17,7 @@ import { resolveTimeRange, parseTimeExpression, parseDuration, isDateOnly } from
17
17
  // Register commands
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
- export function registerCalendarCommands(cli: Goke) {
20
+ export function registerCalendarCommands(cli: ZeleCli) {
21
21
  // =========================================================================
22
22
  // cal list
23
23
  // =========================================================================
@@ -76,11 +76,11 @@ export function registerCalendarCommands(cli: Goke) {
76
76
  .option('--week', 'Show this week')
77
77
  .option('--days <days>', z.number().describe('Show next N days'))
78
78
  .option('--all', 'Fetch from all calendars')
79
- .option('--query <query>', 'Free text search')
80
- .option('--max [max]', 'Max results (default: 20)')
79
+ .option('--filter <filter>', 'Free text search')
80
+ .option('--limit [limit]', 'Max results (default: 20)')
81
81
  .option('--page <page>', 'Pagination token (requires --account, only works for a single account)')
82
82
  .action(async (options) => {
83
- const max = options.max ? Number(options.max) : 20
83
+ const limit = options.limit ? Number(options.limit) : 20
84
84
  const calendarId = options.calendar ?? 'primary'
85
85
 
86
86
  if (options.all && options.calendar) {
@@ -122,8 +122,8 @@ export function registerCalendarCommands(cli: Goke) {
122
122
  calendarId: cal.id,
123
123
  timeMin,
124
124
  timeMax,
125
- query: options.query,
126
- maxResults: max,
125
+ query: options.filter,
126
+ maxResults: limit,
127
127
  })
128
128
  if (r instanceof Error) return r
129
129
  return r.events.map((e) => ({ ...e, calendarId: cal.id }))
@@ -145,8 +145,8 @@ export function registerCalendarCommands(cli: Goke) {
145
145
  calendarId,
146
146
  timeMin,
147
147
  timeMax,
148
- query: options.query,
149
- maxResults: max,
148
+ query: options.filter,
149
+ maxResults: limit,
150
150
  pageToken: options.page,
151
151
  })
152
152
  if (r instanceof Error) return r
@@ -171,7 +171,7 @@ export function registerCalendarCommands(cli: Goke) {
171
171
  result.events.map((e) => ({ ...e, account: email })),
172
172
  )
173
173
  .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
174
- .slice(0, max)
174
+ .slice(0, limit)
175
175
 
176
176
  if (merged.length === 0) {
177
177
  out.printList([], { summary: 'No events found' })
@@ -418,13 +418,12 @@ export function registerCalendarCommands(cli: Goke) {
418
418
  const { client } = await getCalendarClient(options.account)
419
419
 
420
420
  if (!options.force && process.stdin.isTTY) {
421
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
422
- const answer = await new Promise<string>((resolve) => {
423
- rl.question(`Delete event ${eventId}? [y/N] `, resolve)
421
+ const confirmed = await clack.confirm({
422
+ message: `Delete event ${eventId}?`,
423
+ initialValue: false,
424
424
  })
425
- rl.close()
426
425
 
427
- if (answer.toLowerCase() !== 'y') {
426
+ if (clack.isCancel(confirmed) || !confirmed) {
428
427
  out.hint('Cancelled')
429
428
  return
430
429
  }
@@ -3,9 +3,10 @@
3
3
  // Cache invalidation is handled by the client (sendDraft invalidates threadLists).
4
4
  // Multi-account: list fetches all accounts concurrently and merges by date.
5
5
 
6
- import type { Goke } from 'goke'
6
+ import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
8
  import fs from 'node:fs'
9
+ import * as clack from '@clack/prompts'
9
10
  import { getClients, getClient } from '../auth.js'
10
11
  import type { GmailClient } from '../gmail-client.js'
11
12
  import type { ImapSmtpClient } from '../imap-smtp-client.js'
@@ -14,16 +15,16 @@ import * as out from '../output.js'
14
15
  import { handleCommandError } from '../output.js'
15
16
  import pc from 'picocolors'
16
17
 
17
- export function registerDraftCommands(cli: Goke) {
18
+ export function registerDraftCommands(cli: ZeleCli) {
18
19
  // =========================================================================
19
20
  // draft list
20
21
  // =========================================================================
21
22
 
22
23
  cli
23
24
  .command('draft list', 'List drafts')
24
- .option('--max <max>', z.number().default(20).describe('Max results'))
25
+ .option('--limit <limit>', z.number().default(20).describe('Max results'))
25
26
  .option('--page <page>', z.string().describe('Pagination token (requires --account, only works for a single account)'))
26
- .option('--query <query>', z.string().describe('Search query'))
27
+ .option('--filter <filter>', z.string().describe('Search query'))
27
28
  .action(async (options) => {
28
29
  const clients = await getClients(options.account)
29
30
 
@@ -36,8 +37,8 @@ export function registerDraftCommands(cli: Goke) {
36
37
  const results = await Promise.all(
37
38
  clients.map(async ({ email, client }) => {
38
39
  const result = await client.listDrafts({
39
- query: options.query,
40
- maxResults: options.max,
40
+ query: options.filter,
41
+ maxResults: options.limit,
41
42
  pageToken: options.page,
42
43
  })
43
44
  if (result instanceof Error) return result
@@ -51,13 +52,13 @@ export function registerDraftCommands(cli: Goke) {
51
52
  return true
52
53
  })
53
54
 
54
- // Merge drafts from all accounts, sorted by date descending, capped at max
55
+ // Merge drafts from all accounts, sorted by date descending, capped at limit
55
56
  const merged = allResults
56
57
  .flatMap(({ email, result }) =>
57
58
  result.drafts.map((d) => ({ ...d, account: email })),
58
59
  )
59
60
  .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
60
- .slice(0, options.max)
61
+ .slice(0, options.limit)
61
62
 
62
63
  if (merged.length === 0) {
63
64
  out.printList([], { summary: 'No drafts found' })
@@ -89,14 +90,17 @@ export function registerDraftCommands(cli: Goke) {
89
90
  const draft = await client.getDraft({ draftId })
90
91
  if (draft instanceof Error) handleCommandError(draft)
91
92
 
93
+ const fmtRecipients = (list: Array<{ name?: string; email: string }>) =>
94
+ list.map((r) => r.name && r.name !== r.email ? `${r.name} <${r.email}>` : r.email).join(', ')
95
+
92
96
  console.log(pc.bold(`Draft: ${draft.message.subject}`))
93
97
  console.log(pc.dim(`Draft ID: ${draft.id}`))
94
- console.log(`To: ${draft.to.join(', ') || '(none)'}`)
98
+ console.log(`To: ${fmtRecipients(draft.to) || '(none)'}`)
95
99
  if (draft.cc.length > 0) {
96
- console.log(`Cc: ${draft.cc.join(', ')}`)
100
+ console.log(`Cc: ${fmtRecipients(draft.cc)}`)
97
101
  }
98
102
  if (draft.bcc.length > 0) {
99
- console.log(`Bcc: ${draft.bcc.join(', ')}`)
103
+ console.log(`Bcc: ${fmtRecipients(draft.bcc)}`)
100
104
  }
101
105
  console.log()
102
106
 
@@ -161,6 +165,58 @@ export function registerDraftCommands(cli: Goke) {
161
165
  out.success('Draft created')
162
166
  })
163
167
 
168
+ // =========================================================================
169
+ // draft update
170
+ // =========================================================================
171
+
172
+ cli
173
+ .command('draft update <draftId>', 'Update an existing draft')
174
+ .option('--to <to>', z.string().describe('New recipient email(s), comma-separated'))
175
+ .option('--subject <subject>', z.string().describe('New subject'))
176
+ .option('--body <body>', z.string().describe('New body text'))
177
+ .option('--body-file <bodyFile>', z.string().describe('Read new body from file (use - for stdin)'))
178
+ .option('--cc <cc>', z.string().describe('New CC recipients (comma-separated)'))
179
+ .option('--bcc <bcc>', z.string().describe('New BCC recipients (comma-separated)'))
180
+ .option('--from <from>', z.string().describe('Send-as alias email'))
181
+ .action(async (draftId, options) => {
182
+ const { client } = await getClient(options.account)
183
+
184
+ // Fetch existing draft to merge unchanged fields
185
+ const existing = await client.getDraft({ draftId })
186
+ if (existing instanceof Error) handleCommandError(existing)
187
+
188
+ let body = options.body
189
+ if (options.bodyFile) {
190
+ if (options.bodyFile === '-') {
191
+ const chunks: Buffer[] = []
192
+ for await (const chunk of process.stdin) {
193
+ chunks.push(chunk)
194
+ }
195
+ body = Buffer.concat(chunks).toString('utf-8')
196
+ } else {
197
+ body = fs.readFileSync(options.bodyFile, 'utf-8')
198
+ }
199
+ }
200
+
201
+ const parseEmails = (str: string) =>
202
+ str.split(',').map((e) => e.trim()).filter(Boolean).map((email) => ({ email }))
203
+
204
+ const result = await client.updateDraft({
205
+ draftId,
206
+ to: options.to ? parseEmails(options.to) : existing.to,
207
+ subject: options.subject ?? existing.message.subject,
208
+ body: body ?? existing.message.body,
209
+ cc: options.cc ? parseEmails(options.cc) : existing.cc,
210
+ bcc: options.bcc ? parseEmails(options.bcc) : existing.bcc,
211
+ threadId: existing.message.threadId || undefined,
212
+ fromEmail: options.from ?? existing.message.from.email,
213
+ })
214
+ if (result instanceof Error) handleCommandError(result)
215
+
216
+ out.printYaml(result)
217
+ out.success('Draft updated')
218
+ })
219
+
164
220
  // =========================================================================
165
221
  // draft send
166
222
  // =========================================================================
@@ -185,14 +241,12 @@ export function registerDraftCommands(cli: Goke) {
185
241
  .option('--force', 'Skip confirmation')
186
242
  .action(async (draftId, options) => {
187
243
  if (!options.force && process.stdin.isTTY) {
188
- const readline = await import('node:readline')
189
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
190
- const answer = await new Promise<string>((resolve) => {
191
- rl.question(`Delete draft ${draftId}? [y/N] `, resolve)
244
+ const confirmed = await clack.confirm({
245
+ message: `Delete draft ${draftId}?`,
246
+ initialValue: false,
192
247
  })
193
- rl.close()
194
248
 
195
- if (answer.toLowerCase() !== 'y') {
249
+ if (clack.isCancel(confirmed) || !confirmed) {
196
250
  out.hint('Cancelled')
197
251
  return
198
252
  }
@@ -1,14 +1,14 @@
1
1
  // Filter commands: list, create, delete Gmail filters.
2
2
  // Multi-account support via getClients/getClient like label.ts.
3
3
 
4
- import type { Goke } from 'goke'
4
+ import type { ZeleCli } from '../cli-types.js'
5
5
  import { getClients } from '../auth.js'
6
6
  import { AuthError, UnsupportedError, isScopeError } from '../api-utils.js'
7
7
  import type { GmailClient } from '../gmail-client.js'
8
8
  import * as out from '../output.js'
9
9
  import { handleCommandError } from '../output.js'
10
10
 
11
- export function registerFilterCommands(cli: Goke) {
11
+ export function registerFilterCommands(cli: ZeleCli) {
12
12
  // =========================================================================
13
13
  // filter list
14
14
  // =========================================================================
@@ -3,15 +3,16 @@
3
3
  // Cache is handled by the client — commands just call methods and use data.
4
4
  // Multi-account: list and counts fetch all accounts concurrently and merge.
5
5
 
6
- import type { Goke } from 'goke'
6
+ import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
+ import * as clack from '@clack/prompts'
8
9
  import { getClients, getGmailClient } from '../auth.js'
9
10
  import { AuthError, UnsupportedError } from '../api-utils.js'
10
11
  import type { GmailClient } from '../gmail-client.js'
11
12
  import * as out from '../output.js'
12
13
  import { handleCommandError } from '../output.js'
13
14
 
14
- export function registerLabelCommands(cli: Goke) {
15
+ export function registerLabelCommands(cli: ZeleCli) {
15
16
  // =========================================================================
16
17
  // label list
17
18
  // =========================================================================
@@ -122,14 +123,12 @@ export function registerLabelCommands(cli: Goke) {
122
123
  .option('--force', 'Skip confirmation')
123
124
  .action(async (labelId, options) => {
124
125
  if (!options.force && process.stdin.isTTY) {
125
- const readline = await import('node:readline')
126
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
127
- const answer = await new Promise<string>((resolve) => {
128
- rl.question(`Delete label ${labelId}? [y/N] `, resolve)
126
+ const confirmed = await clack.confirm({
127
+ message: `Delete label ${labelId}?`,
128
+ initialValue: false,
129
129
  })
130
- rl.close()
131
130
 
132
- if (answer.toLowerCase() !== 'y') {
131
+ if (clack.isCancel(confirmed) || !confirmed) {
133
132
  out.hint('Cancelled')
134
133
  return
135
134
  }