lastgen-cli 1.0.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 +269 -0
- package/dist/agent-init-CBHFO47V.js +15 -0
- package/dist/chunk-ESFGGWR6.js +799 -0
- package/dist/chunk-JGVBNMNK.js +257 -0
- package/dist/chunk-JUKR4XSW.js +283 -0
- package/dist/chunk-NILKRTZZ.js +108 -0
- package/dist/chunk-UV67GGHZ.js +1169 -0
- package/dist/index.js +4583 -0
- package/dist/jira-6526JRPQ.js +14 -0
- package/dist/trello-DY6WYSKB.js +24 -0
- package/dist/ui-QJPYDWYD.js +75 -0
- package/package.json +42 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/api.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { Agent } from "undici";
|
|
6
|
+
var ApiError = class extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(message, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ApiError";
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function normalizeBaseUrl(baseUrl) {
|
|
15
|
+
return baseUrl.replace(/\/+$/, "");
|
|
16
|
+
}
|
|
17
|
+
function describeHttpError(status, body) {
|
|
18
|
+
switch (status) {
|
|
19
|
+
case 401:
|
|
20
|
+
return "API anahtar\u0131 ge\xE7ersiz veya eksik (401).";
|
|
21
|
+
case 403:
|
|
22
|
+
return "Eri\u015Fim reddedildi (403).";
|
|
23
|
+
case 404:
|
|
24
|
+
return "Endpoint bulunamad\u0131 \u2014 base URL yanl\u0131\u015F olabilir (404).";
|
|
25
|
+
case 429:
|
|
26
|
+
return "H\u0131z s\u0131n\u0131r\u0131 a\u015F\u0131ld\u0131 (429). Biraz bekleyip tekrar deneyin.";
|
|
27
|
+
default:
|
|
28
|
+
if (status >= 500) return `Sunucu hatas\u0131 (${status}).`;
|
|
29
|
+
return `\u0130stek ba\u015Far\u0131s\u0131z (${status})${body ? ": " + body : ""}.`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function extractDelta(data) {
|
|
33
|
+
if (data === "[DONE]") return null;
|
|
34
|
+
try {
|
|
35
|
+
const json = JSON.parse(data);
|
|
36
|
+
const delta = json?.choices?.[0]?.delta;
|
|
37
|
+
const content = delta?.content;
|
|
38
|
+
if (typeof content === "string" && content.length > 0) {
|
|
39
|
+
return { type: "content", text: content };
|
|
40
|
+
}
|
|
41
|
+
const reasoning = delta?.reasoning_content;
|
|
42
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
43
|
+
return { type: "reasoning", text: reasoning };
|
|
44
|
+
}
|
|
45
|
+
const usage = json?.usage;
|
|
46
|
+
if (usage && typeof usage.total_tokens === "number") {
|
|
47
|
+
return {
|
|
48
|
+
type: "usage",
|
|
49
|
+
text: "",
|
|
50
|
+
usage: {
|
|
51
|
+
promptTokens: usage.prompt_tokens ?? 0,
|
|
52
|
+
completionTokens: usage.completion_tokens ?? 0,
|
|
53
|
+
totalTokens: usage.total_tokens
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function connectErrorMessage(err, url) {
|
|
63
|
+
const e = err;
|
|
64
|
+
const name = e?.name;
|
|
65
|
+
const code = e?.cause?.code;
|
|
66
|
+
if (name === "TimeoutError") {
|
|
67
|
+
return "Zaman a\u015F\u0131m\u0131: sunucu yan\u0131t vermedi (30s). Endpoint/a\u011F eri\u015Fimini ve VPN'i kontrol edin.";
|
|
68
|
+
}
|
|
69
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
70
|
+
return `Adres \xE7\xF6z\xFClemedi (${url}). \u0130\xE7 a\u011F/VPN ba\u011Flant\u0131n\u0131z\u0131 ve endpoint adresini kontrol edin.`;
|
|
71
|
+
}
|
|
72
|
+
if (code === "ECONNREFUSED") {
|
|
73
|
+
return `Ba\u011Flant\u0131 reddedildi (${url}). Sunucu \xE7al\u0131\u015F\u0131yor mu ve port do\u011Fru mu?`;
|
|
74
|
+
}
|
|
75
|
+
if (code === "DEPTH_ZERO_SELF_SIGNED_CERT" || code === "SELF_SIGNED_CERT_IN_CHAIN") {
|
|
76
|
+
return "Sertifika do\u011Frulanamad\u0131 (self-signed). config'te verifySsl: false yap\u0131n veya caBundlePath verin.";
|
|
77
|
+
}
|
|
78
|
+
return `Ba\u011Flant\u0131 kurulamad\u0131: ${e?.message ?? String(err)}${code ? ` (${code})` : ""}`;
|
|
79
|
+
}
|
|
80
|
+
function isRetriableConnError(err) {
|
|
81
|
+
const e = err;
|
|
82
|
+
const code = e?.cause?.code ?? e?.code;
|
|
83
|
+
return code === "ECONNRESET" || code === "UND_ERR_SOCKET" || code === "EPIPE" || code === "ECONNABORTED";
|
|
84
|
+
}
|
|
85
|
+
async function fetchWithRetry(url, init) {
|
|
86
|
+
try {
|
|
87
|
+
return await fetch(url, init);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (!isRetriableConnError(err)) throw err;
|
|
90
|
+
return await fetch(url, init);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function safeText(res) {
|
|
94
|
+
try {
|
|
95
|
+
return (await res.text()).slice(0, 200);
|
|
96
|
+
} catch {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function authHeaders(config) {
|
|
101
|
+
return { Authorization: `Bearer ${config.apiKey}` };
|
|
102
|
+
}
|
|
103
|
+
var cachedDispatcher;
|
|
104
|
+
var cachedKey;
|
|
105
|
+
function getDispatcher(config) {
|
|
106
|
+
const insecure = config.verifySsl === false;
|
|
107
|
+
const useCa = !insecure && Boolean(config.caBundlePath);
|
|
108
|
+
if (!insecure && !useCa) return void 0;
|
|
109
|
+
const key = `${insecure}|${useCa ? config.caBundlePath : ""}`;
|
|
110
|
+
if (cachedDispatcher && cachedKey === key) return cachedDispatcher;
|
|
111
|
+
const connect = {};
|
|
112
|
+
if (insecure) connect.rejectUnauthorized = false;
|
|
113
|
+
if (useCa) {
|
|
114
|
+
try {
|
|
115
|
+
connect.ca = readFileSync(config.caBundlePath, "utf8");
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new ApiError(
|
|
118
|
+
`CA dosyas\u0131 okunamad\u0131 (${config.caBundlePath}): ${err.message}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
cachedDispatcher = new Agent({ connect });
|
|
123
|
+
cachedKey = key;
|
|
124
|
+
return cachedDispatcher;
|
|
125
|
+
}
|
|
126
|
+
function withDispatcher(config, init) {
|
|
127
|
+
const dispatcher = getDispatcher(config);
|
|
128
|
+
if (!dispatcher) return init;
|
|
129
|
+
return { ...init, dispatcher };
|
|
130
|
+
}
|
|
131
|
+
async function listModels(config) {
|
|
132
|
+
const url = `${normalizeBaseUrl(config.baseUrl)}/models`;
|
|
133
|
+
let res;
|
|
134
|
+
try {
|
|
135
|
+
res = await fetchWithRetry(
|
|
136
|
+
url,
|
|
137
|
+
withDispatcher(config, {
|
|
138
|
+
headers: authHeaders(config),
|
|
139
|
+
signal: AbortSignal.timeout(3e4)
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
144
|
+
}
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
throw new ApiError(describeHttpError(res.status, await safeText(res)), res.status);
|
|
147
|
+
}
|
|
148
|
+
const data = await res.json();
|
|
149
|
+
const list = Array.isArray(data?.data) ? data.data : [];
|
|
150
|
+
return list.map((m) => m.id).filter((id) => Boolean(id)).sort();
|
|
151
|
+
}
|
|
152
|
+
async function* streamChat(config, messages, signal) {
|
|
153
|
+
const url = `${normalizeBaseUrl(config.baseUrl)}/chat/completions`;
|
|
154
|
+
let res;
|
|
155
|
+
try {
|
|
156
|
+
res = await fetchWithRetry(
|
|
157
|
+
url,
|
|
158
|
+
withDispatcher(config, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
...authHeaders(config)
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
model: config.model,
|
|
166
|
+
messages,
|
|
167
|
+
stream: true,
|
|
168
|
+
stream_options: { include_usage: true }
|
|
169
|
+
}),
|
|
170
|
+
...signal ? { signal } : {}
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
175
|
+
}
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
throw new ApiError(describeHttpError(res.status, await safeText(res)), res.status);
|
|
178
|
+
}
|
|
179
|
+
if (!res.body) throw new ApiError("Yan\u0131t g\xF6vdesi bo\u015F.");
|
|
180
|
+
yield* parseSSE(res.body);
|
|
181
|
+
}
|
|
182
|
+
function isTransientError(err) {
|
|
183
|
+
const e = err;
|
|
184
|
+
if (e?.name === "AbortError") return true;
|
|
185
|
+
if (err instanceof ApiError) {
|
|
186
|
+
if (err.status === void 0) return true;
|
|
187
|
+
return err.status >= 500 || err.status === 429;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
var TTFB_TIMEOUT_MS = 3e4;
|
|
192
|
+
var RATE_LIMIT_BACKOFFS = [8e3, 16e3, 32e3, 64e3];
|
|
193
|
+
async function* streamChatWithRetry(config, messages, attempts = 4, backoffMs = 1e3, onRetry) {
|
|
194
|
+
let lastErr;
|
|
195
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
196
|
+
const ctrl = new AbortController();
|
|
197
|
+
const timer = setTimeout(() => ctrl.abort(), TTFB_TIMEOUT_MS);
|
|
198
|
+
let yielded = false;
|
|
199
|
+
try {
|
|
200
|
+
for await (const ev of streamChat(config, messages, ctrl.signal)) {
|
|
201
|
+
if (!yielded) {
|
|
202
|
+
yielded = true;
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
}
|
|
205
|
+
yield ev;
|
|
206
|
+
}
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
return;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
const e = ctrl.signal.aborted ? new ApiError("Sunucu zaman\u0131nda yan\u0131t vermedi (timeout).") : err;
|
|
212
|
+
lastErr = e;
|
|
213
|
+
if (yielded || !isTransientError(e)) throw e;
|
|
214
|
+
if (attempt < attempts) {
|
|
215
|
+
const is429 = e instanceof ApiError && e.status === 429;
|
|
216
|
+
const wait = is429 ? RATE_LIMIT_BACKOFFS[attempt - 1] ?? 64e3 : backoffMs * attempt;
|
|
217
|
+
onRetry?.(attempt, wait, is429);
|
|
218
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
throw lastErr;
|
|
223
|
+
}
|
|
224
|
+
async function* parseSSE(body) {
|
|
225
|
+
const reader = body.getReader();
|
|
226
|
+
const decoder = new TextDecoder();
|
|
227
|
+
let buffer = "";
|
|
228
|
+
try {
|
|
229
|
+
while (true) {
|
|
230
|
+
const { done, value } = await reader.read();
|
|
231
|
+
if (done) break;
|
|
232
|
+
buffer += decoder.decode(value, { stream: true });
|
|
233
|
+
let idx;
|
|
234
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
235
|
+
const line = buffer.slice(0, idx).trim();
|
|
236
|
+
buffer = buffer.slice(idx + 1);
|
|
237
|
+
if (!line.startsWith("data:")) continue;
|
|
238
|
+
const data = line.slice(5).trim();
|
|
239
|
+
if (data === "[DONE]") return;
|
|
240
|
+
const ev = extractDelta(data);
|
|
241
|
+
if (ev) yield ev;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
reader.releaseLock();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export {
|
|
250
|
+
ApiError,
|
|
251
|
+
describeHttpError,
|
|
252
|
+
connectErrorMessage,
|
|
253
|
+
getDispatcher,
|
|
254
|
+
listModels,
|
|
255
|
+
streamChat,
|
|
256
|
+
streamChatWithRetry
|
|
257
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApiError,
|
|
4
|
+
connectErrorMessage,
|
|
5
|
+
describeHttpError,
|
|
6
|
+
getDispatcher
|
|
7
|
+
} from "./chunk-JGVBNMNK.js";
|
|
8
|
+
|
|
9
|
+
// src/trello.ts
|
|
10
|
+
import { stdin, stdout } from "process";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
var SHORT_LINK_RE = /^[a-zA-Z0-9]{8}$/;
|
|
13
|
+
function extractCardId(input) {
|
|
14
|
+
const s = input.trim();
|
|
15
|
+
const urlMatch = s.match(/\/c\/([a-zA-Z0-9]{8})(?:\/|$)/);
|
|
16
|
+
if (urlMatch) return urlMatch[1];
|
|
17
|
+
if (SHORT_LINK_RE.test(s)) return s;
|
|
18
|
+
throw new ApiError(`Ge\xE7erli bir Trello kart ID'si bulunamad\u0131: "${input}". \xD6rnek: AbCdEfGh veya .../c/AbCdEfGh/...`);
|
|
19
|
+
}
|
|
20
|
+
function describeTrelloError(status) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 401:
|
|
23
|
+
return "Trello kimlik do\u011Frulamas\u0131 ba\u015Far\u0131s\u0131z (401). /trello-login ile yenileyin.";
|
|
24
|
+
case 404:
|
|
25
|
+
return "Trello kart\u0131 bulunamad\u0131 veya eri\u015Fim yetkiniz yok (404).";
|
|
26
|
+
default:
|
|
27
|
+
return describeHttpError(status);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function trelloUrl(path, config) {
|
|
31
|
+
if (!config.trelloKey || !config.trelloToken) {
|
|
32
|
+
throw new ApiError("Trello yap\u0131land\u0131rmas\u0131 yok. \xD6nce /trello-login yap\u0131n.");
|
|
33
|
+
}
|
|
34
|
+
return `https://api.trello.com${path}?key=${encodeURIComponent(config.trelloKey)}&token=${encodeURIComponent(config.trelloToken)}`;
|
|
35
|
+
}
|
|
36
|
+
function trelloInit(config) {
|
|
37
|
+
const dispatcher = getDispatcher(config);
|
|
38
|
+
const base = { signal: AbortSignal.timeout(15e3) };
|
|
39
|
+
if (!dispatcher) return base;
|
|
40
|
+
return { ...base, dispatcher };
|
|
41
|
+
}
|
|
42
|
+
async function fetchListName(config, idList) {
|
|
43
|
+
try {
|
|
44
|
+
const url = trelloUrl(`/1/lists/${encodeURIComponent(idList)}`, config) + "&fields=name";
|
|
45
|
+
const res = await fetch(url, trelloInit(config));
|
|
46
|
+
if (!res.ok) return "(liste bilinmiyor)";
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
return data.name ?? "(liste bilinmiyor)";
|
|
49
|
+
} catch {
|
|
50
|
+
return "(liste bilinmiyor)";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function fetchCard(config, cardId) {
|
|
54
|
+
const url = trelloUrl(`/1/cards/${encodeURIComponent(cardId)}`, config) + "&fields=name,desc,idList,labels,shortLink,url";
|
|
55
|
+
let res;
|
|
56
|
+
try {
|
|
57
|
+
res = await fetch(url, trelloInit(config));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) throw new ApiError(describeTrelloError(res.status), res.status);
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
const listName = data.idList ? await fetchListName(config, data.idList) : "(liste bilinmiyor)";
|
|
64
|
+
const labels = (data.labels ?? []).map((l) => l.name ?? l.color ?? "").filter(Boolean);
|
|
65
|
+
return {
|
|
66
|
+
id: data.shortLink ?? cardId,
|
|
67
|
+
shortLink: data.shortLink ?? cardId,
|
|
68
|
+
name: data.name ?? "(ba\u015Fl\u0131k yok)",
|
|
69
|
+
desc: data.desc ?? "",
|
|
70
|
+
listName,
|
|
71
|
+
labels
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function fetchBoards(config) {
|
|
75
|
+
const url = trelloUrl("/1/members/me/boards", config) + "&fields=name,url&filter=open";
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await fetch(url, trelloInit(config));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
81
|
+
}
|
|
82
|
+
if (!res.ok) throw new ApiError(describeTrelloError(res.status), res.status);
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
return data.map((b) => ({ id: b.id, name: b.name, url: b.url }));
|
|
85
|
+
}
|
|
86
|
+
var TRELLO_BACK = /* @__PURE__ */ Symbol("TRELLO_BACK");
|
|
87
|
+
async function runTrelloBoardPicker(boards, initialSel = 0) {
|
|
88
|
+
if (boards.length === 0) return null;
|
|
89
|
+
if (!stdin.isTTY || !stdout.isTTY) return boards[0];
|
|
90
|
+
const brand = chalk.hex("#1577d4").bold;
|
|
91
|
+
const dim = chalk.dim;
|
|
92
|
+
const hi = chalk.white.bold;
|
|
93
|
+
const hiNum = chalk.greenBright.bold;
|
|
94
|
+
function draw(sel) {
|
|
95
|
+
const R = stdout.rows || 24;
|
|
96
|
+
const lines = [];
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push(" " + brand("Trello") + dim(" \u2014 pano se\xE7"));
|
|
99
|
+
lines.push("");
|
|
100
|
+
boards.forEach((b, i) => {
|
|
101
|
+
const selected = i === sel;
|
|
102
|
+
lines.push(
|
|
103
|
+
(selected ? hiNum(` \u203A ${i + 1}.`) : dim(` ${i + 1}.`)) + " " + (selected ? hi(b.name) : chalk.white(b.name))
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(dim(" \u2191 \u2193 gezin Enter se\xE7 Ctrl+C iptal"));
|
|
108
|
+
lines.push("");
|
|
109
|
+
let buf = "\x1B[?25l\x1B[H";
|
|
110
|
+
for (let r = 0; r < R; r++) buf += `\x1B[${r + 1};1H\x1B[2K` + (lines[r] ?? "");
|
|
111
|
+
stdout.write(buf);
|
|
112
|
+
}
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
let sel = Math.max(0, Math.min(initialSel, boards.length - 1));
|
|
115
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
stdin.removeListener("data", onData);
|
|
118
|
+
stdin.setRawMode(wasRaw);
|
|
119
|
+
if (!wasRaw) stdin.pause();
|
|
120
|
+
stdout.write("\x1B[?25h\x1B[2J\x1B[H");
|
|
121
|
+
};
|
|
122
|
+
const onData = (chunk) => {
|
|
123
|
+
const code = chunk.charCodeAt(0);
|
|
124
|
+
if (code === 3) {
|
|
125
|
+
cleanup();
|
|
126
|
+
resolve(null);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (code === 27 && chunk.length === 1) {
|
|
130
|
+
cleanup();
|
|
131
|
+
resolve(null);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (chunk === "\x1B[A") {
|
|
135
|
+
sel = sel > 0 ? sel - 1 : boards.length - 1;
|
|
136
|
+
draw(sel);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (chunk === "\x1B[B") {
|
|
140
|
+
sel = sel < boards.length - 1 ? sel + 1 : 0;
|
|
141
|
+
draw(sel);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (chunk === "\r" || chunk === "\n") {
|
|
145
|
+
cleanup();
|
|
146
|
+
resolve(boards[sel]);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
stdin.setRawMode(true);
|
|
150
|
+
stdin.resume();
|
|
151
|
+
stdin.setEncoding("utf8");
|
|
152
|
+
stdin.on("data", onData);
|
|
153
|
+
draw(sel);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function fetchBoardCards(config, boardId) {
|
|
157
|
+
const url = trelloUrl(`/1/boards/${encodeURIComponent(boardId)}/cards`, config) + "&fields=name,shortLink,idList&filter=open";
|
|
158
|
+
let res;
|
|
159
|
+
try {
|
|
160
|
+
res = await fetch(url, trelloInit(config));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
163
|
+
}
|
|
164
|
+
if (!res.ok) throw new ApiError(describeTrelloError(res.status), res.status);
|
|
165
|
+
const data = await res.json();
|
|
166
|
+
const listIds = [...new Set(data.map((c) => c.idList))];
|
|
167
|
+
const listNames = /* @__PURE__ */ new Map();
|
|
168
|
+
await Promise.all(listIds.map(async (id) => {
|
|
169
|
+
listNames.set(id, await fetchListName(config, id));
|
|
170
|
+
}));
|
|
171
|
+
return data.map((c) => ({
|
|
172
|
+
shortLink: c.shortLink,
|
|
173
|
+
name: c.name,
|
|
174
|
+
listName: listNames.get(c.idList) ?? "(liste bilinmiyor)"
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
async function runTrelloCardPicker(cards, boardName) {
|
|
178
|
+
if (cards.length === 0) return TRELLO_BACK;
|
|
179
|
+
if (!stdin.isTTY || !stdout.isTTY) return cards[0];
|
|
180
|
+
const brand = chalk.hex("#1577d4").bold;
|
|
181
|
+
const dim = chalk.dim;
|
|
182
|
+
const hi = chalk.white.bold;
|
|
183
|
+
const hiNum = chalk.greenBright.bold;
|
|
184
|
+
function draw(sel, offset) {
|
|
185
|
+
const R = stdout.rows || 24;
|
|
186
|
+
const C = stdout.columns || 80;
|
|
187
|
+
const maxVisible = Math.max(1, R - 6);
|
|
188
|
+
const lines = [];
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push(" " + brand("Trello") + dim(` \u2014 ${boardName} \u2014 kart se\xE7`));
|
|
191
|
+
lines.push("");
|
|
192
|
+
const visible = cards.slice(offset, offset + maxVisible);
|
|
193
|
+
visible.forEach((c, vi) => {
|
|
194
|
+
const i = vi + offset;
|
|
195
|
+
const selected = i === sel;
|
|
196
|
+
const list = dim(`[${c.listName}] `);
|
|
197
|
+
const title = c.name.slice(0, C - 20);
|
|
198
|
+
lines.push(
|
|
199
|
+
(selected ? hiNum(` \u203A ${i + 1}.`) : dim(` ${i + 1}.`)) + " " + list + (selected ? hi(title) : chalk.white(title))
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push(dim(" \u2191 \u2193 gezin Enter se\xE7 ESC panolara d\xF6n Ctrl+C iptal"));
|
|
204
|
+
lines.push("");
|
|
205
|
+
let buf = "\x1B[?25l\x1B[H";
|
|
206
|
+
for (let r = 0; r < R; r++) buf += `\x1B[${r + 1};1H\x1B[2K` + (lines[r] ?? "");
|
|
207
|
+
stdout.write(buf);
|
|
208
|
+
}
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
let sel = 0;
|
|
211
|
+
let offset = 0;
|
|
212
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
213
|
+
const maxVisible = Math.max(1, (stdout.rows || 24) - 6);
|
|
214
|
+
const cleanup = () => {
|
|
215
|
+
stdin.removeListener("data", onData);
|
|
216
|
+
stdin.setRawMode(wasRaw);
|
|
217
|
+
if (!wasRaw) stdin.pause();
|
|
218
|
+
stdout.write("\x1B[?25h\x1B[2J\x1B[H");
|
|
219
|
+
};
|
|
220
|
+
const onData = (chunk) => {
|
|
221
|
+
const code = chunk.charCodeAt(0);
|
|
222
|
+
if (code === 3) {
|
|
223
|
+
cleanup();
|
|
224
|
+
resolve(null);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (code === 27 && chunk.length === 1) {
|
|
228
|
+
cleanup();
|
|
229
|
+
resolve(TRELLO_BACK);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (chunk === "\x1B[A") {
|
|
233
|
+
if (sel > 0) {
|
|
234
|
+
sel--;
|
|
235
|
+
if (sel < offset) offset = sel;
|
|
236
|
+
}
|
|
237
|
+
draw(sel, offset);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (chunk === "\x1B[B") {
|
|
241
|
+
if (sel < cards.length - 1) {
|
|
242
|
+
sel++;
|
|
243
|
+
if (sel >= offset + maxVisible) offset = sel - maxVisible + 1;
|
|
244
|
+
}
|
|
245
|
+
draw(sel, offset);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (chunk === "\r" || chunk === "\n") {
|
|
249
|
+
cleanup();
|
|
250
|
+
resolve(cards[sel]);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
stdin.setRawMode(true);
|
|
254
|
+
stdin.resume();
|
|
255
|
+
stdin.setEncoding("utf8");
|
|
256
|
+
stdin.on("data", onData);
|
|
257
|
+
draw(sel, offset);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async function validateTrelloAuth(config) {
|
|
261
|
+
const url = trelloUrl("/1/members/me", config) + "&fields=fullName,username";
|
|
262
|
+
let res;
|
|
263
|
+
try {
|
|
264
|
+
res = await fetch(url, trelloInit(config));
|
|
265
|
+
} catch (err) {
|
|
266
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
267
|
+
}
|
|
268
|
+
if (!res.ok) throw new ApiError(describeTrelloError(res.status), res.status);
|
|
269
|
+
const data = await res.json();
|
|
270
|
+
return data.fullName ?? data.username ?? "(bilinmeyen kullan\u0131c\u0131)";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export {
|
|
274
|
+
extractCardId,
|
|
275
|
+
describeTrelloError,
|
|
276
|
+
fetchCard,
|
|
277
|
+
fetchBoards,
|
|
278
|
+
TRELLO_BACK,
|
|
279
|
+
runTrelloBoardPicker,
|
|
280
|
+
fetchBoardCards,
|
|
281
|
+
runTrelloCardPicker,
|
|
282
|
+
validateTrelloAuth
|
|
283
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApiError,
|
|
4
|
+
connectErrorMessage,
|
|
5
|
+
describeHttpError,
|
|
6
|
+
getDispatcher
|
|
7
|
+
} from "./chunk-JGVBNMNK.js";
|
|
8
|
+
|
|
9
|
+
// src/jira.ts
|
|
10
|
+
var KEY = "[A-Z][A-Z0-9]+-\\d+";
|
|
11
|
+
function extractIssueKey(input) {
|
|
12
|
+
const s = input.trim();
|
|
13
|
+
const browse = s.match(new RegExp(`/browse/(${KEY})`, "i"));
|
|
14
|
+
if (browse) return browse[1].toUpperCase();
|
|
15
|
+
const selected = s.match(new RegExp(`[?&]selectedIssue=(${KEY})`, "i"));
|
|
16
|
+
if (selected) return selected[1].toUpperCase();
|
|
17
|
+
const bare = s.match(new RegExp(`^(${KEY})$`));
|
|
18
|
+
if (bare) return bare[1].toUpperCase();
|
|
19
|
+
const all = s.match(new RegExp(`(${KEY})`, "g"));
|
|
20
|
+
if (all && all.length > 0) return all[all.length - 1].toUpperCase();
|
|
21
|
+
throw new ApiError(
|
|
22
|
+
`Ge\xE7erli bir Jira issue key bulunamad\u0131: "${input}". \xD6rnek: PROJ-1234 veya .../browse/PROJ-1234`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
function describeJiraError(status, key) {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case 401:
|
|
28
|
+
return "Jira PAT ge\xE7ersiz veya s\xFCresi dolmu\u015F (401). /jira-login ile yenileyin.";
|
|
29
|
+
case 403:
|
|
30
|
+
return "Jira eri\u015Fimi reddedildi (403). Token yetkilerini kontrol edin.";
|
|
31
|
+
case 404:
|
|
32
|
+
return `Issue bulunamad\u0131 veya eri\u015Fim yetkiniz yok: ${key} (404).`;
|
|
33
|
+
default:
|
|
34
|
+
return describeHttpError(status);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function adfToText(node, depth = 0) {
|
|
38
|
+
if (typeof node === "string") return node;
|
|
39
|
+
if (!node || typeof node !== "object") return "";
|
|
40
|
+
const n = node;
|
|
41
|
+
if (n.type === "text") return typeof n.text === "string" ? n.text : "";
|
|
42
|
+
const children = Array.isArray(n.content) ? n.content.map((c) => adfToText(c, depth + 1)).join("") : "";
|
|
43
|
+
const block = ["paragraph", "heading", "bulletList", "orderedList", "listItem", "blockquote", "codeBlock", "rule"];
|
|
44
|
+
return block.includes(n.type) ? children + "\n" : children;
|
|
45
|
+
}
|
|
46
|
+
function extractDescription(raw) {
|
|
47
|
+
if (typeof raw === "string") return raw;
|
|
48
|
+
if (raw && typeof raw === "object") return adfToText(raw).trim();
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
function jiraBase(config) {
|
|
52
|
+
if (!config.jiraBaseUrl || !config.jiraToken) {
|
|
53
|
+
throw new ApiError("Jira yap\u0131land\u0131rmas\u0131 yok. \xD6nce /jira-login yap\u0131n.");
|
|
54
|
+
}
|
|
55
|
+
return config.jiraBaseUrl.replace(/\/+$/, "");
|
|
56
|
+
}
|
|
57
|
+
function jiraInit(config, init) {
|
|
58
|
+
const headers = {
|
|
59
|
+
Authorization: `Bearer ${config.jiraToken}`,
|
|
60
|
+
Accept: "application/json",
|
|
61
|
+
...init.headers ?? {}
|
|
62
|
+
};
|
|
63
|
+
const dispatcher = getDispatcher(config);
|
|
64
|
+
const base = { ...init, headers, signal: AbortSignal.timeout(3e4) };
|
|
65
|
+
if (!dispatcher) return base;
|
|
66
|
+
return { ...base, dispatcher };
|
|
67
|
+
}
|
|
68
|
+
async function fetchIssue(config, key) {
|
|
69
|
+
const url = `${jiraBase(config)}/rest/api/2/issue/${encodeURIComponent(key)}?fields=summary,description,issuetype`;
|
|
70
|
+
let res;
|
|
71
|
+
try {
|
|
72
|
+
res = await fetch(url, jiraInit(config, {}));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new ApiError(describeJiraError(res.status, key), res.status);
|
|
78
|
+
}
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
const f = data.fields ?? {};
|
|
81
|
+
return {
|
|
82
|
+
key,
|
|
83
|
+
summary: f.summary ?? "(ba\u015Fl\u0131k yok)",
|
|
84
|
+
description: extractDescription(f.description),
|
|
85
|
+
issueType: f.issuetype?.name ?? "Task"
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function validateAuth(config) {
|
|
89
|
+
const url = `${jiraBase(config)}/rest/api/2/myself`;
|
|
90
|
+
let res;
|
|
91
|
+
try {
|
|
92
|
+
res = await fetch(url, jiraInit(config, {}));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new ApiError(connectErrorMessage(err, url));
|
|
95
|
+
}
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new ApiError(describeJiraError(res.status, "(myself)"), res.status);
|
|
98
|
+
}
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
return data.displayName ?? data.name ?? "(bilinmeyen kullan\u0131c\u0131)";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
extractIssueKey,
|
|
105
|
+
describeJiraError,
|
|
106
|
+
fetchIssue,
|
|
107
|
+
validateAuth
|
|
108
|
+
};
|