nonotify 0.1.14 → 0.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/CHANGELOG.md +12 -0
- package/dist/cli.js +170 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/notifier.d.ts +19 -0
- package/dist/notifier.js +94 -1
- package/dist/prompt.js +10 -15
- package/dist/telegram.d.ts +24 -1
- package/dist/telegram.js +293 -31
- package/package.json +6 -6
- package/src/cli.ts +224 -4
- package/src/index.ts +4 -0
- package/src/notifier.ts +155 -1
- package/src/prompt.ts +12 -18
- package/src/telegram.ts +451 -48
- package/tsconfig.json +1 -0
package/dist/telegram.js
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
const allowedUpdateTypes = ["message", "callback_query"];
|
|
2
|
+
const updateStreams = new Map();
|
|
3
|
+
const shortPollIntervalMs = 1_000;
|
|
4
|
+
async function telegramRequest(botToken, method, payload, signal) {
|
|
5
|
+
let response;
|
|
6
|
+
try {
|
|
7
|
+
response = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
signal,
|
|
10
|
+
headers: {
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify(payload),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new Error(formatTelegramFetchError(method, error));
|
|
18
|
+
}
|
|
9
19
|
if (!response.ok) {
|
|
10
|
-
throw new Error(`Telegram
|
|
20
|
+
throw new Error(`Telegram ${method} failed with HTTP ${response.status}.`);
|
|
11
21
|
}
|
|
12
22
|
const json = (await response.json());
|
|
13
23
|
if (!json.ok) {
|
|
24
|
+
if (method === "getUpdates" &&
|
|
25
|
+
/webhook/i.test(json.description) &&
|
|
26
|
+
/active|set/i.test(json.description)) {
|
|
27
|
+
throw new Error("This Telegram bot has an active webhook. `nnt ask` requires getUpdates, so disable the webhook or use a separate bot token for nonotify.");
|
|
28
|
+
}
|
|
14
29
|
throw new Error(json.description);
|
|
15
30
|
}
|
|
16
31
|
return json.result;
|
|
@@ -18,7 +33,7 @@ async function telegramRequest(botToken, method, payload) {
|
|
|
18
33
|
export async function getLatestUpdateOffset(botToken) {
|
|
19
34
|
const updates = await telegramRequest(botToken, "getUpdates", {
|
|
20
35
|
timeout: 0,
|
|
21
|
-
allowed_updates:
|
|
36
|
+
allowed_updates: allowedUpdateTypes,
|
|
22
37
|
});
|
|
23
38
|
if (updates.length === 0) {
|
|
24
39
|
return 0;
|
|
@@ -26,30 +41,30 @@ export async function getLatestUpdateOffset(botToken) {
|
|
|
26
41
|
const maxUpdateId = updates.reduce((acc, item) => Math.max(acc, item.update_id), 0);
|
|
27
42
|
return maxUpdateId + 1;
|
|
28
43
|
}
|
|
29
|
-
export async function waitForChatId(botToken, offset, timeoutSeconds = 120) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const updates = await telegramRequest(botToken, "getUpdates", {
|
|
36
|
-
offset: currentOffset,
|
|
37
|
-
timeout: pollTimeout,
|
|
38
|
-
allowed_updates: ["message"],
|
|
39
|
-
});
|
|
40
|
-
for (const update of updates) {
|
|
41
|
-
currentOffset = Math.max(currentOffset, update.update_id + 1);
|
|
42
|
-
if (update.message?.chat?.id !== undefined) {
|
|
43
|
-
return {
|
|
44
|
-
chatId: String(update.message.chat.id),
|
|
45
|
-
username: update.message.from?.username ??
|
|
46
|
-
update.message.chat.username ??
|
|
47
|
-
null,
|
|
48
|
-
};
|
|
44
|
+
export async function waitForChatId(botToken, offset, timeoutSeconds = 120, signal) {
|
|
45
|
+
const stream = getTelegramUpdateStream(botToken, offset);
|
|
46
|
+
try {
|
|
47
|
+
return await stream.waitFor((update) => {
|
|
48
|
+
if (update.message?.chat?.id === undefined) {
|
|
49
|
+
return undefined;
|
|
49
50
|
}
|
|
51
|
+
return {
|
|
52
|
+
chatId: String(update.message.chat.id),
|
|
53
|
+
username: update.message.from?.username ??
|
|
54
|
+
update.message.chat.username ??
|
|
55
|
+
null,
|
|
56
|
+
};
|
|
57
|
+
}, {
|
|
58
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
59
|
+
signal,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof Error && error.message === "Timed out waiting") {
|
|
64
|
+
throw new Error("Timed out waiting for Telegram message. Send a message to your bot and try again.");
|
|
50
65
|
}
|
|
66
|
+
throw error;
|
|
51
67
|
}
|
|
52
|
-
throw new Error("Timed out waiting for Telegram message. Send a message to your bot and try again.");
|
|
53
68
|
}
|
|
54
69
|
export async function sendTelegramMessage(botToken, chatId, text) {
|
|
55
70
|
await telegramRequest(botToken, "sendMessage", {
|
|
@@ -57,3 +72,250 @@ export async function sendTelegramMessage(botToken, chatId, text) {
|
|
|
57
72
|
text,
|
|
58
73
|
});
|
|
59
74
|
}
|
|
75
|
+
export async function sendTelegramChoiceMessage(botToken, chatId, text, options) {
|
|
76
|
+
const message = await telegramRequest(botToken, "sendMessage", {
|
|
77
|
+
chat_id: chatId,
|
|
78
|
+
text,
|
|
79
|
+
reply_markup: {
|
|
80
|
+
inline_keyboard: options.map((option) => [
|
|
81
|
+
{
|
|
82
|
+
text: option.label,
|
|
83
|
+
callback_data: option.callbackData,
|
|
84
|
+
},
|
|
85
|
+
]),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
messageId: message.message_id,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export async function waitForTelegramCallback(botToken, input) {
|
|
93
|
+
const stream = getTelegramUpdateStream(botToken, input.offset);
|
|
94
|
+
const allowedCallbackData = new Set(input.callbackData);
|
|
95
|
+
return stream.waitFor((update) => {
|
|
96
|
+
const callback = update.callback_query;
|
|
97
|
+
if (!callback?.message ||
|
|
98
|
+
String(callback.message.chat?.id) !== input.chatId ||
|
|
99
|
+
callback.message.message_id !== input.messageId ||
|
|
100
|
+
typeof callback.data !== "string" ||
|
|
101
|
+
!allowedCallbackData.has(callback.data)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
callbackQueryId: callback.id,
|
|
106
|
+
data: callback.data,
|
|
107
|
+
};
|
|
108
|
+
}, {
|
|
109
|
+
timeoutMs: input.timeoutMs,
|
|
110
|
+
signal: input.signal,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export async function answerTelegramCallbackQuery(botToken, callbackQueryId) {
|
|
114
|
+
await telegramRequest(botToken, "answerCallbackQuery", {
|
|
115
|
+
callback_query_id: callbackQueryId,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export async function clearTelegramInlineKeyboard(botToken, chatId, messageId) {
|
|
119
|
+
try {
|
|
120
|
+
await telegramRequest(botToken, "editMessageReplyMarkup", {
|
|
121
|
+
chat_id: chatId,
|
|
122
|
+
message_id: messageId,
|
|
123
|
+
reply_markup: {
|
|
124
|
+
inline_keyboard: [],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof Error &&
|
|
130
|
+
/message is not modified/i.test(error.message)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function markTelegramSelectedOption(botToken, chatId, messageId, options, selectedCallbackData) {
|
|
137
|
+
try {
|
|
138
|
+
await telegramRequest(botToken, "editMessageReplyMarkup", {
|
|
139
|
+
chat_id: chatId,
|
|
140
|
+
message_id: messageId,
|
|
141
|
+
reply_markup: {
|
|
142
|
+
inline_keyboard: options.map((option) => [
|
|
143
|
+
{
|
|
144
|
+
text: option.callbackData === selectedCallbackData
|
|
145
|
+
? `✅ ${option.label}`
|
|
146
|
+
: option.label,
|
|
147
|
+
callback_data: option.callbackData,
|
|
148
|
+
},
|
|
149
|
+
]),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof Error &&
|
|
155
|
+
/message is not modified/i.test(error.message)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function getTelegramUpdateStream(botToken, offset = 0) {
|
|
162
|
+
const existing = updateStreams.get(botToken);
|
|
163
|
+
if (existing) {
|
|
164
|
+
existing.setMinimumOffset(offset);
|
|
165
|
+
return existing;
|
|
166
|
+
}
|
|
167
|
+
const stream = new TelegramUpdateStream(botToken, offset);
|
|
168
|
+
updateStreams.set(botToken, stream);
|
|
169
|
+
return stream;
|
|
170
|
+
}
|
|
171
|
+
class TelegramUpdateStream {
|
|
172
|
+
botToken;
|
|
173
|
+
currentOffset;
|
|
174
|
+
waiters = new Set();
|
|
175
|
+
pollPromise = null;
|
|
176
|
+
pollAbortController = null;
|
|
177
|
+
constructor(botToken, initialOffset) {
|
|
178
|
+
this.botToken = botToken;
|
|
179
|
+
this.currentOffset = initialOffset;
|
|
180
|
+
}
|
|
181
|
+
setMinimumOffset(offset) {
|
|
182
|
+
this.currentOffset = Math.max(this.currentOffset, offset);
|
|
183
|
+
}
|
|
184
|
+
async waitFor(match, options = {}) {
|
|
185
|
+
if (options.signal?.aborted) {
|
|
186
|
+
throw options.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
187
|
+
}
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const abortListener = () => {
|
|
190
|
+
unregister();
|
|
191
|
+
reject(options.signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
192
|
+
};
|
|
193
|
+
const timeout = typeof options.timeoutMs === "number"
|
|
194
|
+
? setTimeout(() => {
|
|
195
|
+
unregister();
|
|
196
|
+
reject(new Error("Timed out waiting"));
|
|
197
|
+
}, options.timeoutMs)
|
|
198
|
+
: null;
|
|
199
|
+
const waiter = {
|
|
200
|
+
match,
|
|
201
|
+
resolve: (value) => {
|
|
202
|
+
unregister();
|
|
203
|
+
resolve(value);
|
|
204
|
+
},
|
|
205
|
+
reject: (error) => {
|
|
206
|
+
unregister();
|
|
207
|
+
reject(error);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
const unregister = () => {
|
|
211
|
+
if (timeout) {
|
|
212
|
+
clearTimeout(timeout);
|
|
213
|
+
}
|
|
214
|
+
options.signal?.removeEventListener("abort", abortListener);
|
|
215
|
+
this.waiters.delete(waiter);
|
|
216
|
+
if (this.waiters.size === 0) {
|
|
217
|
+
this.pollAbortController?.abort();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
this.waiters.add(waiter);
|
|
221
|
+
options.signal?.addEventListener("abort", abortListener, { once: true });
|
|
222
|
+
this.ensurePolling();
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
ensurePolling() {
|
|
226
|
+
if (this.pollPromise) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.pollPromise = this.pollLoop().finally(() => {
|
|
230
|
+
this.pollPromise = null;
|
|
231
|
+
if (this.waiters.size === 0) {
|
|
232
|
+
updateStreams.delete(this.botToken);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async pollLoop() {
|
|
237
|
+
while (this.waiters.size > 0) {
|
|
238
|
+
this.pollAbortController = new AbortController();
|
|
239
|
+
try {
|
|
240
|
+
const updates = await telegramRequest(this.botToken, "getUpdates", {
|
|
241
|
+
offset: this.currentOffset,
|
|
242
|
+
timeout: 0,
|
|
243
|
+
allowed_updates: allowedUpdateTypes,
|
|
244
|
+
}, this.pollAbortController.signal);
|
|
245
|
+
for (const update of updates) {
|
|
246
|
+
this.currentOffset = Math.max(this.currentOffset, update.update_id + 1);
|
|
247
|
+
this.dispatch(update);
|
|
248
|
+
}
|
|
249
|
+
if (updates.length === 0 && this.waiters.size > 0) {
|
|
250
|
+
await delay(shortPollIntervalMs, this.pollAbortController.signal);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (isAbortError(error) && this.waiters.size === 0) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.rejectAll(error);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
this.pollAbortController = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
dispatch(update) {
|
|
266
|
+
for (const waiter of Array.from(this.waiters)) {
|
|
267
|
+
try {
|
|
268
|
+
const result = waiter.match(update);
|
|
269
|
+
if (result !== undefined) {
|
|
270
|
+
waiter.resolve(result);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
waiter.reject(error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
rejectAll(error) {
|
|
279
|
+
for (const waiter of Array.from(this.waiters)) {
|
|
280
|
+
waiter.reject(error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function isAbortError(error) {
|
|
285
|
+
return error instanceof DOMException
|
|
286
|
+
? error.name === "AbortError"
|
|
287
|
+
: error instanceof Error && error.name === "AbortError";
|
|
288
|
+
}
|
|
289
|
+
async function delay(ms, signal) {
|
|
290
|
+
if (signal?.aborted) {
|
|
291
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
292
|
+
}
|
|
293
|
+
await new Promise((resolve, reject) => {
|
|
294
|
+
const onAbort = () => {
|
|
295
|
+
clearTimeout(timeout);
|
|
296
|
+
signal?.removeEventListener("abort", onAbort);
|
|
297
|
+
const abortReason = signal?.reason ?? new DOMException("Aborted", "AbortError");
|
|
298
|
+
reject(abortReason);
|
|
299
|
+
};
|
|
300
|
+
const timeout = setTimeout(() => {
|
|
301
|
+
signal?.removeEventListener("abort", onAbort);
|
|
302
|
+
resolve();
|
|
303
|
+
}, ms);
|
|
304
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function formatTelegramFetchError(method, error) {
|
|
308
|
+
if (!(error instanceof Error)) {
|
|
309
|
+
return `Telegram ${method} request failed: ${String(error)}`;
|
|
310
|
+
}
|
|
311
|
+
const causeMessage = typeof error.cause === "object" &&
|
|
312
|
+
error.cause !== null &&
|
|
313
|
+
"message" in error.cause &&
|
|
314
|
+
typeof error.cause.message === "string"
|
|
315
|
+
? error.cause.message
|
|
316
|
+
: null;
|
|
317
|
+
if (causeMessage) {
|
|
318
|
+
return `Telegram ${method} request failed: ${error.message} (${causeMessage})`;
|
|
319
|
+
}
|
|
320
|
+
return `Telegram ${method} request failed: ${error.message}`;
|
|
321
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nonotify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "nnt CLI for Telegram notifications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
],
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@clack/prompts": "^1.0
|
|
38
|
+
"@clack/prompts": "^1.1.0",
|
|
39
39
|
"cli-table3": "^0.6.5",
|
|
40
|
-
"incur": "^0.
|
|
40
|
+
"incur": "^0.3.13"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"tsx": "^4.
|
|
45
|
-
"typescript": "^
|
|
43
|
+
"@types/node": "^25.5.0",
|
|
44
|
+
"tsx": "^4.21.0",
|
|
45
|
+
"typescript": "^6.0.2"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/cli.ts
CHANGED
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
sendTelegramMessage,
|
|
15
15
|
waitForChatId,
|
|
16
16
|
} from "./telegram.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
AskAbortedError,
|
|
19
|
+
AskTimeoutError,
|
|
20
|
+
Notifier,
|
|
21
|
+
NotifierError,
|
|
22
|
+
} from "./notifier.js";
|
|
18
23
|
|
|
19
24
|
const profileCli = Cli.create("profile", {
|
|
20
25
|
description: "Manage notification profiles",
|
|
@@ -453,12 +458,20 @@ const cli = Cli.create("nnt", {
|
|
|
453
458
|
})
|
|
454
459
|
.command(profileCli);
|
|
455
460
|
|
|
461
|
+
type ParsedAskArgs = {
|
|
462
|
+
message: string;
|
|
463
|
+
options: string[];
|
|
464
|
+
profile?: string;
|
|
465
|
+
timeoutMs?: number;
|
|
466
|
+
helpRequested: boolean;
|
|
467
|
+
};
|
|
468
|
+
|
|
456
469
|
function routeDefaultCommand(argv: string[]): string[] {
|
|
457
470
|
if (argv.length === 0) {
|
|
458
471
|
return argv;
|
|
459
472
|
}
|
|
460
473
|
|
|
461
|
-
const topLevelCommands = new Set(["profile", "send", "skills", "mcp"]);
|
|
474
|
+
const topLevelCommands = new Set(["ask", "profile", "send", "skills", "mcp"]);
|
|
462
475
|
const bareGlobalFlags = new Set([
|
|
463
476
|
"--help",
|
|
464
477
|
"-h",
|
|
@@ -512,6 +525,90 @@ function normalizeFormatFlag(argv: string[]): string[] {
|
|
|
512
525
|
return normalized;
|
|
513
526
|
}
|
|
514
527
|
|
|
528
|
+
function getCommandIndex(argv: string[]): number {
|
|
529
|
+
const bareGlobalFlags = new Set([
|
|
530
|
+
"--help",
|
|
531
|
+
"-h",
|
|
532
|
+
"--version",
|
|
533
|
+
"--llms",
|
|
534
|
+
"--mcp",
|
|
535
|
+
"--json",
|
|
536
|
+
"--verbose",
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
let index = 0;
|
|
540
|
+
|
|
541
|
+
while (index < argv.length) {
|
|
542
|
+
const token = argv[index];
|
|
543
|
+
|
|
544
|
+
if (token === "--format") {
|
|
545
|
+
index += 2;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (bareGlobalFlags.has(token)) {
|
|
550
|
+
index += 1;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return index;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function serveAskCommandIfRequested(argv: string[]): Promise<boolean> {
|
|
561
|
+
const commandIndex = getCommandIndex(argv);
|
|
562
|
+
|
|
563
|
+
if (commandIndex >= argv.length || argv[commandIndex] !== "ask") {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const parsed = parseAskArgs(argv.slice(commandIndex + 1));
|
|
569
|
+
|
|
570
|
+
if (parsed.helpRequested) {
|
|
571
|
+
process.stderr.write(getAskUsage());
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const notifier = new Notifier();
|
|
576
|
+
const abortController = new AbortController();
|
|
577
|
+
let exitCode = 0;
|
|
578
|
+
const onAbortSignal = () => abortController.abort();
|
|
579
|
+
|
|
580
|
+
process.once("SIGINT", onAbortSignal);
|
|
581
|
+
process.once("SIGTERM", onAbortSignal);
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
const result = await notifier.ask({
|
|
585
|
+
message: parsed.message,
|
|
586
|
+
options: parsed.options,
|
|
587
|
+
profile: parsed.profile,
|
|
588
|
+
timeoutMs: parsed.timeoutMs,
|
|
589
|
+
signal: abortController.signal,
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
process.stdout.write(`${result.selected}\n`);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
exitCode = renderAskError(error);
|
|
595
|
+
} finally {
|
|
596
|
+
process.removeListener("SIGINT", onAbortSignal);
|
|
597
|
+
process.removeListener("SIGTERM", onAbortSignal);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (exitCode !== 0) {
|
|
601
|
+
process.exitCode = exitCode;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return true;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
process.stderr.write(`${formatAskError(error)}\n`);
|
|
607
|
+
process.exitCode = 1;
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
515
612
|
function isStrictOutputRequested(argv: string[]): boolean {
|
|
516
613
|
for (const token of argv) {
|
|
517
614
|
if (token === "--json" || token === "--verbose" || token === "--format") {
|
|
@@ -580,7 +677,130 @@ function shouldRenderPretty(agent: boolean): boolean {
|
|
|
580
677
|
return !agent && !strictOutputRequested && !isAgentEnvironment();
|
|
581
678
|
}
|
|
582
679
|
|
|
583
|
-
|
|
584
|
-
|
|
680
|
+
if (!(await serveAskCommandIfRequested(normalizedArgv))) {
|
|
681
|
+
const routedArgv = routeDefaultCommand(argvWithAgentDefaults);
|
|
682
|
+
await cli.serve(routedArgv);
|
|
683
|
+
}
|
|
585
684
|
|
|
586
685
|
export default cli;
|
|
686
|
+
|
|
687
|
+
function parseAskArgs(argv: string[]): ParsedAskArgs {
|
|
688
|
+
let profile: string | undefined;
|
|
689
|
+
let timeoutMs: number | undefined;
|
|
690
|
+
let helpRequested = false;
|
|
691
|
+
const positionals: string[] = [];
|
|
692
|
+
|
|
693
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
694
|
+
const token = argv[index];
|
|
695
|
+
|
|
696
|
+
if (token === "--help" || token === "-h") {
|
|
697
|
+
helpRequested = true;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (token === "--profile" || token === "-p") {
|
|
702
|
+
const value = argv[index + 1];
|
|
703
|
+
|
|
704
|
+
if (!value) {
|
|
705
|
+
throw new Error("Missing value for --profile.");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
profile = value;
|
|
709
|
+
index += 1;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (token.startsWith("--profile=")) {
|
|
714
|
+
profile = token.slice("--profile=".length);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (token === "--timeout" || token === "-t") {
|
|
719
|
+
const value = argv[index + 1];
|
|
720
|
+
|
|
721
|
+
if (!value) {
|
|
722
|
+
throw new Error("Missing value for --timeout.");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
timeoutMs = parseTimeoutFlag(value);
|
|
726
|
+
index += 1;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (token.startsWith("--timeout=")) {
|
|
731
|
+
timeoutMs = parseTimeoutFlag(token.slice("--timeout=".length));
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (token.startsWith("-")) {
|
|
736
|
+
throw new Error(`Unknown option: ${token}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
positionals.push(token);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (helpRequested) {
|
|
743
|
+
return {
|
|
744
|
+
message: "",
|
|
745
|
+
options: [],
|
|
746
|
+
profile,
|
|
747
|
+
timeoutMs,
|
|
748
|
+
helpRequested,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (positionals.length < 2) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
"Ask requires a message and at least one option. See `nnt ask --help`."
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
message: positionals[0],
|
|
760
|
+
options: positionals.slice(1),
|
|
761
|
+
profile,
|
|
762
|
+
timeoutMs,
|
|
763
|
+
helpRequested,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function parseTimeoutFlag(value: string): number {
|
|
768
|
+
const seconds = Number(value);
|
|
769
|
+
|
|
770
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
771
|
+
throw new Error("--timeout must be a non-negative number of seconds.");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return Math.round(seconds * 1000);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function getAskUsage(): string {
|
|
778
|
+
return [
|
|
779
|
+
"Usage: nnt ask [--profile <name>] [--timeout <seconds>] <message> <option1> [option2 ... option10]",
|
|
780
|
+
"",
|
|
781
|
+
"Prints only the selected option to stdout.",
|
|
782
|
+
].join("\n");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function renderAskError(error: unknown): number {
|
|
786
|
+
if (error instanceof AskTimeoutError) {
|
|
787
|
+
process.stderr.write(`${error.message}\n`);
|
|
788
|
+
return 1;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (error instanceof AskAbortedError) {
|
|
792
|
+
process.stderr.write("Cancelled.\n");
|
|
793
|
+
return 130;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
process.stderr.write(`${formatAskError(error)}\n`);
|
|
797
|
+
return 1;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function formatAskError(error: unknown): string {
|
|
801
|
+
if (error instanceof NotifierError || error instanceof Error) {
|
|
802
|
+
return error.message;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return String(error);
|
|
806
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export {
|
|
2
|
+
AskAbortedError,
|
|
3
|
+
AskTimeoutError,
|
|
2
4
|
EnvConfigLoader,
|
|
3
5
|
NoProfilesConfiguredError,
|
|
4
6
|
Notifier,
|
|
@@ -6,6 +8,8 @@ export {
|
|
|
6
8
|
ProfileNotFoundError,
|
|
7
9
|
} from "./notifier.js";
|
|
8
10
|
export type {
|
|
11
|
+
AskInput,
|
|
12
|
+
AskResult,
|
|
9
13
|
NotifierConfig,
|
|
10
14
|
NotifierConfigLoader,
|
|
11
15
|
NotifierProfile,
|