nonotify 0.1.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/display.ts ADDED
@@ -0,0 +1,38 @@
1
+ import Table from 'cli-table3'
2
+
3
+ type ProfileRow = {
4
+ name: string
5
+ provider: string
6
+ isDefault: boolean
7
+ }
8
+
9
+ export function printProfilesTable(rows: ProfileRow[]): void {
10
+ if (rows.length === 0) {
11
+ process.stdout.write('No profiles configured. Run `nnt profile add`.\n')
12
+ return
13
+ }
14
+
15
+ const table = new Table({
16
+ head: ['Profile', 'Provider', 'Default'],
17
+ })
18
+
19
+ for (const row of rows) {
20
+ table.push([row.name, row.provider, row.isDefault ? 'yes' : ''])
21
+ }
22
+
23
+ process.stdout.write(`${table.toString()}\n`)
24
+ }
25
+
26
+ export function printKeyValueTable(title: string, rows: Array<{ key: string, value: string }>): void {
27
+ process.stdout.write(`${title}\n`)
28
+
29
+ const table = new Table({
30
+ head: ['Field', 'Value'],
31
+ })
32
+
33
+ for (const row of rows) {
34
+ table.push([row.key, row.value])
35
+ }
36
+
37
+ process.stdout.write(`${table.toString()}\n`)
38
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { cancel, confirm, isCancel, select, text } from '@clack/prompts'
2
+
3
+ export async function askRequired(question: string): Promise<string> {
4
+ return askRequiredWithInitial(question)
5
+ }
6
+
7
+ export async function askRequiredWithInitial(question: string, initialValue?: string): Promise<string> {
8
+ const message = normalizeQuestion(question)
9
+
10
+ const value = await text({
11
+ message,
12
+ initialValue,
13
+ validate(input) {
14
+ if (!input || input.trim() === '') {
15
+ return 'Value cannot be empty'
16
+ }
17
+
18
+ return undefined
19
+ },
20
+ })
21
+
22
+ if (isCancel(value)) {
23
+ cancel('Operation cancelled.')
24
+ process.exit(1)
25
+ }
26
+
27
+ return value.trim()
28
+ }
29
+
30
+ export async function askConfirm(question: string, initialValue = false): Promise<boolean> {
31
+ const value = await confirm({
32
+ message: normalizeQuestion(question),
33
+ initialValue,
34
+ })
35
+
36
+ if (isCancel(value)) {
37
+ cancel('Operation cancelled.')
38
+ process.exit(1)
39
+ }
40
+
41
+ return value
42
+ }
43
+
44
+ type SelectOption = {
45
+ value: string
46
+ label: string
47
+ hint?: string
48
+ disabled?: boolean
49
+ }
50
+
51
+ export async function askSelect(question: string, options: SelectOption[]): Promise<string> {
52
+ const value = await select<string>({
53
+ message: normalizeQuestion(question),
54
+ options,
55
+ })
56
+
57
+ if (isCancel(value)) {
58
+ cancel('Operation cancelled.')
59
+ process.exit(1)
60
+ }
61
+
62
+ return value
63
+ }
64
+
65
+ function normalizeQuestion(question: string): string {
66
+ return question.trim().replace(/:\s*$/, '')
67
+ }
@@ -0,0 +1,109 @@
1
+ type TelegramApiResponse<T> =
2
+ | {
3
+ ok: true
4
+ result: T
5
+ }
6
+ | {
7
+ ok: false
8
+ error_code: number
9
+ description: string
10
+ }
11
+
12
+ type TelegramUpdate = {
13
+ update_id: number
14
+ message?: {
15
+ from?: {
16
+ username?: string
17
+ }
18
+ chat?: {
19
+ id: number
20
+ username?: string
21
+ }
22
+ text?: string
23
+ }
24
+ }
25
+
26
+ export type TelegramConnection = {
27
+ chatId: string
28
+ username: string | null
29
+ }
30
+
31
+ async function telegramRequest<T>(
32
+ botToken: string,
33
+ method: string,
34
+ payload: Record<string, unknown>,
35
+ ): Promise<T> {
36
+ const response = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'content-type': 'application/json',
40
+ },
41
+ body: JSON.stringify(payload),
42
+ })
43
+
44
+ if (!response.ok) {
45
+ throw new Error(`Telegram API HTTP ${response.status}`)
46
+ }
47
+
48
+ const json = await response.json() as TelegramApiResponse<T>
49
+
50
+ if (!json.ok) {
51
+ throw new Error(json.description)
52
+ }
53
+
54
+ return json.result
55
+ }
56
+
57
+ export async function getLatestUpdateOffset(botToken: string): Promise<number> {
58
+ const updates = await telegramRequest<TelegramUpdate[]>(botToken, 'getUpdates', {
59
+ timeout: 0,
60
+ allowed_updates: ['message'],
61
+ })
62
+
63
+ if (updates.length === 0) {
64
+ return 0
65
+ }
66
+
67
+ const maxUpdateId = updates.reduce((acc, item) => Math.max(acc, item.update_id), 0)
68
+ return maxUpdateId + 1
69
+ }
70
+
71
+ export async function waitForChatId(
72
+ botToken: string,
73
+ offset: number,
74
+ timeoutSeconds = 120,
75
+ ): Promise<TelegramConnection> {
76
+ const startedAt = Date.now()
77
+ let currentOffset = offset
78
+
79
+ while ((Date.now() - startedAt) / 1000 < timeoutSeconds) {
80
+ const remainingSeconds = timeoutSeconds - Math.floor((Date.now() - startedAt) / 1000)
81
+ const pollTimeout = Math.max(1, Math.min(25, remainingSeconds))
82
+
83
+ const updates = await telegramRequest<TelegramUpdate[]>(botToken, 'getUpdates', {
84
+ offset: currentOffset,
85
+ timeout: pollTimeout,
86
+ allowed_updates: ['message'],
87
+ })
88
+
89
+ for (const update of updates) {
90
+ currentOffset = Math.max(currentOffset, update.update_id + 1)
91
+
92
+ if (update.message?.chat?.id !== undefined) {
93
+ return {
94
+ chatId: String(update.message.chat.id),
95
+ username: update.message.from?.username ?? update.message.chat.username ?? null,
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ throw new Error('Timed out waiting for Telegram message. Send a message to your bot and try again.')
102
+ }
103
+
104
+ export async function sendTelegramMessage(botToken: string, chatId: string, text: string): Promise<void> {
105
+ await telegramRequest(botToken, 'sendMessage', {
106
+ chat_id: chatId,
107
+ text,
108
+ })
109
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "resolveJsonModule": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "outDir": "dist",
12
+ "rootDir": "src"
13
+ },
14
+ "include": [
15
+ "src/**/*.ts"
16
+ ]
17
+ }