atlass 1.12.0 → 1.14.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 +17 -3
- package/dist/bitbucket.d.mts +1 -1
- package/dist/bitbucket.mjs +1 -1
- package/dist/{build-2A5pjMrK.mjs → build-CFyTL_x2.mjs} +2010 -1505
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/confluence.d.mts +1 -1
- package/dist/confluence.mjs +1 -1
- package/dist/jira.d.mts +1 -1
- package/dist/jira.mjs +1 -1
- package/package.json +4 -4
|
@@ -1,33 +1,62 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { checkbox, confirm, editor, input, password, search, select } from "@inquirer/prompts";
|
|
3
2
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
4
|
import { Entry } from "@napi-rs/keyring";
|
|
7
|
-
import
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
8
7
|
import { stripVTControlCharacters } from "node:util";
|
|
8
|
+
import { checkbox, confirm, editor, input, password, search, select } from "@inquirer/prompts";
|
|
9
|
+
import kleur from "kleur";
|
|
10
|
+
import { common, createLowlight } from "lowlight";
|
|
9
11
|
import { marked } from "marked";
|
|
10
12
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
//#region src/api/http-error.ts
|
|
14
|
+
var HttpError = class extends Error {
|
|
15
|
+
status;
|
|
16
|
+
jira;
|
|
17
|
+
path;
|
|
18
|
+
constructor(status, message, jira = {
|
|
19
|
+
errorMessages: [],
|
|
20
|
+
errors: {}
|
|
21
|
+
}, path = "") {
|
|
22
|
+
super(message);
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.jira = jira;
|
|
25
|
+
this.path = path;
|
|
26
|
+
this.name = "HttpError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function httpError(status, path, body, loginHint) {
|
|
30
|
+
if (status === 401 || status === 403) return new HttpError(status, `Authentication failed (401/403). Run \`${loginHint}\` to update your token.`, void 0, path);
|
|
31
|
+
if (status === 404) return new HttpError(status, `Not found (404): ${path}`);
|
|
32
|
+
if (status === 409) return new HttpError(status, `Conflict (409): ${extractErrorMessage(body) || "the page changed on the server"}`);
|
|
33
|
+
if (status === 413) return new HttpError(status, "Payload too large (413): the page or an attachment exceeds the size limit.");
|
|
34
|
+
if (status === 400) return new HttpError(status, `Bad request (400): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
35
|
+
return new HttpError(status, `Request failed (${status}): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
23
36
|
}
|
|
24
|
-
function
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
function extractErrorMessage(body) {
|
|
38
|
+
if (!body) return "";
|
|
39
|
+
const json = tryParseJson(body);
|
|
40
|
+
const { errorMessages, errors } = jiraErrors(body);
|
|
41
|
+
const fromJira = [...errorMessages, ...Object.entries(errors).map(([f, m]) => `${f}: ${m}`)];
|
|
42
|
+
if (fromJira.length) return fromJira.join("; ");
|
|
43
|
+
if (json?.message) return json.message;
|
|
44
|
+
if (json?.error?.message) return json.error.message;
|
|
45
|
+
return body.slice(0, 300);
|
|
28
46
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
47
|
+
function jiraErrors(body) {
|
|
48
|
+
const json = tryParseJson(body);
|
|
49
|
+
return {
|
|
50
|
+
errorMessages: json?.errorMessages ?? [],
|
|
51
|
+
errors: json?.errors ?? {}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function tryParseJson(text) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(text);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
31
60
|
}
|
|
32
61
|
//#endregion
|
|
33
62
|
//#region src/api/client.ts
|
|
@@ -35,10 +64,12 @@ const DISABLE_XSRF_CHECK = { "X-Atlassian-Token": "nocheck" };
|
|
|
35
64
|
var AtlassianClient = class {
|
|
36
65
|
site;
|
|
37
66
|
authHeader;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
loginHint;
|
|
68
|
+
constructor(credentials, loginHint) {
|
|
69
|
+
this.site = credentials.site;
|
|
70
|
+
const basic = Buffer.from(`${credentials.email}:${credentials.token}`).toString("base64");
|
|
41
71
|
this.authHeader = `Basic ${basic}`;
|
|
72
|
+
this.loginHint = loginHint;
|
|
42
73
|
}
|
|
43
74
|
async request(path, init) {
|
|
44
75
|
const res = await fetch(`${this.site}${path}`, {
|
|
@@ -51,7 +82,7 @@ var AtlassianClient = class {
|
|
|
51
82
|
});
|
|
52
83
|
if (!res.ok) {
|
|
53
84
|
const body = await res.text().catch(() => "");
|
|
54
|
-
throw httpError(res.status, path, body);
|
|
85
|
+
throw httpError(res.status, path, body, this.loginHint);
|
|
55
86
|
}
|
|
56
87
|
return res;
|
|
57
88
|
}
|
|
@@ -111,204 +142,539 @@ function pathAndQuery(absoluteUrl) {
|
|
|
111
142
|
const u = new URL(absoluteUrl);
|
|
112
143
|
return u.pathname + u.search;
|
|
113
144
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/api/session.ts
|
|
147
|
+
const BITBUCKET_ORIGIN = "https://api.bitbucket.org";
|
|
148
|
+
const ATLASSIAN_LOGIN = "atlass auth login";
|
|
149
|
+
const BITBUCKET_LOGIN = "atlass bitbucket login";
|
|
150
|
+
var BitbucketClient = class extends AtlassianClient {
|
|
151
|
+
workspace;
|
|
152
|
+
defaultRepo;
|
|
153
|
+
uuid;
|
|
154
|
+
constructor(email, token, bitbucket) {
|
|
155
|
+
super({
|
|
156
|
+
site: BITBUCKET_ORIGIN,
|
|
157
|
+
email,
|
|
158
|
+
token
|
|
159
|
+
}, BITBUCKET_LOGIN);
|
|
160
|
+
this.workspace = bitbucket.workspace;
|
|
161
|
+
this.defaultRepo = bitbucket.defaultRepo;
|
|
162
|
+
this.uuid = bitbucket.uuid;
|
|
125
163
|
}
|
|
126
164
|
};
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
if (status === 404) return new HttpError(status, `Not found (404): ${path}`);
|
|
130
|
-
if (status === 409) return new HttpError(status, `Conflict (409): ${extractErrorMessage(body) || "the page changed on the server"}`);
|
|
131
|
-
if (status === 413) return new HttpError(status, "Payload too large (413): the page or an attachment exceeds the size limit.");
|
|
132
|
-
if (status === 400) return new HttpError(status, `Bad request (400): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
133
|
-
return new HttpError(status, `Request failed (${status}): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
165
|
+
function sessionFor(credentials) {
|
|
166
|
+
return new AtlassianClient(credentials, ATLASSIAN_LOGIN);
|
|
134
167
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
const json = tryParseJson(body);
|
|
138
|
-
const { errorMessages, errors } = jiraErrors(body);
|
|
139
|
-
const fromJira = [...errorMessages, ...Object.entries(errors).map(([f, m]) => `${f}: ${m}`)];
|
|
140
|
-
if (fromJira.length) return fromJira.join("; ");
|
|
141
|
-
if (json?.message) return json.message;
|
|
142
|
-
if (json?.error?.message) return json.error.message;
|
|
143
|
-
return body.slice(0, 300);
|
|
168
|
+
function bitbucketSessionFor(email, token, bitbucket) {
|
|
169
|
+
return new BitbucketClient(email, token, bitbucket);
|
|
144
170
|
}
|
|
145
|
-
function
|
|
146
|
-
const
|
|
171
|
+
async function openSession(profile) {
|
|
172
|
+
const config = await profile.read();
|
|
173
|
+
if (!config?.site || !config.email) throw new Error("Not logged in. Run `atlass auth login` first.");
|
|
174
|
+
const token = await profile.token(config.email, "atlassian");
|
|
175
|
+
if (!token) throw new Error("No API token found in keyring. Run `atlass auth login` again.");
|
|
176
|
+
return sessionFor({
|
|
177
|
+
site: config.site,
|
|
178
|
+
email: config.email,
|
|
179
|
+
token
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async function openBitbucketSession(profile) {
|
|
183
|
+
const config = await profile.read();
|
|
184
|
+
if (!config?.email || !config.bitbucket?.workspace) throw new Error("Not logged in to Bitbucket. Run `atlass bitbucket login` first.");
|
|
185
|
+
const token = await profile.token(config.email, "bitbucket");
|
|
186
|
+
if (!token) throw new Error("No Bitbucket API token found in keyring. Run `atlass bitbucket login` again.");
|
|
187
|
+
return bitbucketSessionFor(config.email, token, config.bitbucket);
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/files/disk.ts
|
|
191
|
+
async function makeRoom(path) {
|
|
192
|
+
await mkdir(dirname(path), { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
function diskFiles() {
|
|
147
195
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
196
|
+
async readText(path) {
|
|
197
|
+
return readFile(path, "utf8");
|
|
198
|
+
},
|
|
199
|
+
async readBytes(path) {
|
|
200
|
+
const buffer = await readFile(path);
|
|
201
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
202
|
+
},
|
|
203
|
+
async size(path) {
|
|
204
|
+
return (await stat(path)).size;
|
|
205
|
+
},
|
|
206
|
+
async writeText(path, text) {
|
|
207
|
+
await makeRoom(path);
|
|
208
|
+
await writeFile(path, text, "utf8");
|
|
209
|
+
},
|
|
210
|
+
async writeBytes(path, bytes) {
|
|
211
|
+
await makeRoom(path);
|
|
212
|
+
await writeFile(path, bytes);
|
|
213
|
+
}
|
|
150
214
|
};
|
|
151
215
|
}
|
|
152
|
-
function tryParseJson(text) {
|
|
153
|
-
try {
|
|
154
|
-
return JSON.parse(text);
|
|
155
|
-
} catch {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
216
|
//#endregion
|
|
160
|
-
//#region src/
|
|
217
|
+
//#region src/profile/disk.ts
|
|
218
|
+
const SERVICE = "atlass";
|
|
161
219
|
function configDir() {
|
|
162
|
-
|
|
220
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
221
|
+
return join(base, "atlass");
|
|
163
222
|
}
|
|
164
223
|
function configPath() {
|
|
165
224
|
return join(configDir(), "config.json");
|
|
166
225
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const raw = await readFile(configPath(), "utf8");
|
|
170
|
-
const parsed = JSON.parse(raw);
|
|
171
|
-
return {
|
|
172
|
-
site: parsed.site,
|
|
173
|
-
email: parsed.email,
|
|
174
|
-
bitbucket: parsed.bitbucket
|
|
175
|
-
};
|
|
176
|
-
} catch {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async function writeConfig(config) {
|
|
181
|
-
await mkdir(configDir(), { recursive: true });
|
|
182
|
-
await writeFile(configPath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
226
|
+
function entry(email, kind) {
|
|
227
|
+
return new Entry(SERVICE, kind === "bitbucket" ? `${email}:bitbucket` : email);
|
|
183
228
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
229
|
+
function diskProfile() {
|
|
230
|
+
return {
|
|
231
|
+
async read() {
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(await readFile(configPath(), "utf8"));
|
|
234
|
+
return {
|
|
235
|
+
site: parsed.site,
|
|
236
|
+
email: parsed.email,
|
|
237
|
+
bitbucket: parsed.bitbucket
|
|
238
|
+
};
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
async write(config) {
|
|
244
|
+
await mkdir(configDir(), { recursive: true });
|
|
245
|
+
await writeFile(configPath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
246
|
+
},
|
|
247
|
+
async clear() {
|
|
248
|
+
await rm(configPath(), { force: true });
|
|
249
|
+
},
|
|
250
|
+
async token(email, kind) {
|
|
251
|
+
return entry(email, kind).getPassword();
|
|
252
|
+
},
|
|
253
|
+
async setToken(email, kind, token) {
|
|
254
|
+
entry(email, kind).setPassword(token);
|
|
255
|
+
},
|
|
256
|
+
async deleteToken(email, kind) {
|
|
257
|
+
try {
|
|
258
|
+
entry(email, kind).deleteCredential();
|
|
259
|
+
} catch {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
191
264
|
}
|
|
192
265
|
//#endregion
|
|
193
|
-
//#region src/
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
266
|
+
//#region src/terminal/pager.ts
|
|
267
|
+
const DEFAULT_ROWS = 24;
|
|
268
|
+
const DEFAULT_COLUMNS = 80;
|
|
269
|
+
const DEFAULT_PAGER = "less";
|
|
270
|
+
const DEFAULT_LESS = "FRX";
|
|
271
|
+
function renderedHeight(text, columns) {
|
|
272
|
+
const width = Math.max(1, columns);
|
|
273
|
+
return text.split("\n").map((line) => Math.max(1, Math.ceil(stripVTControlCharacters(line).length / width))).reduce((sum, rows) => sum + rows, 0);
|
|
198
274
|
}
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
275
|
+
function shouldPage(text, screen) {
|
|
276
|
+
if (!screen.isTTY) return false;
|
|
277
|
+
const rows = screen.rows ?? DEFAULT_ROWS;
|
|
278
|
+
return renderedHeight(text, screen.columns ?? DEFAULT_COLUMNS) > rows;
|
|
205
279
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
280
|
+
function pagerCommand(env) {
|
|
281
|
+
const [command = DEFAULT_PAGER, ...args] = (env["PAGER"] ?? "").trim().split(/\s+/).filter(Boolean);
|
|
282
|
+
return {
|
|
283
|
+
command,
|
|
284
|
+
args,
|
|
285
|
+
env: {
|
|
286
|
+
...env,
|
|
287
|
+
LESS: env["LESS"] ?? DEFAULT_LESS
|
|
288
|
+
}
|
|
289
|
+
};
|
|
208
290
|
}
|
|
209
|
-
function
|
|
210
|
-
|
|
291
|
+
async function tryPager(text, options = {}) {
|
|
292
|
+
if (!shouldPage(text, options.screen ?? process.stdout)) return false;
|
|
293
|
+
return pipeToPager(text, pagerCommand(options.env ?? process.env));
|
|
211
294
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
295
|
+
function pipeToPager(text, pager) {
|
|
296
|
+
return new Promise((resolve) => {
|
|
297
|
+
const child = spawn(pager.command, pager.args, {
|
|
298
|
+
stdio: [
|
|
299
|
+
"pipe",
|
|
300
|
+
"inherit",
|
|
301
|
+
"inherit"
|
|
302
|
+
],
|
|
303
|
+
env: pager.env
|
|
304
|
+
});
|
|
305
|
+
child.once("error", () => resolve(false));
|
|
306
|
+
child.once("close", () => resolve(true));
|
|
307
|
+
child.stdin.once("error", () => {});
|
|
308
|
+
child.stdin.end(`${text}\n`);
|
|
309
|
+
});
|
|
214
310
|
}
|
|
215
|
-
|
|
216
|
-
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/terminal.ts
|
|
313
|
+
var NotInteractiveError = class extends Error {
|
|
314
|
+
constructor(spec) {
|
|
315
|
+
super(spec.flag ? `Cannot prompt without a terminal. Pass ${spec.flag}.` : `Cannot prompt without a terminal: ${spec.message}`);
|
|
316
|
+
this.name = "NotInteractiveError";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/terminal/prompts.ts
|
|
321
|
+
const inquirerPrompts = {
|
|
322
|
+
text: (spec) => input({
|
|
323
|
+
message: spec.message,
|
|
324
|
+
required: spec.required,
|
|
325
|
+
default: spec.default,
|
|
326
|
+
validate: spec.validate
|
|
327
|
+
}),
|
|
328
|
+
secret: (spec) => password({
|
|
329
|
+
message: spec.message,
|
|
330
|
+
mask: spec.mask
|
|
331
|
+
}),
|
|
332
|
+
confirm: (spec) => confirm({
|
|
333
|
+
message: spec.message,
|
|
334
|
+
default: spec.default
|
|
335
|
+
}),
|
|
336
|
+
pick: (spec) => select({
|
|
337
|
+
message: spec.message,
|
|
338
|
+
choices: spec.choices,
|
|
339
|
+
default: spec.default,
|
|
340
|
+
pageSize: spec.pageSize
|
|
341
|
+
}),
|
|
342
|
+
pickMany: (spec) => checkbox({
|
|
343
|
+
message: spec.message,
|
|
344
|
+
choices: spec.choices,
|
|
345
|
+
required: spec.required,
|
|
346
|
+
pageSize: spec.pageSize
|
|
347
|
+
}),
|
|
348
|
+
edit: (spec) => editor({
|
|
349
|
+
message: spec.message,
|
|
350
|
+
postfix: spec.postfix,
|
|
351
|
+
default: spec.default,
|
|
352
|
+
validate: spec.validate
|
|
353
|
+
}),
|
|
354
|
+
searchPick: (spec) => search({
|
|
355
|
+
message: spec.message,
|
|
356
|
+
source: (term) => spec.source(term),
|
|
357
|
+
pageSize: spec.pageSize
|
|
358
|
+
})
|
|
359
|
+
};
|
|
360
|
+
async function refuse(spec) {
|
|
361
|
+
throw new NotInteractiveError(spec);
|
|
362
|
+
}
|
|
363
|
+
const refusingPrompts = {
|
|
364
|
+
text: refuse,
|
|
365
|
+
secret: refuse,
|
|
366
|
+
confirm: refuse,
|
|
367
|
+
pick: refuse,
|
|
368
|
+
pickMany: refuse,
|
|
369
|
+
edit: refuse,
|
|
370
|
+
searchPick: refuse
|
|
371
|
+
};
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/terminal/tty.ts
|
|
374
|
+
const DEFAULT_WIDTH = 80;
|
|
375
|
+
function ttyTerminal(interactive = true) {
|
|
376
|
+
return {
|
|
377
|
+
interactive,
|
|
378
|
+
out(value) {
|
|
379
|
+
if (Array.isArray(value) && value.length === 0) return;
|
|
380
|
+
console.log(Array.isArray(value) ? value.join("\n") : value);
|
|
381
|
+
},
|
|
382
|
+
err(value) {
|
|
383
|
+
console.error(value);
|
|
384
|
+
},
|
|
385
|
+
json(value) {
|
|
386
|
+
console.log(JSON.stringify(value, null, 2));
|
|
387
|
+
},
|
|
388
|
+
async page(text, options = {}) {
|
|
389
|
+
if (options.pager !== false && await tryPager(text)) return;
|
|
390
|
+
console.log(text);
|
|
391
|
+
},
|
|
392
|
+
get width() {
|
|
393
|
+
return process.stdout.columns ?? DEFAULT_WIDTH;
|
|
394
|
+
},
|
|
395
|
+
ask: interactive ? inquirerPrompts : refusingPrompts
|
|
396
|
+
};
|
|
217
397
|
}
|
|
218
|
-
|
|
219
|
-
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/terminal/open.ts
|
|
400
|
+
function openTerminal(args, stdin) {
|
|
401
|
+
return ttyTerminal(stdin.isTTY === true && !refusedInput(args.at(-1)));
|
|
220
402
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
403
|
+
function refusedInput(last) {
|
|
404
|
+
for (let command = asCommand(last); command; command = command.parent) if (command.opts()["input"] === false) return true;
|
|
405
|
+
return false;
|
|
223
406
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
407
|
+
function asCommand(value) {
|
|
408
|
+
const command = value;
|
|
409
|
+
return typeof command?.opts === "function" ? command : null;
|
|
226
410
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/cli/run.ts
|
|
413
|
+
const SIGINT_EXIT_CODE = 130;
|
|
414
|
+
function bareAction(fn) {
|
|
415
|
+
return guarded((env, args) => fn(env, ...args));
|
|
416
|
+
}
|
|
417
|
+
function jiraAction(fn) {
|
|
418
|
+
return guarded(async (env, args) => fn({
|
|
419
|
+
...env,
|
|
420
|
+
session: await openSession(env.profile)
|
|
421
|
+
}, ...args));
|
|
422
|
+
}
|
|
423
|
+
function bitbucketAction(fn) {
|
|
424
|
+
return guarded(async (env, args) => fn({
|
|
425
|
+
...env,
|
|
426
|
+
session: await openBitbucketSession(env.profile)
|
|
427
|
+
}, ...args));
|
|
428
|
+
}
|
|
429
|
+
function guarded(fn) {
|
|
430
|
+
return async (...args) => {
|
|
431
|
+
try {
|
|
432
|
+
await fn({
|
|
433
|
+
term: openTerminal(args, process.stdin),
|
|
434
|
+
files: diskFiles(),
|
|
435
|
+
profile: diskProfile()
|
|
436
|
+
}, args);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
fail(err);
|
|
439
|
+
}
|
|
236
440
|
};
|
|
237
441
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
email: config.email,
|
|
246
|
-
token,
|
|
247
|
-
workspace: config.bitbucket.workspace,
|
|
248
|
-
defaultRepo: config.bitbucket.defaultRepo,
|
|
249
|
-
uuid: config.bitbucket.uuid
|
|
250
|
-
};
|
|
442
|
+
function fail(err) {
|
|
443
|
+
if (isPromptInterrupt(err)) process.exit(SIGINT_EXIT_CODE);
|
|
444
|
+
ttyTerminal().err(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
function isPromptInterrupt(err) {
|
|
448
|
+
return err instanceof Error && err.name === "ExitPromptError";
|
|
251
449
|
}
|
|
252
450
|
//#endregion
|
|
253
|
-
//#region src/
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
required: true
|
|
258
|
-
}));
|
|
259
|
-
const email = await input({
|
|
260
|
-
message: "Account email:",
|
|
261
|
-
required: true
|
|
262
|
-
});
|
|
263
|
-
const token = await password({
|
|
264
|
-
message: "API token (from id.atlassian.com/manage-profile/security/api-tokens):",
|
|
265
|
-
mask: true
|
|
266
|
-
});
|
|
267
|
-
const me = await new AtlassianClient({
|
|
268
|
-
site,
|
|
269
|
-
email,
|
|
270
|
-
token
|
|
271
|
-
}).getJson("/rest/api/3/myself");
|
|
272
|
-
await writeConfig({
|
|
273
|
-
...await readConfig() ?? {},
|
|
274
|
-
site,
|
|
275
|
-
email
|
|
276
|
-
});
|
|
277
|
-
saveToken(email, token);
|
|
278
|
-
console.log(`Logged in as ${me.displayName} on ${site}.`);
|
|
451
|
+
//#region src/util/parse.ts
|
|
452
|
+
function parseIssueKey(input) {
|
|
453
|
+
const match = input.toUpperCase().match(/[A-Z][A-Z0-9]+-\d+/);
|
|
454
|
+
return match ? match[0] : null;
|
|
279
455
|
}
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
else await clearConfig();
|
|
289
|
-
console.log("Logged out. Credentials removed.");
|
|
456
|
+
function parsePageId(input) {
|
|
457
|
+
const trimmed = input.trim();
|
|
458
|
+
if (/^\d+$/.test(trimmed)) return trimmed;
|
|
459
|
+
const fromPath = trimmed.match(/\/pages\/(\d+)/);
|
|
460
|
+
if (fromPath) return fromPath[1];
|
|
461
|
+
const fromQuery = trimmed.match(/[?&]pageId=(\d+)/);
|
|
462
|
+
if (fromQuery) return fromQuery[1];
|
|
463
|
+
return null;
|
|
290
464
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
465
|
+
const PULL_REQUEST_HOSTS = ["bitbucket.org", "www.bitbucket.org"];
|
|
466
|
+
const PULL_REQUEST_PATH = /^\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)(?:\/|$)/;
|
|
467
|
+
function parsePullRequestRef(input) {
|
|
468
|
+
const trimmed = input.trim();
|
|
469
|
+
const bare = trimmed.replace(/^#/, "");
|
|
470
|
+
if (/^\d+$/.test(bare)) return { id: Number.parseInt(bare, 10) };
|
|
471
|
+
if (!/^https?:\/\//i.test(trimmed)) return null;
|
|
472
|
+
const url = parseUrl(trimmed);
|
|
473
|
+
if (!url || !PULL_REQUEST_HOSTS.includes(url.hostname.toLowerCase())) return null;
|
|
474
|
+
const match = PULL_REQUEST_PATH.exec(url.pathname);
|
|
475
|
+
if (!match) return null;
|
|
476
|
+
const [, workspace = "", repo = "", id = ""] = match;
|
|
477
|
+
return {
|
|
478
|
+
id: Number.parseInt(id, 10),
|
|
479
|
+
repo: {
|
|
480
|
+
workspace: decodeURIComponent(workspace),
|
|
481
|
+
repo: decodeURIComponent(repo)
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function parseUrl(value) {
|
|
486
|
+
try {
|
|
487
|
+
return new URL(value);
|
|
488
|
+
} catch {
|
|
489
|
+
return null;
|
|
296
490
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
491
|
+
}
|
|
492
|
+
function isExternalHref(href) {
|
|
493
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(href);
|
|
494
|
+
}
|
|
495
|
+
function resolveRepo(flag, config) {
|
|
496
|
+
if (flag) {
|
|
497
|
+
if (flag.includes("/")) {
|
|
498
|
+
const parts = flag.split("/");
|
|
499
|
+
const [workspace, repo] = parts;
|
|
500
|
+
if (parts.length !== 2 || !workspace || !repo) throw new Error(`Invalid --repo "${flag}". Expected "workspace/slug" or "slug".`);
|
|
501
|
+
return {
|
|
502
|
+
workspace,
|
|
503
|
+
repo
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (!config.workspace) throw new Error(`No Bitbucket workspace configured. Pass --repo workspace/slug or run \`atlass bitbucket login\`.`);
|
|
507
|
+
return {
|
|
508
|
+
workspace: config.workspace,
|
|
509
|
+
repo: flag
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (config.workspace && config.defaultRepo) return {
|
|
513
|
+
workspace: config.workspace,
|
|
514
|
+
repo: config.defaultRepo
|
|
515
|
+
};
|
|
516
|
+
throw new Error(`No repo given. Pass --repo workspace/slug (or a bare slug), or set a default repo at \`atlass bitbucket login\`.`);
|
|
517
|
+
}
|
|
518
|
+
const DEFAULT_LIMIT = 25;
|
|
519
|
+
const MAX_LIMIT = 100;
|
|
520
|
+
function parseLimit(value) {
|
|
521
|
+
if (!value) return DEFAULT_LIMIT;
|
|
522
|
+
const n = Number.parseInt(value, 10);
|
|
523
|
+
if (!Number.isFinite(n) || n < 1) throw new Error(`Invalid --limit "${value}".`);
|
|
524
|
+
return Math.min(n, MAX_LIMIT);
|
|
525
|
+
}
|
|
526
|
+
function siteOrigin(input) {
|
|
527
|
+
let value = input.trim();
|
|
528
|
+
if (!/^https?:\/\//i.test(value)) value = `https://${value}`;
|
|
529
|
+
return new URL(value).origin;
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/commands/auth.ts
|
|
533
|
+
async function login({ term, profile }) {
|
|
534
|
+
const site = siteOrigin(await term.ask.text({
|
|
535
|
+
message: "Atlassian site (e.g. acme.atlassian.net):",
|
|
536
|
+
required: true
|
|
537
|
+
}));
|
|
538
|
+
const email = await term.ask.text({
|
|
539
|
+
message: "Account email:",
|
|
540
|
+
required: true
|
|
541
|
+
});
|
|
542
|
+
const token = await term.ask.secret({
|
|
543
|
+
message: "API token (from id.atlassian.com/manage-profile/security/api-tokens):",
|
|
544
|
+
mask: true
|
|
545
|
+
});
|
|
546
|
+
const me = await sessionFor({
|
|
547
|
+
site,
|
|
548
|
+
email,
|
|
549
|
+
token
|
|
550
|
+
}).getJson("/rest/api/3/myself");
|
|
551
|
+
const existing = await profile.read() ?? {};
|
|
552
|
+
await profile.write({
|
|
553
|
+
...existing,
|
|
554
|
+
site,
|
|
555
|
+
email
|
|
556
|
+
});
|
|
557
|
+
await profile.setToken(email, "atlassian", token);
|
|
558
|
+
term.out(`Logged in as ${me.displayName} on ${site}.`);
|
|
559
|
+
}
|
|
560
|
+
async function logout({ term, profile }) {
|
|
561
|
+
const config = await profile.read();
|
|
562
|
+
if (config?.email) await profile.deleteToken(config.email, "atlassian");
|
|
563
|
+
const bitbucketLogin = config?.bitbucket && config.email ? {
|
|
564
|
+
email: config.email,
|
|
565
|
+
bitbucket: config.bitbucket
|
|
566
|
+
} : null;
|
|
567
|
+
if (bitbucketLogin) await profile.write(bitbucketLogin);
|
|
568
|
+
else await profile.clear();
|
|
569
|
+
term.out("Logged out. Credentials removed.");
|
|
570
|
+
}
|
|
571
|
+
async function status({ term, profile }) {
|
|
572
|
+
const config = await profile.read();
|
|
573
|
+
if (!config || !config.site || !config.email) {
|
|
574
|
+
term.out("Not logged in. Run `atlass auth login`.");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const hasToken = await profile.token(config.email, "atlassian") !== null;
|
|
578
|
+
term.out([
|
|
579
|
+
`Site: ${config.site}`,
|
|
580
|
+
`Email: ${config.email}`,
|
|
581
|
+
`Token: ${hasToken ? "stored in keyring" : "MISSING (run `atlass auth login`)"}`
|
|
582
|
+
]);
|
|
301
583
|
}
|
|
302
584
|
//#endregion
|
|
303
585
|
//#region src/cli/auth.ts
|
|
304
586
|
function registerAuth(auth) {
|
|
305
587
|
auth.description("Manage Atlassian credentials");
|
|
306
|
-
auth.command("login").description("Store site, email, and API token").action(
|
|
307
|
-
auth.command("logout").description("Remove stored credentials").action(
|
|
308
|
-
auth.command("status").description("Show the current login").action(
|
|
588
|
+
auth.command("login").description("Store site, email, and API token").action(bareAction(login));
|
|
589
|
+
auth.command("logout").description("Remove stored credentials").action(bareAction(logout));
|
|
590
|
+
auth.command("status").description("Show the current login").action(bareAction(status));
|
|
309
591
|
return auth;
|
|
310
592
|
}
|
|
311
593
|
//#endregion
|
|
594
|
+
//#region src/api/bitbucket-user.ts
|
|
595
|
+
async function fetchCurrentUserUuid(client) {
|
|
596
|
+
const user = await client.getJson("/2.0/user");
|
|
597
|
+
if (!user.uuid) throw new Error("Bitbucket did not return an account uuid for the current user.");
|
|
598
|
+
return user.uuid;
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/commands/bitbucket-auth.ts
|
|
602
|
+
async function bitbucketLogin({ term, profile }) {
|
|
603
|
+
const existing = await profile.read() ?? {};
|
|
604
|
+
const email = existing.email ?? await term.ask.text({
|
|
605
|
+
message: "Account email:",
|
|
606
|
+
required: true
|
|
607
|
+
});
|
|
608
|
+
const workspace = (await term.ask.text({
|
|
609
|
+
message: "Bitbucket workspace (e.g. acme):",
|
|
610
|
+
required: true
|
|
611
|
+
})).trim();
|
|
612
|
+
const defaultRepo = (await term.ask.text({ message: "Default repo slug (optional):" })).trim() || void 0;
|
|
613
|
+
const token = await term.ask.secret({
|
|
614
|
+
message: "Bitbucket API token (needs pipeline, pull request, account, and workspace read scopes):",
|
|
615
|
+
mask: true
|
|
616
|
+
});
|
|
617
|
+
const session = bitbucketSessionFor(email, token, { workspace });
|
|
618
|
+
const ws = await verifyWorkspace(session, workspace);
|
|
619
|
+
const uuid = await fetchCurrentUserUuid(session).catch(() => void 0);
|
|
620
|
+
await profile.write({
|
|
621
|
+
...existing,
|
|
622
|
+
email,
|
|
623
|
+
bitbucket: {
|
|
624
|
+
workspace,
|
|
625
|
+
...defaultRepo ? { defaultRepo } : {},
|
|
626
|
+
...uuid ? { uuid } : {}
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
await profile.setToken(email, "bitbucket", token);
|
|
630
|
+
term.out(`Logged in to Bitbucket workspace ${ws.name ?? workspace} as ${email}.`);
|
|
631
|
+
}
|
|
632
|
+
async function bitbucketLogout({ term, profile }) {
|
|
633
|
+
const config = await profile.read();
|
|
634
|
+
if (config?.email) await profile.deleteToken(config.email, "bitbucket");
|
|
635
|
+
const jiraLogin = config?.site && config.email ? {
|
|
636
|
+
site: config.site,
|
|
637
|
+
email: config.email
|
|
638
|
+
} : null;
|
|
639
|
+
if (jiraLogin) await profile.write(jiraLogin);
|
|
640
|
+
else await profile.clear();
|
|
641
|
+
term.out("Logged out of Bitbucket. Credentials removed.");
|
|
642
|
+
}
|
|
643
|
+
async function bitbucketStatus({ term, profile }) {
|
|
644
|
+
const config = await profile.read();
|
|
645
|
+
if (!config?.email || !config.bitbucket?.workspace) {
|
|
646
|
+
term.out("Not logged in to Bitbucket. Run `atlass bitbucket login`.");
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const hasToken = await profile.token(config.email, "bitbucket") !== null;
|
|
650
|
+
term.out([
|
|
651
|
+
`Workspace: ${config.bitbucket.workspace}`,
|
|
652
|
+
`Email: ${config.email}`,
|
|
653
|
+
`Default repo: ${config.bitbucket.defaultRepo ?? "(none)"}`,
|
|
654
|
+
`Token: ${hasToken ? "stored in keyring" : "MISSING (run `atlass bitbucket login`)"}`
|
|
655
|
+
]);
|
|
656
|
+
}
|
|
657
|
+
async function rememberBitbucketUuid(profile, uuid) {
|
|
658
|
+
const config = await profile.read();
|
|
659
|
+
if (!config?.bitbucket) return;
|
|
660
|
+
await profile.write({
|
|
661
|
+
...config,
|
|
662
|
+
bitbucket: {
|
|
663
|
+
...config.bitbucket,
|
|
664
|
+
uuid
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
async function verifyWorkspace(transport, workspace) {
|
|
669
|
+
try {
|
|
670
|
+
return await transport.getJson(`/2.0/workspaces/${encodeURIComponent(workspace)}`);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) throw new Error(`Could not verify Bitbucket workspace "${workspace}" (401/403). Check the token and that it has workspace read + read:pipeline:bitbucket scopes.`);
|
|
673
|
+
if (err instanceof HttpError && err.status === 404) throw new Error(`Bitbucket workspace "${workspace}" not found (404). Check the slug.`);
|
|
674
|
+
throw err;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
312
678
|
//#region src/api/bitbucket-repo.ts
|
|
313
679
|
const BITBUCKET_WEB_ORIGIN = "https://bitbucket.org";
|
|
314
680
|
function repoPath(ref, resource) {
|
|
@@ -322,13 +688,34 @@ async function* walkPages(client, firstPath) {
|
|
|
322
688
|
path = page.next ? pathAndQuery(page.next) : null;
|
|
323
689
|
}
|
|
324
690
|
}
|
|
325
|
-
async function
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
691
|
+
async function collectCapped(client, first, cap, map) {
|
|
692
|
+
const items = [];
|
|
693
|
+
let total;
|
|
694
|
+
let path = first;
|
|
695
|
+
while (path) {
|
|
696
|
+
const page = await client.getJson(path);
|
|
697
|
+
total ??= page.size;
|
|
698
|
+
for (const value of page.values ?? []) {
|
|
699
|
+
items.push(map(value));
|
|
700
|
+
if (items.length >= cap) return {
|
|
701
|
+
items,
|
|
702
|
+
total,
|
|
703
|
+
truncated: hasMore(page, total, cap)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
path = page.next ? pathAndQuery(page.next) : null;
|
|
330
707
|
}
|
|
331
|
-
return
|
|
708
|
+
return {
|
|
709
|
+
items,
|
|
710
|
+
total: total ?? items.length,
|
|
711
|
+
truncated: false
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function hasMore(page, total, cap) {
|
|
715
|
+
return total === void 0 ? Boolean(page.next) : total > cap;
|
|
716
|
+
}
|
|
717
|
+
async function collectPages(client, first, limit, map) {
|
|
718
|
+
return (await collectCapped(client, first, limit, map)).items;
|
|
332
719
|
}
|
|
333
720
|
//#endregion
|
|
334
721
|
//#region src/api/bitbucket-pull-requests.ts
|
|
@@ -402,100 +789,115 @@ function toPullRequestSummary(ref, pr) {
|
|
|
402
789
|
async function listPullRequests(client, ref, params) {
|
|
403
790
|
return collectPages(client, `${repoPath(ref, "pullrequests")}?${pullRequestsQuery(params)}`, params.limit, (value) => toPullRequestSummary(ref, value));
|
|
404
791
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
return user.uuid;
|
|
792
|
+
const PAGELEN = 100;
|
|
793
|
+
const RESOLUTION_FIELDS = "fields=%2Bvalues.resolution.*";
|
|
794
|
+
function pullRequestPath(ref, id, resource) {
|
|
795
|
+
const base = `${repoPath(ref, "pullrequests")}/${id}`;
|
|
796
|
+
return resource ? `${base}/${resource}` : base;
|
|
411
797
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
798
|
+
function toPullRequestDetail(ref, value) {
|
|
799
|
+
return {
|
|
800
|
+
id: value.id,
|
|
801
|
+
title: value.title ?? "",
|
|
802
|
+
description: value.description ?? "",
|
|
803
|
+
state: value.state ?? "",
|
|
804
|
+
draft: value.draft ?? false,
|
|
805
|
+
author: value.author?.display_name ?? "",
|
|
806
|
+
sourceBranch: value.source?.branch?.name ?? "",
|
|
807
|
+
destinationBranch: value.destination?.branch?.name ?? "",
|
|
808
|
+
closedBy: value.closed_by?.display_name ?? "",
|
|
809
|
+
createdOn: value.created_on ?? "",
|
|
810
|
+
updatedOn: value.updated_on ?? "",
|
|
811
|
+
url: pullRequestUrl(ref, value.id),
|
|
812
|
+
reviewers: toReviewers(value),
|
|
813
|
+
participants: toParticipants(value)
|
|
814
|
+
};
|
|
419
815
|
}
|
|
420
|
-
function
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
816
|
+
function toParticipants(value) {
|
|
817
|
+
const rows = /* @__PURE__ */ new Map();
|
|
818
|
+
const users = [value.author, ...(value.participants ?? []).map((entry) => entry.user)];
|
|
819
|
+
for (const user of users) {
|
|
820
|
+
if (!user?.account_id || !user.display_name) continue;
|
|
821
|
+
rows.set(user.account_id, {
|
|
822
|
+
accountId: user.account_id,
|
|
823
|
+
name: user.display_name
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
return [...rows.values()];
|
|
425
827
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
828
|
+
function toReviewers(value) {
|
|
829
|
+
const rows = /* @__PURE__ */ new Map();
|
|
830
|
+
for (const requested of value.reviewers ?? []) rows.set(principalKey(requested), {
|
|
831
|
+
name: requested.display_name ?? "",
|
|
832
|
+
state: ""
|
|
833
|
+
});
|
|
834
|
+
for (const participant of value.participants ?? []) {
|
|
835
|
+
const state = participantState(participant);
|
|
836
|
+
if (participant.role !== "REVIEWER" && !state) continue;
|
|
837
|
+
rows.set(principalKey(participant.user ?? {}), {
|
|
838
|
+
name: participant.user?.display_name ?? "",
|
|
839
|
+
state
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return [...rows.values()];
|
|
432
843
|
}
|
|
433
|
-
function
|
|
434
|
-
|
|
844
|
+
function participantState(participant) {
|
|
845
|
+
if (participant.state) return participant.state.toUpperCase();
|
|
846
|
+
return participant.approved ? "APPROVED" : "";
|
|
435
847
|
}
|
|
436
|
-
function
|
|
437
|
-
return
|
|
848
|
+
function principalKey(user) {
|
|
849
|
+
return user.uuid ?? user.display_name ?? "";
|
|
438
850
|
}
|
|
439
|
-
function
|
|
851
|
+
function toComment(value) {
|
|
852
|
+
if (value.deleted) return null;
|
|
853
|
+
const body = value.content?.raw ?? "";
|
|
854
|
+
if (!body.trim()) return null;
|
|
440
855
|
return {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
uuid: p.uuid,
|
|
449
|
-
url: pipelineUrl(ref, p.build_number)
|
|
856
|
+
id: value.id,
|
|
857
|
+
parentId: value.parent?.id ?? null,
|
|
858
|
+
author: value.user?.display_name ?? "",
|
|
859
|
+
created: value.created_on ?? "",
|
|
860
|
+
body,
|
|
861
|
+
anchor: inlineAnchor(value),
|
|
862
|
+
resolved: toResolution(value.resolution)
|
|
450
863
|
};
|
|
451
864
|
}
|
|
452
|
-
function
|
|
865
|
+
function toResolution(value) {
|
|
866
|
+
if (!value?.created_on) return null;
|
|
453
867
|
return {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
trigger: p.trigger?.name ?? ""
|
|
868
|
+
by: value.user?.display_name ?? "",
|
|
869
|
+
at: value.created_on
|
|
457
870
|
};
|
|
458
871
|
}
|
|
459
|
-
function
|
|
872
|
+
function inlineAnchor(value) {
|
|
873
|
+
const path = value.inline?.path;
|
|
874
|
+
if (!path) return "";
|
|
875
|
+
const line = value.inline?.to ?? value.inline?.from;
|
|
876
|
+
return line == null ? path : `${path}:${line}`;
|
|
877
|
+
}
|
|
878
|
+
function toChangedFile(value) {
|
|
879
|
+
const before = value.old?.path ?? "";
|
|
880
|
+
const after = value.new?.path ?? "";
|
|
460
881
|
return {
|
|
461
|
-
|
|
462
|
-
status:
|
|
463
|
-
|
|
882
|
+
path: before && after && before !== after ? `${before} -> ${after}` : after || before,
|
|
883
|
+
status: value.status ?? "",
|
|
884
|
+
added: value.lines_added ?? 0,
|
|
885
|
+
removed: value.lines_removed ?? 0
|
|
464
886
|
};
|
|
465
887
|
}
|
|
466
|
-
function
|
|
467
|
-
return
|
|
468
|
-
}
|
|
469
|
-
async function listPipelines(client, ref, limit) {
|
|
470
|
-
return collectPages(client, `${pipelinesPath(ref)}?${pipelinesQuery(limit)}`, limit, (value) => toPipelineSummary(ref, value));
|
|
471
|
-
}
|
|
472
|
-
async function getPipeline(client, ref, buildNumber) {
|
|
473
|
-
const found = await fetchByBuildNumber(client, ref, buildNumber) ?? await findInRecentRuns(client, ref, buildNumber);
|
|
474
|
-
if (!found) throw new Error(`Could not find pipeline #${buildNumber} in the ${BUILD_NUMBER_SCAN_LIMIT} most recent runs. It may be too old.`);
|
|
475
|
-
return toPipelineDetail(ref, found);
|
|
476
|
-
}
|
|
477
|
-
async function fetchByBuildNumber(client, ref, buildNumber) {
|
|
478
|
-
try {
|
|
479
|
-
return await client.getJson(`${pipelinesPath(ref)}/${encodeURIComponent(String(buildNumber))}`);
|
|
480
|
-
} catch (err) {
|
|
481
|
-
if (err instanceof HttpError && (err.status === 400 || err.status === 404)) return null;
|
|
482
|
-
throw err;
|
|
483
|
-
}
|
|
888
|
+
async function getPullRequest(client, ref, id) {
|
|
889
|
+
return toPullRequestDetail(ref, await client.getJson(pullRequestPath(ref, id)));
|
|
484
890
|
}
|
|
485
|
-
async function
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
return null;
|
|
891
|
+
async function listComments(client, ref, id) {
|
|
892
|
+
const page = await collectCapped(client, `${pullRequestPath(ref, id, "comments")}?pagelen=${PAGELEN}&${RESOLUTION_FIELDS}`, 200, toComment);
|
|
893
|
+
const items = page.items.filter((comment) => comment !== null).sort((a, b) => a.created.localeCompare(b.created));
|
|
894
|
+
return {
|
|
895
|
+
...page,
|
|
896
|
+
items
|
|
897
|
+
};
|
|
493
898
|
}
|
|
494
|
-
async function
|
|
495
|
-
|
|
496
|
-
const first = `${pipelinesPath(ref)}/${encodeURIComponent(pipelineId)}/steps`;
|
|
497
|
-
for await (const step of walkPages(client, first)) out.push(toStepSummary(step));
|
|
498
|
-
return out;
|
|
899
|
+
async function listChangedFiles(client, ref, id) {
|
|
900
|
+
return collectCapped(client, `${pullRequestPath(ref, id, "diffstat")}?pagelen=${PAGELEN}`, 50, toChangedFile);
|
|
499
901
|
}
|
|
500
902
|
//#endregion
|
|
501
903
|
//#region src/util/format.ts
|
|
@@ -507,13 +909,19 @@ function formatDuration(seconds) {
|
|
|
507
909
|
if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`;
|
|
508
910
|
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
509
911
|
}
|
|
912
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
913
|
+
function formatDate$1(iso) {
|
|
914
|
+
const date = new Date(iso);
|
|
915
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
916
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
917
|
+
}
|
|
510
918
|
function formatDateTime(iso) {
|
|
511
919
|
const date = new Date(iso);
|
|
512
920
|
if (Number.isNaN(date.getTime())) return iso;
|
|
513
|
-
return date.
|
|
921
|
+
return `${formatDate$1(iso)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
514
922
|
}
|
|
515
923
|
const MINUTE = 60;
|
|
516
|
-
const HOUR =
|
|
924
|
+
const HOUR = 3600;
|
|
517
925
|
const DAY = 24 * HOUR;
|
|
518
926
|
function relativeTime(iso, nowMs) {
|
|
519
927
|
if (!iso) return "-";
|
|
@@ -529,254 +937,72 @@ function relativeTime(iso, nowMs) {
|
|
|
529
937
|
return `${Math.floor(days / 365)}y ago`;
|
|
530
938
|
}
|
|
531
939
|
//#endregion
|
|
532
|
-
//#region src/
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
940
|
+
//#region src/commands/bitbucket-comments.ts
|
|
941
|
+
const MENTION = /@\{([^}]+)\}/g;
|
|
942
|
+
function renderedPrComments(comments, participants) {
|
|
943
|
+
const names = new Map(participants.map((person) => [person.accountId, person.name]));
|
|
944
|
+
return threadsOf(comments).map((thread) => renderThread(thread, names));
|
|
945
|
+
}
|
|
946
|
+
function threadsOf(comments) {
|
|
947
|
+
const byId = new Map(comments.map((comment) => [comment.id, comment]));
|
|
948
|
+
const threads = /* @__PURE__ */ new Map();
|
|
949
|
+
for (const comment of comments) {
|
|
950
|
+
const root = rootOf(comment, byId);
|
|
951
|
+
const thread = threads.get(root.id);
|
|
952
|
+
if (thread) thread.push(comment);
|
|
953
|
+
else threads.set(root.id, [comment]);
|
|
954
|
+
}
|
|
955
|
+
return [...threads.values()];
|
|
956
|
+
}
|
|
957
|
+
function rootOf(comment, byId) {
|
|
958
|
+
const seen = /* @__PURE__ */ new Set([comment.id]);
|
|
959
|
+
let current = comment;
|
|
960
|
+
while (current.parentId !== null) {
|
|
961
|
+
const parent = byId.get(current.parentId);
|
|
962
|
+
if (!parent || seen.has(parent.id)) break;
|
|
963
|
+
seen.add(parent.id);
|
|
964
|
+
current = parent;
|
|
965
|
+
}
|
|
966
|
+
return current;
|
|
967
|
+
}
|
|
968
|
+
function renderThread(thread, names) {
|
|
969
|
+
const rows = thread.map((comment, index) => ({
|
|
970
|
+
author: comment.author,
|
|
971
|
+
created: comment.created,
|
|
972
|
+
markdown: resolveMentions(comment.body, names),
|
|
973
|
+
...index === 0 ? { anchor: comment.anchor } : {}
|
|
560
974
|
}));
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
printRows(rows, options);
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
if (options.out?.endsWith(".md")) throw new Error("--out must be a directory when using --copy; a .md file path would overwrite each selection.");
|
|
581
|
-
await copySelected(rows, noun, copyOne);
|
|
582
|
-
}
|
|
583
|
-
function searchFooter(limit) {
|
|
584
|
-
return `
|
|
585
|
-
Showing first ${limit}; refine with flags or raise --limit.`;
|
|
586
|
-
}
|
|
587
|
-
async function copySelected(rows, noun, copyOne) {
|
|
588
|
-
if (!process.stdin.isTTY) throw new Error("--copy requires an interactive terminal.");
|
|
589
|
-
const selected = await checkbox({
|
|
590
|
-
message: `Select ${noun.plural} to copy:`,
|
|
591
|
-
choices: rows.map((r) => ({
|
|
592
|
-
name: formatRow(r),
|
|
593
|
-
value: r.id
|
|
594
|
-
})),
|
|
595
|
-
pageSize: 20
|
|
596
|
-
});
|
|
597
|
-
if (selected.length === 0) {
|
|
598
|
-
console.log("Nothing selected.");
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
let copied = 0;
|
|
602
|
-
const failures = [];
|
|
603
|
-
const queue = [...selected];
|
|
604
|
-
async function worker() {
|
|
605
|
-
for (let id = queue.shift(); id !== void 0; id = queue.shift()) try {
|
|
606
|
-
await copyOne(id);
|
|
607
|
-
copied++;
|
|
608
|
-
} catch (err) {
|
|
609
|
-
failures.push(`${id} (${err instanceof Error ? err.message : String(err)})`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
await Promise.all(Array.from({ length: Math.min(COPY_CONCURRENCY, selected.length) }, worker));
|
|
613
|
-
const summary = `Copied ${copied} ${copied === 1 ? noun.singular : noun.plural}`;
|
|
614
|
-
if (failures.length === 0) console.log(summary);
|
|
615
|
-
else console.log(`${summary}, failed ${failures.length}: ${failures.join(", ")}`);
|
|
616
|
-
}
|
|
617
|
-
function formatRow(row, width = process.stdout.columns ?? 80) {
|
|
618
|
-
const room = width - stripVTControlCharacters(row.fixedColumns).length - 2;
|
|
619
|
-
const text = room > 0 ? truncate(row.freeText, room) : "";
|
|
620
|
-
const columns = hyperlink(row.id, row.url) + row.fixedColumns.slice(row.id.length);
|
|
621
|
-
return text ? `${columns} ${hyperlink(text, row.url)}` : columns;
|
|
622
|
-
}
|
|
623
|
-
function truncate(text, max) {
|
|
624
|
-
const clean = text.replace(/\s+/g, " ").trim();
|
|
625
|
-
return clean.length <= max ? clean : `${clean.slice(0, Math.max(0, max - 3))}...`;
|
|
626
|
-
}
|
|
627
|
-
//#endregion
|
|
628
|
-
//#region src/util/parse.ts
|
|
629
|
-
function parseIssueKey(input) {
|
|
630
|
-
const match = input.toUpperCase().match(/[A-Z][A-Z0-9]+-\d+/);
|
|
631
|
-
return match ? match[0] : null;
|
|
632
|
-
}
|
|
633
|
-
function parsePageId(input) {
|
|
634
|
-
const trimmed = input.trim();
|
|
635
|
-
if (/^\d+$/.test(trimmed)) return trimmed;
|
|
636
|
-
const fromPath = trimmed.match(/\/pages\/(\d+)/);
|
|
637
|
-
if (fromPath) return fromPath[1];
|
|
638
|
-
const fromQuery = trimmed.match(/[?&]pageId=(\d+)/);
|
|
639
|
-
if (fromQuery) return fromQuery[1];
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
function isExternalHref(href) {
|
|
643
|
-
return /^[a-z][a-z0-9+.-]*:\/\//i.test(href);
|
|
644
|
-
}
|
|
645
|
-
function resolveRepo(flag, config) {
|
|
646
|
-
if (flag) {
|
|
647
|
-
if (flag.includes("/")) {
|
|
648
|
-
const parts = flag.split("/");
|
|
649
|
-
const [workspace, repo] = parts;
|
|
650
|
-
if (parts.length !== 2 || !workspace || !repo) throw new Error(`Invalid --repo "${flag}". Expected "workspace/slug" or "slug".`);
|
|
651
|
-
return {
|
|
652
|
-
workspace,
|
|
653
|
-
repo
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
if (!config.workspace) throw new Error(`No Bitbucket workspace configured. Pass --repo workspace/slug or run \`atlass bitbucket login\`.`);
|
|
657
|
-
return {
|
|
658
|
-
workspace: config.workspace,
|
|
659
|
-
repo: flag
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
if (config.workspace && config.defaultRepo) return {
|
|
663
|
-
workspace: config.workspace,
|
|
664
|
-
repo: config.defaultRepo
|
|
665
|
-
};
|
|
666
|
-
throw new Error(`No repo given. Pass --repo workspace/slug (or a bare slug), or set a default repo at \`atlass bitbucket login\`.`);
|
|
667
|
-
}
|
|
668
|
-
const DEFAULT_LIMIT = 25;
|
|
669
|
-
const MAX_LIMIT = 100;
|
|
670
|
-
function parseLimit(value) {
|
|
671
|
-
if (!value) return DEFAULT_LIMIT;
|
|
672
|
-
const n = Number.parseInt(value, 10);
|
|
673
|
-
if (!Number.isFinite(n) || n < 1) throw new Error(`Invalid --limit "${value}".`);
|
|
674
|
-
return Math.min(n, MAX_LIMIT);
|
|
975
|
+
const trailer = resolvedTrailer(thread);
|
|
976
|
+
const last = rows.at(-1);
|
|
977
|
+
if (trailer && last) last.trailer = trailer;
|
|
978
|
+
return rows;
|
|
979
|
+
}
|
|
980
|
+
function resolvedTrailer(thread) {
|
|
981
|
+
const resolved = thread.find((comment) => comment.resolved)?.resolved;
|
|
982
|
+
if (!resolved) return "";
|
|
983
|
+
const by = resolved.by ? ` by ${resolved.by}` : "";
|
|
984
|
+
return kleur.green(` ↳ resolved${by} · ${formatDateTime(resolved.at)}`);
|
|
985
|
+
}
|
|
986
|
+
function resolveMentions(body, names) {
|
|
987
|
+
return body.replace(MENTION, (raw, id) => {
|
|
988
|
+
const name = names.get(id);
|
|
989
|
+
return name ? `@${name}` : raw;
|
|
990
|
+
});
|
|
675
991
|
}
|
|
676
992
|
//#endregion
|
|
677
|
-
//#region src/commands/bitbucket.ts
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
required: true
|
|
683
|
-
});
|
|
684
|
-
const workspace = (await input({
|
|
685
|
-
message: "Bitbucket workspace (e.g. acme):",
|
|
686
|
-
required: true
|
|
687
|
-
})).trim();
|
|
688
|
-
const defaultRepo = (await input({ message: "Default repo slug (optional):" })).trim() || void 0;
|
|
689
|
-
const token = await password({
|
|
690
|
-
message: "Bitbucket API token (needs pipeline, pull request, account, and workspace read scopes):",
|
|
691
|
-
mask: true
|
|
692
|
-
});
|
|
693
|
-
const client = new AtlassianClient({
|
|
694
|
-
site: BITBUCKET_ORIGIN,
|
|
695
|
-
email,
|
|
696
|
-
token
|
|
697
|
-
});
|
|
698
|
-
const ws = await verifyWorkspace(client, workspace);
|
|
699
|
-
const uuid = await fetchCurrentUserUuid(client).catch(() => void 0);
|
|
700
|
-
await writeConfig({
|
|
701
|
-
...existing,
|
|
702
|
-
email,
|
|
703
|
-
bitbucket: {
|
|
704
|
-
workspace,
|
|
705
|
-
...defaultRepo ? { defaultRepo } : {},
|
|
706
|
-
...uuid ? { uuid } : {}
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
saveBitbucketToken(email, token);
|
|
710
|
-
console.log(`Logged in to Bitbucket workspace ${ws.name ?? workspace} as ${email}.`);
|
|
711
|
-
}
|
|
712
|
-
async function rememberBitbucketUuid(uuid) {
|
|
713
|
-
const config = await readConfig();
|
|
714
|
-
if (!config?.bitbucket) return;
|
|
715
|
-
await writeConfig({
|
|
716
|
-
...config,
|
|
717
|
-
bitbucket: {
|
|
718
|
-
...config.bitbucket,
|
|
719
|
-
uuid
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
async function bitbucketLogout() {
|
|
724
|
-
const config = await readConfig();
|
|
725
|
-
if (config?.email) deleteBitbucketToken(config.email);
|
|
726
|
-
const jiraLogin = config?.site && config.email ? {
|
|
727
|
-
site: config.site,
|
|
728
|
-
email: config.email
|
|
729
|
-
} : null;
|
|
730
|
-
if (jiraLogin) await writeConfig(jiraLogin);
|
|
731
|
-
else await clearConfig();
|
|
732
|
-
console.log("Logged out of Bitbucket. Credentials removed.");
|
|
733
|
-
}
|
|
734
|
-
async function bitbucketStatus() {
|
|
735
|
-
const config = await readConfig();
|
|
736
|
-
if (!config?.email || !config.bitbucket?.workspace) {
|
|
737
|
-
console.log("Not logged in to Bitbucket. Run `atlass bitbucket login`.");
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
const hasToken = readBitbucketToken(config.email) !== null;
|
|
741
|
-
console.log(`Workspace: ${config.bitbucket.workspace}`);
|
|
742
|
-
console.log(`Email: ${config.email}`);
|
|
743
|
-
console.log(`Default repo: ${config.bitbucket.defaultRepo ?? "(none)"}`);
|
|
744
|
-
console.log(`Token: ${hasToken ? "stored in keyring" : "MISSING (run `atlass bitbucket login`)"}`);
|
|
745
|
-
}
|
|
746
|
-
async function bitbucketPipelines(options) {
|
|
747
|
-
const auth = await requireBitbucketAuth();
|
|
748
|
-
const ref = resolveRepo(options.repo, auth);
|
|
749
|
-
const client = new AtlassianClient(auth);
|
|
750
|
-
const limit = parseLimit(options.limit);
|
|
751
|
-
printRows(pipelineRows(await withScopeHint(PIPELINE_SCOPE, () => listPipelines(client, ref, limit)), Date.now()), {
|
|
752
|
-
json: options.json,
|
|
753
|
-
empty: "No pipelines found."
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
async function bitbucketPipeline(arg, options) {
|
|
757
|
-
const buildNumber = parseBuildNumber(arg);
|
|
758
|
-
const auth = await requireBitbucketAuth();
|
|
759
|
-
const ref = resolveRepo(options.repo, auth);
|
|
760
|
-
const client = new AtlassianClient(auth);
|
|
761
|
-
const detail = await withScopeHint(PIPELINE_SCOPE, () => getPipeline(client, ref, buildNumber));
|
|
762
|
-
printPipelineDetail(detail, await withScopeHint(PIPELINE_SCOPE, () => listSteps(client, ref, detail.uuid)), Date.now());
|
|
763
|
-
}
|
|
764
|
-
function pipelineRows(pipelines, nowMs) {
|
|
765
|
-
return alignedRows(pipelines, nowMs, (p) => ({
|
|
766
|
-
id: `#${p.buildNumber}`,
|
|
767
|
-
url: p.url,
|
|
768
|
-
label: p.status || "-",
|
|
769
|
-
color: colorForBitbucketState(p.status),
|
|
770
|
-
text: `${refWithCommit(p)} · ${formatDuration(p.durationSeconds)}`,
|
|
771
|
-
timestamp: p.createdOn
|
|
772
|
-
}));
|
|
773
|
-
}
|
|
993
|
+
//#region src/commands/bitbucket-shared.ts
|
|
994
|
+
const PIPELINE_SCOPE = "read:pipeline:bitbucket";
|
|
995
|
+
const PULL_REQUEST_SCOPE = "read:pullrequest:bitbucket";
|
|
996
|
+
const REPOSITORY_SCOPE = "read:repository:bitbucket";
|
|
997
|
+
const ACCOUNT_SCOPE = "read:account";
|
|
774
998
|
const STATE_COLORS = {
|
|
775
999
|
SUCCESSFUL: kleur.green,
|
|
776
1000
|
MERGED: kleur.green,
|
|
1001
|
+
APPROVED: kleur.green,
|
|
777
1002
|
FAILED: kleur.red,
|
|
778
1003
|
ERROR: kleur.red,
|
|
779
1004
|
DECLINED: kleur.red,
|
|
1005
|
+
CHANGES_REQUESTED: kleur.red,
|
|
780
1006
|
IN_PROGRESS: kleur.yellow,
|
|
781
1007
|
PENDING: kleur.yellow,
|
|
782
1008
|
OPEN: kleur.yellow,
|
|
@@ -788,875 +1014,1218 @@ const STATE_COLORS = {
|
|
|
788
1014
|
function colorForBitbucketState(state) {
|
|
789
1015
|
return STATE_COLORS[state.toUpperCase()] ?? kleur.white;
|
|
790
1016
|
}
|
|
791
|
-
function formatStepRows(steps) {
|
|
792
|
-
const rows = steps.map((s) => ({
|
|
793
|
-
name: s.name || "-",
|
|
794
|
-
status: s.status || "-",
|
|
795
|
-
dur: formatDuration(s.durationSeconds)
|
|
796
|
-
}));
|
|
797
|
-
const wn = Math.max(...rows.map((r) => r.name.length));
|
|
798
|
-
const ws = Math.max(...rows.map((r) => r.status.length));
|
|
799
|
-
return rows.map((r) => ` ${r.name.padEnd(wn)} ${r.status.padEnd(ws)} ${r.dur}`);
|
|
800
|
-
}
|
|
801
|
-
function printPipelineDetail(detail, steps, nowMs) {
|
|
802
|
-
console.log(`Pipeline #${detail.buildNumber} ${detail.status || "-"}`);
|
|
803
|
-
if (detail.repo) console.log(`Repo: ${detail.repo}`);
|
|
804
|
-
console.log(`Ref: ${refWithCommit(detail)}`);
|
|
805
|
-
if (detail.trigger) console.log(`Trigger: ${detail.trigger}`);
|
|
806
|
-
console.log(`Duration: ${formatDuration(detail.durationSeconds)}`);
|
|
807
|
-
const by = detail.creator ? ` by ${detail.creator}` : "";
|
|
808
|
-
console.log(`Created: ${relativeTime(detail.createdOn, nowMs)}${by}`);
|
|
809
|
-
if (steps.length > 0) {
|
|
810
|
-
console.log("");
|
|
811
|
-
console.log("Steps:");
|
|
812
|
-
for (const line of formatStepRows(steps)) console.log(line);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
function refWithCommit(pipeline) {
|
|
816
|
-
if (!pipeline.ref) return pipeline.commit || "-";
|
|
817
|
-
return pipeline.commit ? `${pipeline.ref} (${pipeline.commit})` : pipeline.ref;
|
|
818
|
-
}
|
|
819
|
-
async function verifyWorkspace(client, workspace) {
|
|
820
|
-
try {
|
|
821
|
-
return await client.getJson(`/2.0/workspaces/${encodeURIComponent(workspace)}`);
|
|
822
|
-
} catch (err) {
|
|
823
|
-
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) throw new Error(`Could not verify Bitbucket workspace "${workspace}" (401/403). Check the token and that it has workspace read + read:pipeline:bitbucket scopes.`);
|
|
824
|
-
if (err instanceof HttpError && err.status === 404) throw new Error(`Bitbucket workspace "${workspace}" not found (404). Check the slug.`);
|
|
825
|
-
throw err;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
function parseBuildNumber(arg) {
|
|
829
|
-
const raw = (arg ?? "").replace(/^#/, "").trim();
|
|
830
|
-
if (!/^\d+$/.test(raw)) throw new Error(`Invalid pipeline number "${arg ?? ""}". Expected a build number, e.g. 123.`);
|
|
831
|
-
return Number.parseInt(raw, 10);
|
|
832
|
-
}
|
|
833
|
-
const PIPELINE_SCOPE = "read:pipeline:bitbucket";
|
|
834
1017
|
async function withScopeHint(scope, fn) {
|
|
835
1018
|
try {
|
|
836
1019
|
return await fn();
|
|
837
1020
|
} catch (err) {
|
|
838
|
-
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) throw new Error(`Bitbucket rejected the request (401/403). Check the token has the ${scope} scope, or run \`atlass bitbucket login\` to update it.`);
|
|
1021
|
+
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) throw new Error(`Bitbucket rejected ${err.path || "the request"} (401/403). Check the token has the ${scope} scope, or run \`atlass bitbucket login\` to update it.`);
|
|
839
1022
|
throw err;
|
|
840
1023
|
}
|
|
841
1024
|
}
|
|
842
1025
|
//#endregion
|
|
843
|
-
//#region src/
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const ref = resolveRepo(options.repo, auth);
|
|
851
|
-
const client = new AtlassianClient(auth);
|
|
852
|
-
const limit = parseLimit(options.limit);
|
|
853
|
-
const query = pullRequestQuery({
|
|
854
|
-
...await resolvePrincipals(client, auth, options),
|
|
855
|
-
query: options.query
|
|
856
|
-
});
|
|
857
|
-
const prs = await withScopeHint(PULL_REQUEST_SCOPE, () => listPullRequests(client, ref, {
|
|
858
|
-
limit,
|
|
859
|
-
states,
|
|
860
|
-
query
|
|
861
|
-
}));
|
|
862
|
-
printRows(pullRequestRows(prs, Date.now()), {
|
|
863
|
-
json: options.json,
|
|
864
|
-
empty: emptyMessage(options),
|
|
865
|
-
footer: prs.length === limit ? searchFooter(limit) : void 0
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
function pullRequestStates(options) {
|
|
869
|
-
if (!options.all) return parsePullRequestStates(options.state);
|
|
870
|
-
if (options.state) throw new Error("--all cannot be combined with --state.");
|
|
871
|
-
return allPullRequestStates();
|
|
872
|
-
}
|
|
873
|
-
function pullRequestRows(prs, nowMs) {
|
|
874
|
-
return alignedRows(prs, nowMs, (pr) => {
|
|
875
|
-
const label = pr.draft ? "DRAFT" : pr.state || "-";
|
|
876
|
-
return {
|
|
877
|
-
id: `#${pr.id}`,
|
|
878
|
-
url: pr.url,
|
|
879
|
-
label,
|
|
880
|
-
color: colorForBitbucketState(label),
|
|
881
|
-
text: pr.title,
|
|
882
|
-
timestamp: pr.updatedOn
|
|
883
|
-
};
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
async function resolvePrincipals(client, auth, options) {
|
|
887
|
-
const mine = isMe(options.author) || isMe(options.reviewer) ? await currentUuid(client, auth) : void 0;
|
|
888
|
-
return {
|
|
889
|
-
author: isMe(options.author) ? mine : options.author,
|
|
890
|
-
reviewer: isMe(options.reviewer) ? mine : options.reviewer
|
|
891
|
-
};
|
|
1026
|
+
//#region src/adf/to-markdown-inline.ts
|
|
1027
|
+
function renderMedia(node, ctx) {
|
|
1028
|
+
const attrs = node.attrs ?? {};
|
|
1029
|
+
const alt = attrs.alt ?? "";
|
|
1030
|
+
const resolved = ctx.resolveMedia?.(attrs);
|
|
1031
|
+
if (resolved) return ``;
|
|
1032
|
+
return `[embedded media: ${alt || attrs.id || "media"}]`;
|
|
892
1033
|
}
|
|
893
|
-
function
|
|
894
|
-
return
|
|
1034
|
+
function renderInline(nodes, ctx) {
|
|
1035
|
+
return nodes.map((n) => renderInlineNode(n, ctx)).join("");
|
|
895
1036
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1037
|
+
function renderInlineNode(node, ctx) {
|
|
1038
|
+
switch (node.type) {
|
|
1039
|
+
case "text": return applyMarks(node.text ?? "", node.marks ?? []);
|
|
1040
|
+
case "hardBreak": return " \n";
|
|
1041
|
+
case "mention": return (typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : "") || "@unknown";
|
|
1042
|
+
case "emoji": {
|
|
1043
|
+
const text = node.attrs?.["text"];
|
|
1044
|
+
if (typeof text === "string" && text.length > 0) return text;
|
|
1045
|
+
const short = node.attrs?.["shortName"];
|
|
1046
|
+
return typeof short === "string" ? short : "";
|
|
1047
|
+
}
|
|
1048
|
+
case "date": return formatDate(node.attrs?.["timestamp"]);
|
|
1049
|
+
case "status": return `\`[${typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : ""}]\``;
|
|
1050
|
+
case "inlineCard": {
|
|
1051
|
+
const url = node.attrs?.["url"];
|
|
1052
|
+
if (typeof url === "string") return `[${url}](${url})`;
|
|
1053
|
+
return "";
|
|
1054
|
+
}
|
|
1055
|
+
case "media": return renderMedia(node, ctx);
|
|
1056
|
+
default: return node.text ?? "";
|
|
1057
|
+
}
|
|
901
1058
|
}
|
|
902
|
-
function
|
|
903
|
-
|
|
1059
|
+
function applyMarks(text, marks) {
|
|
1060
|
+
if (text.length === 0) return text;
|
|
1061
|
+
let out = text;
|
|
1062
|
+
let href;
|
|
1063
|
+
for (const mark of marks) switch (mark.type) {
|
|
1064
|
+
case "code":
|
|
1065
|
+
out = `\`${out}\``;
|
|
1066
|
+
break;
|
|
1067
|
+
case "strong":
|
|
1068
|
+
out = `**${out}**`;
|
|
1069
|
+
break;
|
|
1070
|
+
case "em":
|
|
1071
|
+
out = `*${out}*`;
|
|
1072
|
+
break;
|
|
1073
|
+
case "strike":
|
|
1074
|
+
out = `~~${out}~~`;
|
|
1075
|
+
break;
|
|
1076
|
+
case "link": {
|
|
1077
|
+
const value = mark.attrs?.["href"];
|
|
1078
|
+
if (typeof value === "string") href = value;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (href) out = `[${out}](${href})`;
|
|
1083
|
+
return out;
|
|
904
1084
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
bitbucket.command("login").description("Store Bitbucket workspace and API token").action(run(bitbucketLogin));
|
|
910
|
-
bitbucket.command("logout").description("Remove stored Bitbucket credentials").action(run(bitbucketLogout));
|
|
911
|
-
bitbucket.command("status").description("Show the current Bitbucket login").action(run(bitbucketStatus));
|
|
912
|
-
bitbucket.command("pipelines").description("List recent pipeline runs for a repo").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(run(bitbucketPipelines));
|
|
913
|
-
bitbucket.command("prs").description("List pull requests for a repo").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("-s, --state <state...>", "open, merged, declined, or superseded").option("--all", "include every state").option("-a, --author <who>", "limit to an author ('me', an account id, or a uuid)").option("--reviewer <who>", "limit to a reviewer ('me', an account id, or a uuid)").option("-q, --query <bbql>", "raw Bitbucket query (not for use with --author or --reviewer)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(run(bitbucketPrs));
|
|
914
|
-
bitbucket.command("pipeline <number>").description("Show one pipeline run and its steps").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").action(run(bitbucketPipeline));
|
|
915
|
-
return bitbucket;
|
|
1085
|
+
function formatDate(timestamp) {
|
|
1086
|
+
const ms = typeof timestamp === "string" ? Number(timestamp) : typeof timestamp === "number" ? timestamp : NaN;
|
|
1087
|
+
if (Number.isNaN(ms)) return "";
|
|
1088
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
916
1089
|
}
|
|
917
1090
|
//#endregion
|
|
918
|
-
//#region src/adf/
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
return
|
|
923
|
-
type: "mediaSingle",
|
|
924
|
-
attrs: { layout: "center" },
|
|
925
|
-
content: [{
|
|
926
|
-
type: "media",
|
|
927
|
-
attrs
|
|
928
|
-
}]
|
|
929
|
-
};
|
|
1091
|
+
//#region src/adf/to-markdown.ts
|
|
1092
|
+
function adfToMarkdown(doc, options = {}) {
|
|
1093
|
+
if (!doc) return "";
|
|
1094
|
+
const ctx = { resolveMedia: options.resolveMedia };
|
|
1095
|
+
return (doc.type === "doc" ? renderBlocks(doc.content ?? [], ctx, "") : renderBlock(doc, ctx, "")).trim();
|
|
930
1096
|
}
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
marked.walkTokens(marked.lexer(md), (token) => {
|
|
934
|
-
if (token.type === "image") hrefs.add(token.href);
|
|
935
|
-
});
|
|
936
|
-
return [...hrefs];
|
|
1097
|
+
function renderBlocks(nodes, ctx, indent) {
|
|
1098
|
+
return nodes.map((n) => renderBlock(n, ctx, indent)).filter((s) => s.length > 0).join("\n\n");
|
|
937
1099
|
}
|
|
938
|
-
function
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1100
|
+
function renderBlock(node, ctx, indent) {
|
|
1101
|
+
switch (node.type) {
|
|
1102
|
+
case "paragraph": return indent + renderInline(node.content ?? [], ctx);
|
|
1103
|
+
case "heading": {
|
|
1104
|
+
const level = clampLevel$1(node.attrs?.["level"]);
|
|
1105
|
+
return `${"#".repeat(level)} ${renderInline(node.content ?? [], ctx)}`;
|
|
1106
|
+
}
|
|
1107
|
+
case "bulletList": return renderList(node, ctx, indent, "bullet");
|
|
1108
|
+
case "orderedList": return renderList(node, ctx, indent, "ordered");
|
|
1109
|
+
case "taskList": return renderTaskList(node, ctx, indent);
|
|
1110
|
+
case "decisionList": return renderDecisionList(node, ctx, indent);
|
|
1111
|
+
case "codeBlock": return renderCodeBlock(node, indent);
|
|
1112
|
+
case "blockquote": return prefixLines(renderBlocks(node.content ?? [], ctx, ""), `${indent}> `);
|
|
1113
|
+
case "panel": return renderPanel(node, ctx, indent);
|
|
1114
|
+
case "rule": return `${indent}---`;
|
|
1115
|
+
case "table": return renderTable(node, ctx);
|
|
1116
|
+
case "mediaSingle":
|
|
1117
|
+
case "mediaGroup": return renderBlocks(node.content ?? [], ctx, indent);
|
|
1118
|
+
case "media": return indent + renderMedia(node, ctx);
|
|
1119
|
+
case "expand":
|
|
1120
|
+
case "nestedExpand": return renderExpand(node, ctx, indent);
|
|
1121
|
+
default:
|
|
1122
|
+
if (node.content?.length) return renderBlocks(node.content, ctx, indent);
|
|
1123
|
+
return node.text ? indent + node.text : "";
|
|
1124
|
+
}
|
|
945
1125
|
}
|
|
946
|
-
function
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
1126
|
+
function renderList(node, ctx, indent, kind) {
|
|
1127
|
+
const start = kind === "ordered" ? toNumber(node.attrs?.["order"]) ?? 1 : 0;
|
|
1128
|
+
return (node.content ?? []).filter((n) => n.type === "listItem").map((item, i) => {
|
|
1129
|
+
return renderListItem(item, ctx, indent, kind === "ordered" ? `${start + i}. ` : "- ");
|
|
1130
|
+
}).join("\n");
|
|
950
1131
|
}
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
case "heading": return [{
|
|
955
|
-
type: "heading",
|
|
956
|
-
attrs: { level: clampLevel$1(token.depth) },
|
|
957
|
-
content: inline(token.tokens ?? [], ctx)
|
|
958
|
-
}];
|
|
959
|
-
case "paragraph": return paragraphsSplitAtImages(token.tokens ?? [], ctx);
|
|
960
|
-
case "text": return paragraphsSplitAtImages(token.tokens ?? [textToken(token.text)], ctx);
|
|
961
|
-
case "list": return [list(token, ctx)];
|
|
962
|
-
case "code": return [codeBlock(token)];
|
|
963
|
-
case "blockquote": return [{
|
|
964
|
-
type: "blockquote",
|
|
965
|
-
content: blocks(token.tokens ?? [], ctx)
|
|
966
|
-
}];
|
|
967
|
-
case "table": return [table(token, ctx)];
|
|
968
|
-
case "hr": return [{ type: "rule" }];
|
|
969
|
-
default: return [];
|
|
970
|
-
}
|
|
1132
|
+
function renderListItem(item, ctx, indent, marker) {
|
|
1133
|
+
const continuationIndent = `${indent}${" ".repeat(marker.length)}`;
|
|
1134
|
+
return (item.content ?? []).map((block, i) => i === 0 ? `${indent}${marker}${renderBlock(block, ctx, "")}` : renderBlock(block, ctx, continuationIndent)).join("\n");
|
|
971
1135
|
}
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
if (buffer.length === 0) return;
|
|
977
|
-
const nodes = inline(buffer, ctx);
|
|
978
|
-
if (nodes.length > 0) out.push({
|
|
979
|
-
type: "paragraph",
|
|
980
|
-
content: nodes
|
|
981
|
-
});
|
|
982
|
-
buffer = [];
|
|
983
|
-
};
|
|
984
|
-
for (const token of tokens) if (token.type === "image") {
|
|
985
|
-
flush();
|
|
986
|
-
const image = token;
|
|
987
|
-
const node = ctx.resolveImage(image.href, image.text ?? "");
|
|
988
|
-
if (node) out.push(node);
|
|
989
|
-
} else buffer.push(token);
|
|
990
|
-
flush();
|
|
991
|
-
return out;
|
|
1136
|
+
function renderTaskList(node, ctx, indent) {
|
|
1137
|
+
return (node.content ?? []).filter((n) => n.type === "taskItem").map((item) => {
|
|
1138
|
+
return `${indent}- [${item.attrs?.["state"] === "DONE" ? "x" : " "}] ${renderInline(item.content ?? [], ctx)}`;
|
|
1139
|
+
}).join("\n");
|
|
992
1140
|
}
|
|
993
|
-
function
|
|
994
|
-
|
|
995
|
-
type: "taskList",
|
|
996
|
-
attrs: { localId: randomUUID() },
|
|
997
|
-
content: token.items.map((item) => ({
|
|
998
|
-
type: "taskItem",
|
|
999
|
-
attrs: {
|
|
1000
|
-
localId: randomUUID(),
|
|
1001
|
-
state: item.checked ? "DONE" : "TODO"
|
|
1002
|
-
},
|
|
1003
|
-
content: inline(taskItemInline(item), ctx)
|
|
1004
|
-
}))
|
|
1005
|
-
};
|
|
1006
|
-
const node = {
|
|
1007
|
-
type: token.ordered ? "orderedList" : "bulletList",
|
|
1008
|
-
content: token.items.map((item) => listItem(item, ctx))
|
|
1009
|
-
};
|
|
1010
|
-
const start = Number(token.start);
|
|
1011
|
-
if (token.ordered && Number.isFinite(start) && start !== 1) node.attrs = { order: start };
|
|
1012
|
-
return node;
|
|
1141
|
+
function renderDecisionList(node, ctx, indent) {
|
|
1142
|
+
return (node.content ?? []).filter((n) => n.type === "decisionItem").map((item) => `${indent}- (decision) ${renderInline(item.content ?? [], ctx)}`).join("\n");
|
|
1013
1143
|
}
|
|
1014
|
-
function
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
};
|
|
1144
|
+
function renderCodeBlock(node, indent) {
|
|
1145
|
+
const lang = typeof node.attrs?.["language"] === "string" ? node.attrs["language"] : "";
|
|
1146
|
+
const code = (node.content ?? []).map((n) => n.text ?? "").join("");
|
|
1147
|
+
const fence = "```";
|
|
1148
|
+
return prefixLines(`${fence}${lang}\n${code}\n${fence}`, indent);
|
|
1020
1149
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1150
|
+
const PANEL_LABELS = {
|
|
1151
|
+
info: "Info",
|
|
1152
|
+
note: "Note",
|
|
1153
|
+
warning: "Warning",
|
|
1154
|
+
success: "Success",
|
|
1155
|
+
error: "Error"
|
|
1156
|
+
};
|
|
1157
|
+
function renderPanel(node, ctx, indent) {
|
|
1158
|
+
const type = typeof node.attrs?.["panelType"] === "string" ? node.attrs["panelType"] : "info";
|
|
1159
|
+
return prefixLines(`**${PANEL_LABELS[type] ?? "Note"}**\n\n${renderBlocks(node.content ?? [], ctx, "")}`, `${indent}> `);
|
|
1026
1160
|
}
|
|
1027
|
-
function
|
|
1028
|
-
|
|
1029
|
-
if (first && "tokens" in first && first.tokens) return first.tokens;
|
|
1030
|
-
return [textToken(item.text)];
|
|
1161
|
+
function renderExpand(node, ctx, indent) {
|
|
1162
|
+
return `${indent}<details><summary>${typeof node.attrs?.["title"] === "string" ? node.attrs["title"] : "Details"}</summary>\n\n${renderBlocks(node.content ?? [], ctx, "")}\n\n${indent}</details>`;
|
|
1031
1163
|
}
|
|
1032
|
-
function
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1164
|
+
function renderTable(node, ctx) {
|
|
1165
|
+
const rows = (node.content ?? []).filter((n) => n.type === "tableRow");
|
|
1166
|
+
if (rows.length === 0) return "";
|
|
1167
|
+
const grid = rows.map((row) => (row.content ?? []).map((cell) => renderCellOnOneLine(cell, ctx)));
|
|
1168
|
+
const cols = Math.max(...grid.map((r) => r.length));
|
|
1169
|
+
const pad = (r) => {
|
|
1170
|
+
const copy = [...r];
|
|
1171
|
+
while (copy.length < cols) copy.push("");
|
|
1172
|
+
return copy;
|
|
1040
1173
|
};
|
|
1174
|
+
const header = pad(grid[0] ?? []);
|
|
1175
|
+
const lines = [`| ${header.join(" | ")} |`, `| ${header.map(() => "---").join(" | ")} |`];
|
|
1176
|
+
for (const row of grid.slice(1)) lines.push(`| ${pad(row).join(" | ")} |`);
|
|
1177
|
+
return lines.join("\n");
|
|
1041
1178
|
}
|
|
1042
|
-
function
|
|
1043
|
-
return
|
|
1044
|
-
type: "table",
|
|
1045
|
-
content: [{
|
|
1046
|
-
type: "tableRow",
|
|
1047
|
-
content: token.header.map((cell) => tableCell(cell, "tableHeader", ctx))
|
|
1048
|
-
}, ...token.rows.map((row) => ({
|
|
1049
|
-
type: "tableRow",
|
|
1050
|
-
content: row.map((cell) => tableCell(cell, "tableCell", ctx))
|
|
1051
|
-
}))]
|
|
1052
|
-
};
|
|
1179
|
+
function renderCellOnOneLine(cell, ctx) {
|
|
1180
|
+
return renderBlocks(cell.content ?? [], ctx, "").replace(/\n+/g, " ").replace(/\|/g, "\\|").trim();
|
|
1053
1181
|
}
|
|
1054
|
-
function
|
|
1055
|
-
return
|
|
1056
|
-
type,
|
|
1057
|
-
content: [{
|
|
1058
|
-
type: "paragraph",
|
|
1059
|
-
content: inline(cell.tokens, ctx)
|
|
1060
|
-
}]
|
|
1061
|
-
};
|
|
1182
|
+
function prefixLines(text, prefix) {
|
|
1183
|
+
return text.split("\n").map((line) => line.length > 0 ? prefix + line : prefix.trimEnd()).join("\n");
|
|
1062
1184
|
}
|
|
1063
|
-
function
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
return out;
|
|
1185
|
+
function clampLevel$1(value) {
|
|
1186
|
+
const n = toNumber(value) ?? 1;
|
|
1187
|
+
return Math.min(6, Math.max(1, n));
|
|
1067
1188
|
}
|
|
1068
|
-
function
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1189
|
+
function toNumber(value) {
|
|
1190
|
+
return typeof value === "number" ? value : void 0;
|
|
1191
|
+
}
|
|
1192
|
+
//#endregion
|
|
1193
|
+
//#region src/markdown/highlight.ts
|
|
1194
|
+
const HEADING = /^#{1,6} /;
|
|
1195
|
+
const QUOTE = /^(\s*)>( |$)/;
|
|
1196
|
+
const LIST = /^(\s*)(-|\d+\.) (\[[ x]\] )?/;
|
|
1197
|
+
const RULE = /^\s*---$/;
|
|
1198
|
+
const TABLE_ROW = /^\|.*\|$/;
|
|
1199
|
+
const TABLE_SEPARATOR = /^\|( --- \|)+$/;
|
|
1200
|
+
const CELL_DIVIDER = /(?<!\\)\|/;
|
|
1201
|
+
const DETAILS_OPEN = /^(\s*)<details><summary>(.*)<\/summary>$/;
|
|
1202
|
+
const DETAILS_CLOSE = /^\s*<\/details>$/;
|
|
1203
|
+
const FENCE_OPEN = /^((?:\s|> |- |\d+\. )*)```(\S*)$/;
|
|
1204
|
+
const FENCE = "```";
|
|
1205
|
+
const INLINE = /(`[^`]+`)|\[([^\]]+)\]\(((?:[^()]|\([^()]*\))+)\)|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(?!\*)(.+?)(?<!\*)\*(?!\*)|~~(.+?)~~/g;
|
|
1206
|
+
const TOKEN_STYLES = {
|
|
1207
|
+
keyword: kleur.magenta,
|
|
1208
|
+
built_in: kleur.magenta,
|
|
1209
|
+
literal: kleur.magenta,
|
|
1210
|
+
string: kleur.green,
|
|
1211
|
+
regexp: kleur.green,
|
|
1212
|
+
addition: kleur.green,
|
|
1213
|
+
number: kleur.yellow,
|
|
1214
|
+
symbol: kleur.yellow,
|
|
1215
|
+
meta: kleur.yellow,
|
|
1216
|
+
attr: kleur.yellow,
|
|
1217
|
+
comment: kleur.dim,
|
|
1218
|
+
quote: kleur.dim,
|
|
1219
|
+
deletion: kleur.dim,
|
|
1220
|
+
title: kleur.cyan,
|
|
1221
|
+
type: kleur.cyan,
|
|
1222
|
+
section: kleur.cyan
|
|
1223
|
+
};
|
|
1224
|
+
const lowlight = createLowlight(common);
|
|
1225
|
+
function highlightMarkdown(md) {
|
|
1226
|
+
const lines = md.split("\n");
|
|
1227
|
+
const out = [];
|
|
1228
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1229
|
+
const line = lines[i] ?? "";
|
|
1230
|
+
const fence = line.match(FENCE_OPEN);
|
|
1231
|
+
if (!fence) {
|
|
1232
|
+
out.push(highlightLine(line, lines[i + 1]));
|
|
1233
|
+
continue;
|
|
1075
1234
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1235
|
+
const [, prefix = "", lang = ""] = fence;
|
|
1236
|
+
const styledPrefix = prefix.replace(/\S+/g, (marker) => kleur.dim(marker));
|
|
1237
|
+
out.push(`${styledPrefix}${kleur.dim(`${FENCE}${lang}`)}`);
|
|
1238
|
+
const code = [];
|
|
1239
|
+
const prefixed = [];
|
|
1240
|
+
let j = i + 1;
|
|
1241
|
+
for (; j < lines.length; j++) {
|
|
1242
|
+
const raw = lines[j] ?? "";
|
|
1243
|
+
const hasPrefix = raw.startsWith(prefix) || raw === prefix.trimEnd();
|
|
1244
|
+
const body = raw.startsWith(prefix) ? raw.slice(prefix.length) : hasPrefix ? "" : raw;
|
|
1245
|
+
if (body === FENCE) break;
|
|
1246
|
+
code.push(body);
|
|
1247
|
+
prefixed.push(hasPrefix);
|
|
1086
1248
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1249
|
+
if (code.length > 0) highlightCode(code.join("\n"), lang).split("\n").forEach((codeLine, k) => {
|
|
1250
|
+
const lead = prefixed[k] ? styledPrefix : "";
|
|
1251
|
+
out.push(codeLine ? `${lead}${codeLine}` : lead.trimEnd());
|
|
1252
|
+
});
|
|
1253
|
+
if (j < lines.length) {
|
|
1254
|
+
const lead = (lines[j] ?? "").startsWith(prefix) ? styledPrefix : "";
|
|
1255
|
+
out.push(`${lead}${kleur.dim(FENCE)}`);
|
|
1256
|
+
}
|
|
1257
|
+
i = j;
|
|
1090
1258
|
}
|
|
1259
|
+
return out.join("\n");
|
|
1091
1260
|
}
|
|
1092
|
-
function
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1095
|
-
type: "text",
|
|
1096
|
-
text
|
|
1097
|
-
};
|
|
1098
|
-
if (marks.length > 0) node.marks = marks;
|
|
1099
|
-
return [node];
|
|
1261
|
+
function highlightCode(code, lang) {
|
|
1262
|
+
if (!lang || !lowlight.registered(lang)) return styleEachLine(code, kleur.yellow);
|
|
1263
|
+
return renderTree(lowlight.highlight(lang, code), (text) => text);
|
|
1100
1264
|
}
|
|
1101
|
-
function
|
|
1102
|
-
|
|
1265
|
+
function renderTree(node, style) {
|
|
1266
|
+
if (node.type === "text") return styleEachLine(node.value, style);
|
|
1267
|
+
if (node.type !== "root" && node.type !== "element") return "";
|
|
1268
|
+
const own = node.type === "element" ? tokenStyle(node.properties["className"]) : void 0;
|
|
1269
|
+
const next = own ? (text) => style(own(text)) : style;
|
|
1270
|
+
return node.children.map((child) => renderTree(child, next)).join("");
|
|
1103
1271
|
}
|
|
1104
|
-
function
|
|
1105
|
-
return
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1272
|
+
function tokenStyle(className) {
|
|
1273
|
+
if (!Array.isArray(className)) return void 0;
|
|
1274
|
+
for (const name of className) {
|
|
1275
|
+
if (typeof name !== "string" || !name.startsWith("hljs-")) continue;
|
|
1276
|
+
const style = TOKEN_STYLES[name.slice(5)];
|
|
1277
|
+
if (style) return style;
|
|
1278
|
+
}
|
|
1109
1279
|
}
|
|
1110
|
-
function
|
|
1111
|
-
return
|
|
1112
|
-
type: "text",
|
|
1113
|
-
raw: text,
|
|
1114
|
-
text,
|
|
1115
|
-
escaped: false
|
|
1116
|
-
};
|
|
1280
|
+
function styleEachLine(text, style) {
|
|
1281
|
+
return text.split("\n").map((line) => line ? style(line) : line).join("\n");
|
|
1117
1282
|
}
|
|
1118
|
-
function
|
|
1119
|
-
|
|
1283
|
+
function highlightLine(line, nextLine) {
|
|
1284
|
+
const quote = line.match(QUOTE);
|
|
1285
|
+
if (quote) {
|
|
1286
|
+
const [whole, indent = ""] = quote;
|
|
1287
|
+
const rest = line.slice(whole.length);
|
|
1288
|
+
const gap = whole.endsWith(" ") ? " " : "";
|
|
1289
|
+
return `${indent}${kleur.dim(">")}${gap}${highlightLine(rest, stripPrefix(nextLine, whole))}`;
|
|
1290
|
+
}
|
|
1291
|
+
const list = line.match(LIST);
|
|
1292
|
+
if (list) {
|
|
1293
|
+
const [whole, indent = "", marker = "", checkbox] = list;
|
|
1294
|
+
const rest = line.slice(whole.length);
|
|
1295
|
+
const box = checkbox ? `${kleur.bold(checkbox.trim())} ` : "";
|
|
1296
|
+
return `${indent}${kleur.dim(marker)} ${box}${highlightLine(rest, void 0)}`;
|
|
1297
|
+
}
|
|
1298
|
+
if (HEADING.test(line)) return kleur.bold().cyan(line);
|
|
1299
|
+
if (RULE.test(line)) return kleur.dim(line);
|
|
1300
|
+
if (TABLE_SEPARATOR.test(line)) return kleur.dim(line);
|
|
1301
|
+
if (TABLE_ROW.test(line)) return tableRow(line, nextLine !== void 0 && TABLE_SEPARATOR.test(nextLine));
|
|
1302
|
+
const details = line.match(DETAILS_OPEN);
|
|
1303
|
+
if (details) {
|
|
1304
|
+
const [, indent = "", title = ""] = details;
|
|
1305
|
+
return `${indent}${kleur.dim("<details><summary>")}${highlightInline(title)}${kleur.dim("</summary>")}`;
|
|
1306
|
+
}
|
|
1307
|
+
if (DETAILS_CLOSE.test(line)) return kleur.dim(line);
|
|
1308
|
+
return highlightInline(line);
|
|
1120
1309
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const UNKNOWN_SIZE = -1;
|
|
1124
|
-
async function listAttachments(client, id) {
|
|
1125
|
-
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.map((a) => ({
|
|
1126
|
-
filename: a.title ?? a.id,
|
|
1127
|
-
fileId: mediaNodeId(a),
|
|
1128
|
-
size: typeof a.fileSize === "number" ? a.fileSize : UNKNOWN_SIZE
|
|
1129
|
-
}));
|
|
1310
|
+
function stripPrefix(line, prefix) {
|
|
1311
|
+
return line?.startsWith(prefix) ? line.slice(prefix.length) : void 0;
|
|
1130
1312
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1313
|
+
function tableRow(line, header) {
|
|
1314
|
+
const styled = line.split(CELL_DIVIDER).slice(1, -1).map((cell) => header ? kleur.bold(highlightInline(cell)) : highlightInline(cell));
|
|
1315
|
+
const pipe = kleur.dim("|");
|
|
1316
|
+
return `${pipe}${styled.join(pipe)}${pipe}`;
|
|
1133
1317
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1318
|
+
function highlightInline(text) {
|
|
1319
|
+
return text.replace(INLINE, (match, code, linkText, url, boldItalic, bold, italic, strike) => {
|
|
1320
|
+
if (code !== void 0) return kleur.yellow(code);
|
|
1321
|
+
if (linkText !== void 0 && url !== void 0) return link(linkText, url);
|
|
1322
|
+
if (boldItalic !== void 0) return kleur.bold().italic(match);
|
|
1323
|
+
if (bold !== void 0) return kleur.bold(`**${highlightInline(bold)}**`);
|
|
1324
|
+
if (italic !== void 0) return kleur.italic(`*${highlightInline(italic)}*`);
|
|
1325
|
+
if (strike !== void 0) return kleur.strikethrough(`~~${highlightInline(strike)}~~`);
|
|
1326
|
+
return match;
|
|
1327
|
+
});
|
|
1138
1328
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
url: withWikiContextPath(a.downloadLink ?? "")
|
|
1144
|
-
}));
|
|
1329
|
+
function link(text, url) {
|
|
1330
|
+
const open = kleur.dim("[");
|
|
1331
|
+
const middle = kleur.dim("](");
|
|
1332
|
+
return `${open}${highlightInline(text)}${middle}${kleur.dim().underline(url)}${kleur.dim(")")}`;
|
|
1145
1333
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1334
|
+
//#endregion
|
|
1335
|
+
//#region src/commands/view.ts
|
|
1336
|
+
const INDENT = " ";
|
|
1337
|
+
function fieldLines(pairs) {
|
|
1338
|
+
const kept = pairs.filter(([, value]) => value !== "");
|
|
1339
|
+
const width = Math.max(...kept.map(([label]) => label.length)) + 1;
|
|
1340
|
+
return kept.map(([label, value]) => `${kleur.dim(`${label}:`.padEnd(width))} ${value}`);
|
|
1148
1341
|
}
|
|
1149
|
-
function
|
|
1150
|
-
if (
|
|
1151
|
-
return
|
|
1342
|
+
function dateWithAge(iso, nowMs) {
|
|
1343
|
+
if (!iso) return "";
|
|
1344
|
+
return `${formatDate$1(iso)} (${relativeTime(iso, nowMs)})`;
|
|
1152
1345
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
async function fetchPage(client, site, id) {
|
|
1156
|
-
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
1157
|
-
const names = new UserNames(client);
|
|
1158
|
-
const [spaceKey, attachments, comments, author] = await Promise.all([
|
|
1159
|
-
fetchSpaceKey(client, page.spaceId),
|
|
1160
|
-
fetchAttachments(client, id),
|
|
1161
|
-
fetchComments$1(client, id, names),
|
|
1162
|
-
names.resolve(page.version?.authorId ?? page.authorId)
|
|
1163
|
-
]);
|
|
1164
|
-
const webui = page._links?.webui ?? "";
|
|
1165
|
-
return {
|
|
1166
|
-
id: page.id,
|
|
1167
|
-
title: page.title,
|
|
1168
|
-
spaceKey,
|
|
1169
|
-
version: page.version?.number ?? 0,
|
|
1170
|
-
author,
|
|
1171
|
-
createdAt: page.createdAt ?? "",
|
|
1172
|
-
updatedAt: page.version?.createdAt ?? "",
|
|
1173
|
-
url: webui ? `${site}/wiki${webui}` : "",
|
|
1174
|
-
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
1175
|
-
attachments,
|
|
1176
|
-
comments
|
|
1177
|
-
};
|
|
1346
|
+
function markdownBody(md) {
|
|
1347
|
+
return md ? ["", highlightMarkdown(md)] : [];
|
|
1178
1348
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1349
|
+
function bodyLines(body) {
|
|
1350
|
+
return markdownBody(adfToMarkdown(body));
|
|
1351
|
+
}
|
|
1352
|
+
function renderedComments(comments) {
|
|
1353
|
+
return comments.map((comment) => [{
|
|
1354
|
+
author: comment.author,
|
|
1355
|
+
created: comment.created,
|
|
1356
|
+
markdown: adfToMarkdown(comment.body)
|
|
1357
|
+
}]);
|
|
1358
|
+
}
|
|
1359
|
+
function commentSection(threads, options) {
|
|
1360
|
+
const total = threads.reduce((running, thread) => running + thread.length, 0);
|
|
1361
|
+
if (total === 0) return [];
|
|
1362
|
+
const visible = options.allComments ? threads : threads.slice(-5);
|
|
1363
|
+
const lines = ["", kleur.bold(commentHeading(total, threads.length, visible.length, options))];
|
|
1364
|
+
for (const thread of visible) thread.forEach((comment, index) => {
|
|
1365
|
+
lines.push("", commentHeader(comment, index > 0), ...commentBody(comment, index > 0), ...commentTrailer(comment));
|
|
1366
|
+
});
|
|
1367
|
+
return lines;
|
|
1186
1368
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
body: {
|
|
1193
|
-
representation: "atlas_doc_format",
|
|
1194
|
-
value: JSON.stringify(params.body)
|
|
1195
|
-
},
|
|
1196
|
-
version: {
|
|
1197
|
-
number: params.nextVersion,
|
|
1198
|
-
message: params.message
|
|
1199
|
-
}
|
|
1200
|
-
})).version?.number ?? params.nextVersion;
|
|
1369
|
+
function commentHeading(total, threads, visible, options) {
|
|
1370
|
+
const count = options.truncated ? `${total}+` : String(total);
|
|
1371
|
+
if (visible === threads) return `Comments (${count})`;
|
|
1372
|
+
if (threads === total) return `Comments (${count}, showing last ${visible} — --all-comments for all)`;
|
|
1373
|
+
return `Comments (${count} in ${threads} threads, showing last ${visible} threads — --all-comments for all)`;
|
|
1201
1374
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1375
|
+
function commentHeader(comment, reply) {
|
|
1376
|
+
const author = kleur.bold(comment.author || "Unknown");
|
|
1377
|
+
const when = comment.created ? ` · ${kleur.dim(formatDateTime(comment.created))}` : "";
|
|
1378
|
+
const where = comment.anchor ? ` · ${kleur.dim(comment.anchor)}` : "";
|
|
1379
|
+
return `${reply ? `${INDENT}↳ ` : "─ "}${author}${when}${where}`;
|
|
1380
|
+
}
|
|
1381
|
+
function commentBody(comment, reply) {
|
|
1382
|
+
if (!comment.markdown) return [];
|
|
1383
|
+
const body = highlightMarkdown(comment.markdown);
|
|
1384
|
+
if (!reply) return [body];
|
|
1385
|
+
return [body.split("\n").map((line) => line ? `${INDENT}${line}` : line).join("\n")];
|
|
1386
|
+
}
|
|
1387
|
+
function commentTrailer(comment) {
|
|
1388
|
+
return comment.trailer ? ["", comment.trailer] : [];
|
|
1389
|
+
}
|
|
1390
|
+
function attachmentSection(attachments) {
|
|
1391
|
+
if (attachments.length === 0) return [];
|
|
1392
|
+
return [
|
|
1393
|
+
"",
|
|
1394
|
+
kleur.bold("Attachments"),
|
|
1395
|
+
...attachments.map((a) => `- ${a.filename}`)
|
|
1396
|
+
];
|
|
1397
|
+
}
|
|
1398
|
+
//#endregion
|
|
1399
|
+
//#region src/commands/bitbucket-pr.ts
|
|
1400
|
+
async function bitbucketPr({ session, term }, arg, options) {
|
|
1401
|
+
const ref = parseRef(arg);
|
|
1402
|
+
const repo = ref.repo ?? resolveRepo(options.repo, session);
|
|
1403
|
+
const detail = await withScopeHint(PULL_REQUEST_SCOPE, () => fetchDetail(session, repo, ref.id));
|
|
1404
|
+
const [comments, files] = await Promise.all([withScopeHint(PULL_REQUEST_SCOPE, () => listComments(session, repo, ref.id)), fetchFiles(session, repo, ref.id)]);
|
|
1405
|
+
if (options.json) {
|
|
1406
|
+
term.json({
|
|
1407
|
+
...detail,
|
|
1408
|
+
comments: comments.items,
|
|
1409
|
+
files: files?.items ?? null,
|
|
1410
|
+
truncated: {
|
|
1411
|
+
comments: comments.truncated,
|
|
1412
|
+
files: files?.truncated ?? false
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
return;
|
|
1208
1416
|
}
|
|
1417
|
+
const lines = formatPullRequestView(detail, comments, files, Date.now(), options.allComments);
|
|
1418
|
+
await term.page(lines.join("\n"), { pager: options.pager });
|
|
1209
1419
|
}
|
|
1210
|
-
|
|
1211
|
-
const
|
|
1212
|
-
return
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1420
|
+
function formatPullRequestView(detail, comments, files, nowMs, allComments = false) {
|
|
1421
|
+
const label = detail.draft ? "DRAFT" : detail.state || "-";
|
|
1422
|
+
return [
|
|
1423
|
+
kleur.bold(detail.title),
|
|
1424
|
+
...fieldLines([
|
|
1425
|
+
["State", colorForBitbucketState(label)(label)],
|
|
1426
|
+
["Author", detail.author],
|
|
1427
|
+
["Branch", branchLine(detail)],
|
|
1428
|
+
["Approvals", approvalCount(detail.reviewers)],
|
|
1429
|
+
["Created", dateWithAge(detail.createdOn, nowMs)],
|
|
1430
|
+
["Updated", dateWithAge(detail.updatedOn, nowMs)],
|
|
1431
|
+
["Closed by", detail.closedBy],
|
|
1432
|
+
["URL", detail.url]
|
|
1433
|
+
]),
|
|
1434
|
+
...markdownBody(detail.description),
|
|
1435
|
+
...reviewerSection(detail.reviewers),
|
|
1436
|
+
...fileSection(files),
|
|
1437
|
+
...commentSection(renderedPrComments(comments.items, detail.participants), {
|
|
1438
|
+
allComments,
|
|
1439
|
+
truncated: comments.truncated
|
|
1440
|
+
})
|
|
1441
|
+
];
|
|
1217
1442
|
}
|
|
1218
|
-
function
|
|
1219
|
-
|
|
1443
|
+
function reviewerSection(reviewers) {
|
|
1444
|
+
const heading = ["", kleur.bold("Reviewers")];
|
|
1445
|
+
if (reviewers.length === 0) return [...heading, ` ${kleur.dim("None assigned")}`];
|
|
1446
|
+
const width = Math.max(...reviewers.map((r) => r.name.length));
|
|
1447
|
+
return [...heading, ...reviewers.map((r) => ` ${r.name.padEnd(width)} ${reviewerState(r)}`)];
|
|
1448
|
+
}
|
|
1449
|
+
function fileSection(files) {
|
|
1450
|
+
if (!files) return [
|
|
1451
|
+
"",
|
|
1452
|
+
kleur.bold("Files"),
|
|
1453
|
+
` ${kleur.dim(`Unavailable: the token needs the ${REPOSITORY_SCOPE} scope.`)}`
|
|
1454
|
+
];
|
|
1455
|
+
if (files.items.length === 0) return [];
|
|
1456
|
+
const rows = files.items.map((file) => ({
|
|
1457
|
+
added: `+${file.added}`,
|
|
1458
|
+
removed: `-${file.removed}`,
|
|
1459
|
+
path: file.path
|
|
1460
|
+
}));
|
|
1461
|
+
const wa = Math.max(...rows.map((r) => r.added.length));
|
|
1462
|
+
const wr = Math.max(...rows.map((r) => r.removed.length));
|
|
1463
|
+
const total = files.total ?? files.items.length;
|
|
1464
|
+
const added = sum(files.items, (f) => f.added);
|
|
1465
|
+
const removed = sum(files.items, (f) => f.removed);
|
|
1466
|
+
return [
|
|
1467
|
+
"",
|
|
1468
|
+
kleur.bold(`Files (${total}, +${added} -${removed})`),
|
|
1469
|
+
...rows.map((r) => ` ${kleur.green(r.added.padStart(wa))} ${kleur.red(r.removed.padStart(wr))} ${r.path}`),
|
|
1470
|
+
...moreFilesLine(files)
|
|
1471
|
+
];
|
|
1472
|
+
}
|
|
1473
|
+
function moreFilesLine(files) {
|
|
1474
|
+
if (!files.truncated) return [];
|
|
1475
|
+
const rest = files.total === void 0 ? void 0 : files.total - files.items.length;
|
|
1476
|
+
return [kleur.dim(rest === void 0 ? " ... more files not shown" : ` ... and ${rest} more`)];
|
|
1477
|
+
}
|
|
1478
|
+
function sum(files, pick) {
|
|
1479
|
+
return files.reduce((running, file) => running + pick(file), 0);
|
|
1480
|
+
}
|
|
1481
|
+
function reviewerState(reviewer) {
|
|
1482
|
+
if (!reviewer.state) return kleur.dim("—");
|
|
1483
|
+
return colorForBitbucketState(reviewer.state)(reviewer.state);
|
|
1484
|
+
}
|
|
1485
|
+
function approvalCount(reviewers) {
|
|
1486
|
+
return `${reviewers.filter((r) => r.state === "APPROVED").length}/${reviewers.length}`;
|
|
1487
|
+
}
|
|
1488
|
+
function branchLine(detail) {
|
|
1489
|
+
if (!detail.sourceBranch || !detail.destinationBranch) return detail.sourceBranch || detail.destinationBranch;
|
|
1490
|
+
return `${detail.sourceBranch} → ${detail.destinationBranch}`;
|
|
1491
|
+
}
|
|
1492
|
+
function parseRef(arg) {
|
|
1493
|
+
const ref = parsePullRequestRef(arg ?? "");
|
|
1494
|
+
if (!ref) throw new Error(`Invalid pull request "${arg ?? ""}". Expected a PR number, e.g. 42, or a Bitbucket pull request URL.`);
|
|
1495
|
+
return ref;
|
|
1496
|
+
}
|
|
1497
|
+
async function fetchFiles(session, repo, id) {
|
|
1220
1498
|
try {
|
|
1221
|
-
return
|
|
1222
|
-
} catch {
|
|
1223
|
-
return null;
|
|
1499
|
+
return await listChangedFiles(session, repo, id);
|
|
1500
|
+
} catch (err) {
|
|
1501
|
+
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) return null;
|
|
1502
|
+
throw err;
|
|
1224
1503
|
}
|
|
1225
1504
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1505
|
+
async function fetchDetail(session, repo, id) {
|
|
1506
|
+
try {
|
|
1507
|
+
return await getPullRequest(session, repo, id);
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
if (err instanceof HttpError && err.status === 404) throw new Error(`Pull request #${id} not found in ${repo.workspace}/${repo.repo}.`);
|
|
1510
|
+
throw err;
|
|
1231
1511
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1512
|
+
}
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region src/util/link.ts
|
|
1515
|
+
const OSC8 = "\x1B]8;;";
|
|
1516
|
+
const BEL = "\x07";
|
|
1517
|
+
function hyperlink(text, url) {
|
|
1518
|
+
if (!kleur.enabled || !url) return text;
|
|
1519
|
+
return `${OSC8}${url}${BEL}${text}${OSC8}${BEL}`;
|
|
1520
|
+
}
|
|
1521
|
+
//#endregion
|
|
1522
|
+
//#region src/commands/search-run.ts
|
|
1523
|
+
function alignedRows(items, nowMs, cells) {
|
|
1524
|
+
const rows = items.map((item) => {
|
|
1525
|
+
const cell = cells(item);
|
|
1526
|
+
return {
|
|
1527
|
+
...cell,
|
|
1528
|
+
age: relativeTime(cell.timestamp, nowMs),
|
|
1529
|
+
json: { ...item }
|
|
1530
|
+
};
|
|
1531
|
+
});
|
|
1532
|
+
const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
|
|
1533
|
+
const idWidth = width((row) => row.id);
|
|
1534
|
+
const labelWidth = width((row) => row.label);
|
|
1535
|
+
const ageWidth = width((row) => row.age);
|
|
1536
|
+
return rows.map((row) => ({
|
|
1537
|
+
id: row.id,
|
|
1538
|
+
url: row.url,
|
|
1539
|
+
fixedColumns: `${row.id.padEnd(idWidth)} ${row.color(row.label.padEnd(labelWidth))} ${row.age.padEnd(ageWidth)}`,
|
|
1540
|
+
freeText: row.text,
|
|
1541
|
+
json: row.json
|
|
1542
|
+
}));
|
|
1543
|
+
}
|
|
1544
|
+
function formatRows(rows, options) {
|
|
1545
|
+
if (rows.length === 0) return [options.empty];
|
|
1546
|
+
const lines = rows.map((row) => formatRow(row, options.width));
|
|
1547
|
+
return options.footer ? [...lines, options.footer] : lines;
|
|
1548
|
+
}
|
|
1549
|
+
function writeRows(term, rows, options) {
|
|
1550
|
+
if (options.json) term.json(rows.map((r) => r.json));
|
|
1551
|
+
else term.out(formatRows(rows, {
|
|
1552
|
+
...options,
|
|
1553
|
+
width: term.width
|
|
1554
|
+
}));
|
|
1555
|
+
}
|
|
1556
|
+
const COPY_CONCURRENCY = 5;
|
|
1557
|
+
async function runSearch(term, rows, options, noun, copyOne) {
|
|
1558
|
+
if (!(options.copy && !options.json && rows.length > 0)) {
|
|
1559
|
+
writeRows(term, rows, options);
|
|
1560
|
+
return;
|
|
1239
1561
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1562
|
+
if (options.out?.endsWith(".md")) throw new Error("--out must be a directory when using --copy; a .md file path would overwrite each selection.");
|
|
1563
|
+
await copySelected(term, rows, noun, copyOne);
|
|
1564
|
+
}
|
|
1565
|
+
function searchFooter(limit) {
|
|
1566
|
+
return `
|
|
1567
|
+
Showing first ${limit}; refine with flags or raise --limit.`;
|
|
1568
|
+
}
|
|
1569
|
+
async function copySelected(term, rows, noun, copyOne) {
|
|
1570
|
+
if (!term.interactive) throw new Error("--copy requires an interactive terminal.");
|
|
1571
|
+
const selected = await term.ask.pickMany({
|
|
1572
|
+
message: `Select ${noun.plural} to copy:`,
|
|
1573
|
+
choices: rows.map((r) => ({
|
|
1574
|
+
name: formatRow(r, term.width),
|
|
1575
|
+
value: r.id
|
|
1576
|
+
})),
|
|
1577
|
+
pageSize: 20
|
|
1578
|
+
});
|
|
1579
|
+
if (selected.length === 0) {
|
|
1580
|
+
term.out("Nothing selected.");
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
let copied = 0;
|
|
1584
|
+
const failures = [];
|
|
1585
|
+
const queue = [...selected];
|
|
1586
|
+
async function worker() {
|
|
1587
|
+
for (let id = queue.shift(); id !== void 0; id = queue.shift()) try {
|
|
1588
|
+
await copyOne(id);
|
|
1589
|
+
copied++;
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
failures.push(`${id} (${err instanceof Error ? err.message : String(err)})`);
|
|
1245
1592
|
}
|
|
1246
1593
|
}
|
|
1247
|
-
};
|
|
1594
|
+
await Promise.all(Array.from({ length: Math.min(COPY_CONCURRENCY, selected.length) }, worker));
|
|
1595
|
+
const summary = `Copied ${copied} ${copied === 1 ? noun.singular : noun.plural}`;
|
|
1596
|
+
if (failures.length === 0) term.out(summary);
|
|
1597
|
+
else term.out(`${summary}, failed ${failures.length}: ${failures.join(", ")}`);
|
|
1598
|
+
}
|
|
1599
|
+
function formatRow(row, width) {
|
|
1600
|
+
const room = width - stripVTControlCharacters(row.fixedColumns).length - 2;
|
|
1601
|
+
const text = room > 0 ? truncate(row.freeText, room) : "";
|
|
1602
|
+
const columns = hyperlink(row.id, row.url) + row.fixedColumns.slice(row.id.length);
|
|
1603
|
+
return text ? `${columns} ${hyperlink(text, row.url)}` : columns;
|
|
1604
|
+
}
|
|
1605
|
+
function truncate(text, max) {
|
|
1606
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
1607
|
+
return clean.length <= max ? clean : `${clean.slice(0, Math.max(0, max - 3))}...`;
|
|
1608
|
+
}
|
|
1248
1609
|
//#endregion
|
|
1249
|
-
//#region src/
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
};
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1610
|
+
//#region src/commands/bitbucket-prs.ts
|
|
1611
|
+
async function bitbucketPrs({ session, term, profile }, options) {
|
|
1612
|
+
if (options.query && (options.author || options.reviewer)) throw new Error("--query cannot be combined with --author or --reviewer.");
|
|
1613
|
+
const states = pullRequestStates(options);
|
|
1614
|
+
const ref = resolveRepo(options.repo, session);
|
|
1615
|
+
const limit = parseLimit(options.limit);
|
|
1616
|
+
const query = pullRequestQuery({
|
|
1617
|
+
...await resolvePrincipals(session, profile, options),
|
|
1618
|
+
query: options.query
|
|
1619
|
+
});
|
|
1620
|
+
const prs = await withScopeHint(PULL_REQUEST_SCOPE, () => listPullRequests(session, ref, {
|
|
1621
|
+
limit,
|
|
1622
|
+
states,
|
|
1623
|
+
query
|
|
1624
|
+
}));
|
|
1625
|
+
writeRows(term, pullRequestRows(prs, Date.now()), {
|
|
1626
|
+
json: options.json,
|
|
1627
|
+
empty: emptyMessage(options),
|
|
1628
|
+
footer: prs.length === limit ? searchFooter(limit) : void 0
|
|
1266
1629
|
});
|
|
1267
1630
|
}
|
|
1268
|
-
function
|
|
1269
|
-
|
|
1631
|
+
function pullRequestStates(options) {
|
|
1632
|
+
if (!options.all) return parsePullRequestStates(options.state);
|
|
1633
|
+
if (options.state) throw new Error("--all cannot be combined with --state.");
|
|
1634
|
+
return allPullRequestStates();
|
|
1270
1635
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1636
|
+
function pullRequestRows(prs, nowMs) {
|
|
1637
|
+
return alignedRows(prs, nowMs, (pr) => {
|
|
1638
|
+
const label = pr.draft ? "DRAFT" : pr.state || "-";
|
|
1639
|
+
return {
|
|
1640
|
+
id: `#${pr.id}`,
|
|
1641
|
+
url: pr.url,
|
|
1642
|
+
label,
|
|
1643
|
+
color: colorForBitbucketState(label),
|
|
1644
|
+
text: pr.title,
|
|
1645
|
+
timestamp: pr.updatedOn
|
|
1646
|
+
};
|
|
1279
1647
|
});
|
|
1280
|
-
|
|
1281
|
-
|
|
1648
|
+
}
|
|
1649
|
+
async function resolvePrincipals(session, profile, options) {
|
|
1650
|
+
const mine = isMe(options.author) || isMe(options.reviewer) ? await currentUuid(session, profile) : void 0;
|
|
1282
1651
|
return {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
space: r.space?.key ?? r.resultGlobalContainer?.title ?? "",
|
|
1286
|
-
title: decodeEntities(r.content?.title ?? r.title ?? ""),
|
|
1287
|
-
updated: r.lastModified ?? "",
|
|
1288
|
-
url: r.url ? `${site}/wiki${r.url}` : ""
|
|
1289
|
-
})),
|
|
1290
|
-
hasMore: serverPageWasFull
|
|
1652
|
+
author: isMe(options.author) ? mine : options.author,
|
|
1653
|
+
reviewer: isMe(options.reviewer) ? mine : options.reviewer
|
|
1291
1654
|
};
|
|
1292
1655
|
}
|
|
1293
|
-
function
|
|
1294
|
-
|
|
1295
|
-
const clauses = ["type = page"];
|
|
1296
|
-
if (params.starred) clauses.push("favourite = currentUser()");
|
|
1297
|
-
if (params.space) clauses.push(`space = ${cqlStringLiteral(params.space)}`);
|
|
1298
|
-
if (params.text) clauses.push(`text ~ ${cqlStringLiteral(params.text)}`);
|
|
1299
|
-
return `${clauses.join(" AND ")} ORDER BY lastmodified DESC`;
|
|
1656
|
+
function isMe(value) {
|
|
1657
|
+
return value?.toLowerCase() === "me";
|
|
1300
1658
|
}
|
|
1301
|
-
function
|
|
1302
|
-
|
|
1659
|
+
async function currentUuid(session, profile) {
|
|
1660
|
+
if (session.uuid) return session.uuid;
|
|
1661
|
+
const uuid = await withScopeHint(ACCOUNT_SCOPE, () => fetchCurrentUserUuid(session));
|
|
1662
|
+
await rememberBitbucketUuid(profile, uuid);
|
|
1663
|
+
return uuid;
|
|
1303
1664
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
async function resolveRef(arg, kind) {
|
|
1307
|
-
const raw = arg ?? await input({
|
|
1308
|
-
message: kind.message,
|
|
1309
|
-
required: true
|
|
1310
|
-
});
|
|
1311
|
-
const ref = kind.parse(raw);
|
|
1312
|
-
if (!ref) throw new Error(kind.notFound(raw));
|
|
1313
|
-
return ref;
|
|
1665
|
+
function emptyMessage(options) {
|
|
1666
|
+
return options.author || options.reviewer || options.query || options.state || options.all ? "No matching pull requests." : "No open pull requests.";
|
|
1314
1667
|
}
|
|
1315
1668
|
//#endregion
|
|
1316
|
-
//#region src/
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
return `[embedded media: ${alt || attrs.id || "media"}]`;
|
|
1323
|
-
}
|
|
1324
|
-
function renderInline(nodes, ctx) {
|
|
1325
|
-
return nodes.map((n) => renderInlineNode(n, ctx)).join("");
|
|
1669
|
+
//#region src/api/bitbucket-pipelines.ts
|
|
1670
|
+
const BUILD_NUMBER_SCAN_LIMIT = 1e3;
|
|
1671
|
+
function pipelineStatus(state) {
|
|
1672
|
+
if (!state) return "";
|
|
1673
|
+
if (state.name === "COMPLETED") return state.result?.name ?? "COMPLETED";
|
|
1674
|
+
return state.name ?? "";
|
|
1326
1675
|
}
|
|
1327
|
-
function
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1676
|
+
function pipelinesQuery(limit) {
|
|
1677
|
+
return new URLSearchParams({
|
|
1678
|
+
sort: "-created_on",
|
|
1679
|
+
pagelen: String(Math.min(Math.max(limit, 1), 100))
|
|
1680
|
+
}).toString();
|
|
1681
|
+
}
|
|
1682
|
+
function wallClockSeconds(startOn, endOn) {
|
|
1683
|
+
if (!startOn || !endOn) return null;
|
|
1684
|
+
const start = Date.parse(startOn);
|
|
1685
|
+
const end = Date.parse(endOn);
|
|
1686
|
+
if (Number.isNaN(start) || Number.isNaN(end)) return null;
|
|
1687
|
+
return Math.floor((end - start) / 1e3);
|
|
1688
|
+
}
|
|
1689
|
+
function shortHash(hash) {
|
|
1690
|
+
return hash?.slice(0, 7) ?? "";
|
|
1691
|
+
}
|
|
1692
|
+
function pipelineUrl(ref, buildNumber) {
|
|
1693
|
+
return `${BITBUCKET_WEB_ORIGIN}/${ref.workspace}/${ref.repo}/pipelines/results/${buildNumber}`;
|
|
1694
|
+
}
|
|
1695
|
+
function toPipelineSummary(ref, p) {
|
|
1696
|
+
return {
|
|
1697
|
+
buildNumber: p.build_number,
|
|
1698
|
+
status: pipelineStatus(p.state),
|
|
1699
|
+
ref: p.target?.ref_name ?? "",
|
|
1700
|
+
commit: shortHash(p.target?.commit?.hash),
|
|
1701
|
+
durationSeconds: wallClockSeconds(p.created_on, p.completed_on),
|
|
1702
|
+
createdOn: p.created_on ?? "",
|
|
1703
|
+
creator: p.creator?.display_name ?? "",
|
|
1704
|
+
uuid: p.uuid,
|
|
1705
|
+
url: pipelineUrl(ref, p.build_number)
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function toPipelineDetail(ref, p) {
|
|
1709
|
+
return {
|
|
1710
|
+
...toPipelineSummary(ref, p),
|
|
1711
|
+
repo: p.repository?.full_name ?? "",
|
|
1712
|
+
trigger: p.trigger?.name ?? ""
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function toStepSummary(step) {
|
|
1716
|
+
return {
|
|
1717
|
+
name: step.name ?? "",
|
|
1718
|
+
status: pipelineStatus(step.state),
|
|
1719
|
+
durationSeconds: wallClockSeconds(step.started_on, step.completed_on)
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function pipelinesPath(ref) {
|
|
1723
|
+
return repoPath(ref, "pipelines");
|
|
1724
|
+
}
|
|
1725
|
+
async function listPipelines(client, ref, limit) {
|
|
1726
|
+
return collectPages(client, `${pipelinesPath(ref)}?${pipelinesQuery(limit)}`, limit, (value) => toPipelineSummary(ref, value));
|
|
1727
|
+
}
|
|
1728
|
+
async function getPipeline(client, ref, buildNumber) {
|
|
1729
|
+
const found = await fetchByBuildNumber(client, ref, buildNumber) ?? await findInRecentRuns(client, ref, buildNumber);
|
|
1730
|
+
if (!found) throw new Error(`Could not find pipeline #${buildNumber} in the ${BUILD_NUMBER_SCAN_LIMIT} most recent runs. It may be too old.`);
|
|
1731
|
+
return toPipelineDetail(ref, found);
|
|
1732
|
+
}
|
|
1733
|
+
async function fetchByBuildNumber(client, ref, buildNumber) {
|
|
1734
|
+
try {
|
|
1735
|
+
return await client.getJson(`${pipelinesPath(ref)}/${encodeURIComponent(String(buildNumber))}`);
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
if (err instanceof HttpError && (err.status === 400 || err.status === 404)) return null;
|
|
1738
|
+
throw err;
|
|
1347
1739
|
}
|
|
1348
1740
|
}
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1351
|
-
let
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
out = `\`${out}\``;
|
|
1356
|
-
break;
|
|
1357
|
-
case "strong":
|
|
1358
|
-
out = `**${out}**`;
|
|
1359
|
-
break;
|
|
1360
|
-
case "em":
|
|
1361
|
-
out = `*${out}*`;
|
|
1362
|
-
break;
|
|
1363
|
-
case "strike":
|
|
1364
|
-
out = `~~${out}~~`;
|
|
1365
|
-
break;
|
|
1366
|
-
case "link": {
|
|
1367
|
-
const value = mark.attrs?.["href"];
|
|
1368
|
-
if (typeof value === "string") href = value;
|
|
1369
|
-
break;
|
|
1370
|
-
}
|
|
1371
|
-
default: break;
|
|
1741
|
+
async function findInRecentRuns(client, ref, buildNumber) {
|
|
1742
|
+
const first = `${pipelinesPath(ref)}?${pipelinesQuery(100)}`;
|
|
1743
|
+
let scanned = 0;
|
|
1744
|
+
for await (const pipeline of walkPages(client, first)) {
|
|
1745
|
+
if (pipeline.build_number === buildNumber) return pipeline;
|
|
1746
|
+
if (++scanned >= BUILD_NUMBER_SCAN_LIMIT) break;
|
|
1372
1747
|
}
|
|
1373
|
-
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
async function listSteps(client, ref, pipelineId) {
|
|
1751
|
+
const out = [];
|
|
1752
|
+
const first = `${pipelinesPath(ref)}/${encodeURIComponent(pipelineId)}/steps`;
|
|
1753
|
+
for await (const step of walkPages(client, first)) out.push(toStepSummary(step));
|
|
1374
1754
|
return out;
|
|
1375
1755
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1756
|
+
//#endregion
|
|
1757
|
+
//#region src/commands/bitbucket.ts
|
|
1758
|
+
async function bitbucketPipelines({ session, term }, options) {
|
|
1759
|
+
const ref = resolveRepo(options.repo, session);
|
|
1760
|
+
const limit = parseLimit(options.limit);
|
|
1761
|
+
writeRows(term, pipelineRows(await withScopeHint(PIPELINE_SCOPE, () => listPipelines(session, ref, limit)), Date.now()), {
|
|
1762
|
+
json: options.json,
|
|
1763
|
+
empty: "No pipelines found."
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
async function bitbucketPipeline({ session, term }, arg, options) {
|
|
1767
|
+
const buildNumber = parseBuildNumber(arg);
|
|
1768
|
+
const ref = resolveRepo(options.repo, session);
|
|
1769
|
+
const detail = await withScopeHint(PIPELINE_SCOPE, () => getPipeline(session, ref, buildNumber));
|
|
1770
|
+
const steps = await withScopeHint(PIPELINE_SCOPE, () => listSteps(session, ref, detail.uuid));
|
|
1771
|
+
term.out(formatPipelineDetail(detail, steps, Date.now()));
|
|
1772
|
+
}
|
|
1773
|
+
function pipelineRows(pipelines, nowMs) {
|
|
1774
|
+
return alignedRows(pipelines, nowMs, (p) => ({
|
|
1775
|
+
id: `#${p.buildNumber}`,
|
|
1776
|
+
url: p.url,
|
|
1777
|
+
label: p.status || "-",
|
|
1778
|
+
color: colorForBitbucketState(p.status),
|
|
1779
|
+
text: `${refWithCommit(p)} · ${formatDuration(p.durationSeconds)}`,
|
|
1780
|
+
timestamp: p.createdOn
|
|
1781
|
+
}));
|
|
1782
|
+
}
|
|
1783
|
+
function formatStepRows(steps) {
|
|
1784
|
+
const rows = steps.map((s) => ({
|
|
1785
|
+
name: s.name || "-",
|
|
1786
|
+
status: s.status || "-",
|
|
1787
|
+
dur: formatDuration(s.durationSeconds)
|
|
1788
|
+
}));
|
|
1789
|
+
const wn = Math.max(...rows.map((r) => r.name.length));
|
|
1790
|
+
const ws = Math.max(...rows.map((r) => r.status.length));
|
|
1791
|
+
return rows.map((r) => ` ${r.name.padEnd(wn)} ${r.status.padEnd(ws)} ${r.dur}`);
|
|
1792
|
+
}
|
|
1793
|
+
function formatPipelineDetail(detail, steps, nowMs) {
|
|
1794
|
+
const by = detail.creator ? ` by ${detail.creator}` : "";
|
|
1795
|
+
return [
|
|
1796
|
+
`Pipeline #${detail.buildNumber} ${detail.status || "-"}`,
|
|
1797
|
+
...detail.repo ? [`Repo: ${detail.repo}`] : [],
|
|
1798
|
+
`Ref: ${refWithCommit(detail)}`,
|
|
1799
|
+
...detail.trigger ? [`Trigger: ${detail.trigger}`] : [],
|
|
1800
|
+
`Duration: ${formatDuration(detail.durationSeconds)}`,
|
|
1801
|
+
`Created: ${relativeTime(detail.createdOn, nowMs)}${by}`,
|
|
1802
|
+
...steps.length > 0 ? [
|
|
1803
|
+
"",
|
|
1804
|
+
"Steps:",
|
|
1805
|
+
...formatStepRows(steps)
|
|
1806
|
+
] : []
|
|
1807
|
+
];
|
|
1808
|
+
}
|
|
1809
|
+
function refWithCommit(pipeline) {
|
|
1810
|
+
if (!pipeline.ref) return pipeline.commit || "-";
|
|
1811
|
+
return pipeline.commit ? `${pipeline.ref} (${pipeline.commit})` : pipeline.ref;
|
|
1812
|
+
}
|
|
1813
|
+
function parseBuildNumber(arg) {
|
|
1814
|
+
const raw = (arg ?? "").replace(/^#/, "").trim();
|
|
1815
|
+
if (!/^\d+$/.test(raw)) throw new Error(`Invalid pipeline number "${arg ?? ""}". Expected a build number, e.g. 123.`);
|
|
1816
|
+
return Number.parseInt(raw, 10);
|
|
1817
|
+
}
|
|
1818
|
+
//#endregion
|
|
1819
|
+
//#region src/cli/bitbucket.ts
|
|
1820
|
+
function registerBitbucket(bitbucket) {
|
|
1821
|
+
bitbucket.description("Bitbucket commands");
|
|
1822
|
+
bitbucket.command("login").description("Store Bitbucket workspace and API token").action(bareAction(bitbucketLogin));
|
|
1823
|
+
bitbucket.command("logout").description("Remove stored Bitbucket credentials").action(bareAction(bitbucketLogout));
|
|
1824
|
+
bitbucket.command("status").description("Show the current Bitbucket login").action(bareAction(bitbucketStatus));
|
|
1825
|
+
bitbucket.command("pipelines").description("List recent pipeline runs for a repo").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(bitbucketAction(bitbucketPipelines));
|
|
1826
|
+
bitbucket.command("prs").description("List pull requests for a repo").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("-s, --state <state...>", "open, merged, declined, or superseded").option("--all", "include every state").option("-a, --author <who>", "limit to an author ('me', an account id, or a uuid)").option("--reviewer <who>", "limit to a reviewer ('me', an account id, or a uuid)").option("-q, --query <bbql>", "raw Bitbucket query (not for use with --author or --reviewer)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(bitbucketAction(bitbucketPrs));
|
|
1827
|
+
bitbucket.command("pr <id>").description("Show one pull request, its reviewers, files, and comments").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").option("--json", "output the pull request as JSON").action(bitbucketAction(bitbucketPr));
|
|
1828
|
+
bitbucket.command("pipeline <number>").description("Show one pipeline run and its steps").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").action(bitbucketAction(bitbucketPipeline));
|
|
1829
|
+
return bitbucket;
|
|
1830
|
+
}
|
|
1831
|
+
//#endregion
|
|
1832
|
+
//#region src/adf/from-markdown.ts
|
|
1833
|
+
function mediaNode(source, alt) {
|
|
1834
|
+
const attrs = { ...source };
|
|
1835
|
+
if (alt) attrs["alt"] = alt;
|
|
1836
|
+
return {
|
|
1837
|
+
type: "mediaSingle",
|
|
1838
|
+
attrs: { layout: "center" },
|
|
1839
|
+
content: [{
|
|
1840
|
+
type: "media",
|
|
1841
|
+
attrs
|
|
1842
|
+
}]
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function imageHrefs(md) {
|
|
1846
|
+
const hrefs = /* @__PURE__ */ new Set();
|
|
1847
|
+
marked.walkTokens(marked.lexer(md), (token) => {
|
|
1848
|
+
if (token.type === "image") hrefs.add(token.href);
|
|
1849
|
+
});
|
|
1850
|
+
return [...hrefs];
|
|
1851
|
+
}
|
|
1852
|
+
function markdownToAdf(md, options = {}) {
|
|
1853
|
+
const ctx = { resolveImage: options.resolveImage ?? externalMedia };
|
|
1854
|
+
return {
|
|
1855
|
+
type: "doc",
|
|
1856
|
+
version: 1,
|
|
1857
|
+
content: blocks(marked.lexer(md), ctx)
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
function blocks(tokens, ctx) {
|
|
1861
|
+
const out = [];
|
|
1862
|
+
for (const token of tokens) out.push(...block(token, ctx));
|
|
1863
|
+
return out;
|
|
1864
|
+
}
|
|
1865
|
+
function block(token, ctx) {
|
|
1866
|
+
switch (token.type) {
|
|
1867
|
+
case "space": return [];
|
|
1868
|
+
case "heading": return [{
|
|
1869
|
+
type: "heading",
|
|
1870
|
+
attrs: { level: clampLevel(token.depth) },
|
|
1871
|
+
content: inline(token.tokens ?? [], ctx)
|
|
1872
|
+
}];
|
|
1873
|
+
case "paragraph": return paragraphsSplitAtImages(token.tokens ?? [], ctx);
|
|
1874
|
+
case "text": return paragraphsSplitAtImages(token.tokens ?? [textToken(token.text)], ctx);
|
|
1875
|
+
case "list": return [list(token, ctx)];
|
|
1876
|
+
case "code": return [codeBlock(token)];
|
|
1877
|
+
case "blockquote": return [{
|
|
1878
|
+
type: "blockquote",
|
|
1879
|
+
content: blocks(token.tokens ?? [], ctx)
|
|
1880
|
+
}];
|
|
1881
|
+
case "table": return [table(token, ctx)];
|
|
1882
|
+
case "hr": return [{ type: "rule" }];
|
|
1883
|
+
default: return [];
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function paragraphsSplitAtImages(tokens, ctx) {
|
|
1887
|
+
const out = [];
|
|
1888
|
+
let buffer = [];
|
|
1889
|
+
const flush = () => {
|
|
1890
|
+
if (buffer.length === 0) return;
|
|
1891
|
+
const nodes = inline(buffer, ctx);
|
|
1892
|
+
if (nodes.length > 0) out.push({
|
|
1893
|
+
type: "paragraph",
|
|
1894
|
+
content: nodes
|
|
1895
|
+
});
|
|
1896
|
+
buffer = [];
|
|
1897
|
+
};
|
|
1898
|
+
for (const token of tokens) if (token.type === "image") {
|
|
1899
|
+
flush();
|
|
1900
|
+
const image = token;
|
|
1901
|
+
const node = ctx.resolveImage(image.href, image.text ?? "");
|
|
1902
|
+
if (node) out.push(node);
|
|
1903
|
+
} else buffer.push(token);
|
|
1904
|
+
flush();
|
|
1905
|
+
return out;
|
|
1906
|
+
}
|
|
1907
|
+
function list(token, ctx) {
|
|
1908
|
+
if (token.items.length > 0 && token.items.every((i) => i.task)) return {
|
|
1909
|
+
type: "taskList",
|
|
1910
|
+
attrs: { localId: randomUUID() },
|
|
1911
|
+
content: token.items.map((item) => ({
|
|
1912
|
+
type: "taskItem",
|
|
1913
|
+
attrs: {
|
|
1914
|
+
localId: randomUUID(),
|
|
1915
|
+
state: item.checked ? "DONE" : "TODO"
|
|
1916
|
+
},
|
|
1917
|
+
content: inline(taskItemInline(item), ctx)
|
|
1918
|
+
}))
|
|
1919
|
+
};
|
|
1920
|
+
const node = {
|
|
1921
|
+
type: token.ordered ? "orderedList" : "bulletList",
|
|
1922
|
+
content: token.items.map((item) => listItem(item, ctx))
|
|
1923
|
+
};
|
|
1924
|
+
const start = Number(token.start);
|
|
1925
|
+
if (token.ordered && Number.isFinite(start) && start !== 1) node.attrs = { order: start };
|
|
1926
|
+
return node;
|
|
1927
|
+
}
|
|
1928
|
+
function listItem(item, ctx) {
|
|
1929
|
+
const children = blocks(item.tokens.filter((t) => t.type !== "checkbox"), ctx);
|
|
1930
|
+
return {
|
|
1931
|
+
type: "listItem",
|
|
1932
|
+
content: children.length > 0 ? children : [emptyParagraph()]
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function emptyParagraph() {
|
|
1936
|
+
return {
|
|
1937
|
+
type: "paragraph",
|
|
1938
|
+
content: []
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
function taskItemInline(item) {
|
|
1942
|
+
const first = item.tokens.find((t) => t.type === "text");
|
|
1943
|
+
if (first && "tokens" in first && first.tokens) return first.tokens;
|
|
1944
|
+
return [textToken(item.text)];
|
|
1945
|
+
}
|
|
1946
|
+
function codeBlock(token) {
|
|
1947
|
+
return {
|
|
1948
|
+
type: "codeBlock",
|
|
1949
|
+
attrs: token.lang ? { language: token.lang } : {},
|
|
1950
|
+
content: token.text.length > 0 ? [{
|
|
1951
|
+
type: "text",
|
|
1952
|
+
text: token.text
|
|
1953
|
+
}] : []
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function table(token, ctx) {
|
|
1957
|
+
return {
|
|
1958
|
+
type: "table",
|
|
1959
|
+
content: [{
|
|
1960
|
+
type: "tableRow",
|
|
1961
|
+
content: token.header.map((cell) => tableCell(cell, "tableHeader", ctx))
|
|
1962
|
+
}, ...token.rows.map((row) => ({
|
|
1963
|
+
type: "tableRow",
|
|
1964
|
+
content: row.map((cell) => tableCell(cell, "tableCell", ctx))
|
|
1965
|
+
}))]
|
|
1966
|
+
};
|
|
1380
1967
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1968
|
+
function tableCell(cell, type, ctx) {
|
|
1969
|
+
return {
|
|
1970
|
+
type,
|
|
1971
|
+
content: [{
|
|
1972
|
+
type: "paragraph",
|
|
1973
|
+
content: inline(cell.tokens, ctx)
|
|
1974
|
+
}]
|
|
1975
|
+
};
|
|
1387
1976
|
}
|
|
1388
|
-
function
|
|
1389
|
-
|
|
1977
|
+
function inline(tokens, ctx, marks = []) {
|
|
1978
|
+
const out = [];
|
|
1979
|
+
for (const token of tokens) out.push(...inlineNode(token, ctx, marks));
|
|
1980
|
+
return out;
|
|
1390
1981
|
}
|
|
1391
|
-
function
|
|
1392
|
-
switch (
|
|
1393
|
-
case "
|
|
1394
|
-
case "
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1982
|
+
function inlineNode(token, ctx, marks) {
|
|
1983
|
+
switch (token.type) {
|
|
1984
|
+
case "text":
|
|
1985
|
+
case "escape": {
|
|
1986
|
+
const t = token;
|
|
1987
|
+
if ("tokens" in t && t.tokens?.length) return inline(t.tokens, ctx, marks);
|
|
1988
|
+
return textNode(t.text, marks);
|
|
1397
1989
|
}
|
|
1398
|
-
case "
|
|
1399
|
-
case "
|
|
1400
|
-
case "
|
|
1401
|
-
case "
|
|
1402
|
-
case "
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
case "
|
|
1410
|
-
case "
|
|
1411
|
-
|
|
1412
|
-
default:
|
|
1413
|
-
if (node.content?.length) return renderBlocks(node.content, ctx, indent);
|
|
1414
|
-
return node.text ? indent + node.text : "";
|
|
1990
|
+
case "strong": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strong" }));
|
|
1991
|
+
case "em": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "em" }));
|
|
1992
|
+
case "del": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strike" }));
|
|
1993
|
+
case "codespan": return textNode(token.text, withUniqueMark(marks, { type: "code" }));
|
|
1994
|
+
case "link": {
|
|
1995
|
+
const link = token;
|
|
1996
|
+
return inline(link.tokens, ctx, withUniqueMark(marks, {
|
|
1997
|
+
type: "link",
|
|
1998
|
+
attrs: { href: link.href }
|
|
1999
|
+
}));
|
|
2000
|
+
}
|
|
2001
|
+
case "br": return [{ type: "hardBreak" }];
|
|
2002
|
+
case "html": return textNode(token.text, marks);
|
|
2003
|
+
default: return "text" in token && token.text ? textNode(token.text, marks) : [];
|
|
1415
2004
|
}
|
|
1416
2005
|
}
|
|
1417
|
-
function
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
return (item.content ?? []).map((block, i) => i === 0 ? `${indent}${marker}${renderBlock(block, ctx, "")}` : renderBlock(block, ctx, continuationIndent)).join("\n");
|
|
2006
|
+
function textNode(text, marks) {
|
|
2007
|
+
if (text.length === 0) return [];
|
|
2008
|
+
const node = {
|
|
2009
|
+
type: "text",
|
|
2010
|
+
text
|
|
2011
|
+
};
|
|
2012
|
+
if (marks.length > 0) node.marks = marks;
|
|
2013
|
+
return [node];
|
|
1426
2014
|
}
|
|
1427
|
-
function
|
|
1428
|
-
return
|
|
1429
|
-
return `${indent}- [${item.attrs?.["state"] === "DONE" ? "x" : " "}] ${renderInline(item.content ?? [], ctx)}`;
|
|
1430
|
-
}).join("\n");
|
|
2015
|
+
function withUniqueMark(marks, mark) {
|
|
2016
|
+
return [...marks.filter((m) => m.type !== mark.type), mark];
|
|
1431
2017
|
}
|
|
1432
|
-
function
|
|
1433
|
-
return (
|
|
2018
|
+
function externalMedia(href, alt) {
|
|
2019
|
+
return mediaNode({
|
|
2020
|
+
type: "external",
|
|
2021
|
+
url: href
|
|
2022
|
+
}, alt);
|
|
1434
2023
|
}
|
|
1435
|
-
function
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
2024
|
+
function textToken(text) {
|
|
2025
|
+
return {
|
|
2026
|
+
type: "text",
|
|
2027
|
+
raw: text,
|
|
2028
|
+
text,
|
|
2029
|
+
escaped: false
|
|
2030
|
+
};
|
|
1440
2031
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
note: "Note",
|
|
1444
|
-
warning: "Warning",
|
|
1445
|
-
success: "Success",
|
|
1446
|
-
error: "Error"
|
|
1447
|
-
};
|
|
1448
|
-
function renderPanel(node, ctx, indent) {
|
|
1449
|
-
return prefixLines(`**${PANEL_LABELS[typeof node.attrs?.["panelType"] === "string" ? node.attrs["panelType"] : "info"] ?? "Note"}**\n\n${renderBlocks(node.content ?? [], ctx, "")}`, `${indent}> `);
|
|
2032
|
+
function clampLevel(value) {
|
|
2033
|
+
return Math.min(6, Math.max(1, Math.trunc(value) || 1));
|
|
1450
2034
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
2035
|
+
//#endregion
|
|
2036
|
+
//#region src/api/confluence-attachments.ts
|
|
2037
|
+
const UNKNOWN_SIZE = -1;
|
|
2038
|
+
async function listAttachments(client, id) {
|
|
2039
|
+
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.map((a) => ({
|
|
2040
|
+
filename: a.title ?? a.id,
|
|
2041
|
+
fileId: mediaNodeId(a),
|
|
2042
|
+
size: typeof a.fileSize === "number" ? a.fileSize : UNKNOWN_SIZE
|
|
2043
|
+
}));
|
|
1453
2044
|
}
|
|
1454
|
-
function
|
|
1455
|
-
|
|
1456
|
-
if (rows.length === 0) return "";
|
|
1457
|
-
const grid = rows.map((row) => (row.content ?? []).map((cell) => renderCellOnOneLine(cell, ctx)));
|
|
1458
|
-
const cols = Math.max(...grid.map((r) => r.length));
|
|
1459
|
-
const pad = (r) => {
|
|
1460
|
-
const copy = [...r];
|
|
1461
|
-
while (copy.length < cols) copy.push("");
|
|
1462
|
-
return copy;
|
|
1463
|
-
};
|
|
1464
|
-
const header = pad(grid[0] ?? []);
|
|
1465
|
-
const lines = [`| ${header.join(" | ")} |`, `| ${header.map(() => "---").join(" | ")} |`];
|
|
1466
|
-
for (const row of grid.slice(1)) lines.push(`| ${pad(row).join(" | ")} |`);
|
|
1467
|
-
return lines.join("\n");
|
|
2045
|
+
async function uploadAttachment(client, pageId, filename, bytes) {
|
|
2046
|
+
return (await client.postMultipart(`/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment`, filename, bytes)).results?.[0]?.extensions?.fileId ?? await fileIdByListing(client, pageId, filename);
|
|
1468
2047
|
}
|
|
1469
|
-
function
|
|
1470
|
-
|
|
2048
|
+
async function fileIdByListing(client, pageId, filename) {
|
|
2049
|
+
const match = (await listAttachments(client, pageId)).find((a) => a.filename === filename);
|
|
2050
|
+
if (match) return match.fileId;
|
|
2051
|
+
throw new Error(`Upload of "${filename}" did not return a fileId.`);
|
|
1471
2052
|
}
|
|
1472
|
-
function
|
|
1473
|
-
return
|
|
2053
|
+
async function fetchAttachments(client, id) {
|
|
2054
|
+
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.filter((a) => a.downloadLink).map((a) => ({
|
|
2055
|
+
mediaId: mediaNodeId(a),
|
|
2056
|
+
filename: a.title ?? a.id,
|
|
2057
|
+
url: withWikiContextPath(a.downloadLink ?? "")
|
|
2058
|
+
}));
|
|
1474
2059
|
}
|
|
1475
|
-
function
|
|
1476
|
-
|
|
1477
|
-
return Math.min(6, Math.max(1, n));
|
|
2060
|
+
function mediaNodeId(a) {
|
|
2061
|
+
return a.fileId ?? a.id;
|
|
1478
2062
|
}
|
|
1479
|
-
function
|
|
1480
|
-
|
|
2063
|
+
function withWikiContextPath(link) {
|
|
2064
|
+
if (link.startsWith("http") || link.startsWith("/wiki")) return link;
|
|
2065
|
+
return `/wiki${link}`;
|
|
1481
2066
|
}
|
|
1482
2067
|
//#endregion
|
|
1483
|
-
//#region src/
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
const
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
comment: kleur.dim,
|
|
1508
|
-
quote: kleur.dim,
|
|
1509
|
-
deletion: kleur.dim,
|
|
1510
|
-
title: kleur.cyan,
|
|
1511
|
-
type: kleur.cyan,
|
|
1512
|
-
section: kleur.cyan
|
|
1513
|
-
};
|
|
1514
|
-
const lowlight = createLowlight(common);
|
|
1515
|
-
function highlightMarkdown(md) {
|
|
1516
|
-
const lines = md.split("\n");
|
|
1517
|
-
const out = [];
|
|
1518
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1519
|
-
const line = lines[i] ?? "";
|
|
1520
|
-
const fence = line.match(FENCE_OPEN);
|
|
1521
|
-
if (!fence) {
|
|
1522
|
-
out.push(highlightLine(line, lines[i + 1]));
|
|
1523
|
-
continue;
|
|
1524
|
-
}
|
|
1525
|
-
const [, prefix = "", lang = ""] = fence;
|
|
1526
|
-
const styledPrefix = prefix.replace(/\S+/g, (marker) => kleur.dim(marker));
|
|
1527
|
-
out.push(`${styledPrefix}${kleur.dim(`${FENCE}${lang}`)}`);
|
|
1528
|
-
const code = [];
|
|
1529
|
-
const prefixed = [];
|
|
1530
|
-
let j = i + 1;
|
|
1531
|
-
for (; j < lines.length; j++) {
|
|
1532
|
-
const raw = lines[j] ?? "";
|
|
1533
|
-
const hasPrefix = raw.startsWith(prefix) || raw === prefix.trimEnd();
|
|
1534
|
-
const body = raw.startsWith(prefix) ? raw.slice(prefix.length) : hasPrefix ? "" : raw;
|
|
1535
|
-
if (body === FENCE) break;
|
|
1536
|
-
code.push(body);
|
|
1537
|
-
prefixed.push(hasPrefix);
|
|
1538
|
-
}
|
|
1539
|
-
if (code.length > 0) highlightCode(code.join("\n"), lang).split("\n").forEach((codeLine, k) => {
|
|
1540
|
-
const lead = prefixed[k] ? styledPrefix : "";
|
|
1541
|
-
out.push(codeLine ? `${lead}${codeLine}` : lead.trimEnd());
|
|
1542
|
-
});
|
|
1543
|
-
if (j < lines.length) {
|
|
1544
|
-
const lead = (lines[j] ?? "").startsWith(prefix) ? styledPrefix : "";
|
|
1545
|
-
out.push(`${lead}${kleur.dim(FENCE)}`);
|
|
1546
|
-
}
|
|
1547
|
-
i = j;
|
|
1548
|
-
}
|
|
1549
|
-
return out.join("\n");
|
|
2068
|
+
//#region src/api/confluence-pages.ts
|
|
2069
|
+
async function fetchPage(client, site, id) {
|
|
2070
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
2071
|
+
const names = new UserNames(client);
|
|
2072
|
+
const [spaceKey, attachments, comments, author] = await Promise.all([
|
|
2073
|
+
fetchSpaceKey(client, page.spaceId),
|
|
2074
|
+
fetchAttachments(client, id),
|
|
2075
|
+
fetchComments$1(client, id, names),
|
|
2076
|
+
names.resolve(page.version?.authorId ?? page.authorId)
|
|
2077
|
+
]);
|
|
2078
|
+
const webui = page._links?.webui ?? "";
|
|
2079
|
+
return {
|
|
2080
|
+
id: page.id,
|
|
2081
|
+
title: page.title,
|
|
2082
|
+
spaceKey,
|
|
2083
|
+
version: page.version?.number ?? 0,
|
|
2084
|
+
author,
|
|
2085
|
+
createdAt: page.createdAt ?? "",
|
|
2086
|
+
updatedAt: page.version?.createdAt ?? "",
|
|
2087
|
+
url: webui ? `${site}/wiki${webui}` : "",
|
|
2088
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
2089
|
+
attachments,
|
|
2090
|
+
comments
|
|
2091
|
+
};
|
|
1550
2092
|
}
|
|
1551
|
-
function
|
|
1552
|
-
|
|
1553
|
-
return
|
|
2093
|
+
async function fetchPageState(client, id) {
|
|
2094
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
2095
|
+
return {
|
|
2096
|
+
version: page.version?.number ?? 0,
|
|
2097
|
+
title: page.title,
|
|
2098
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value)
|
|
2099
|
+
};
|
|
1554
2100
|
}
|
|
1555
|
-
function
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
2101
|
+
async function updatePage(client, id, params) {
|
|
2102
|
+
return (await client.putJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}`, {
|
|
2103
|
+
id,
|
|
2104
|
+
status: "current",
|
|
2105
|
+
title: params.title,
|
|
2106
|
+
body: {
|
|
2107
|
+
representation: "atlas_doc_format",
|
|
2108
|
+
value: JSON.stringify(params.body)
|
|
2109
|
+
},
|
|
2110
|
+
version: {
|
|
2111
|
+
number: params.nextVersion,
|
|
2112
|
+
message: params.message
|
|
2113
|
+
}
|
|
2114
|
+
})).version?.number ?? params.nextVersion;
|
|
1561
2115
|
}
|
|
1562
|
-
function
|
|
1563
|
-
if (!
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2116
|
+
async function fetchSpaceKey(client, spaceId) {
|
|
2117
|
+
if (!spaceId) return "";
|
|
2118
|
+
try {
|
|
2119
|
+
return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
|
|
2120
|
+
} catch {
|
|
2121
|
+
return "";
|
|
1568
2122
|
}
|
|
1569
2123
|
}
|
|
1570
|
-
function
|
|
1571
|
-
|
|
2124
|
+
async function fetchComments$1(client, id, names) {
|
|
2125
|
+
const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
|
|
2126
|
+
return Promise.all(res.results.map(async (c) => ({
|
|
2127
|
+
author: await names.resolve(c.version?.authorId),
|
|
2128
|
+
created: c.version?.createdAt ?? "",
|
|
2129
|
+
body: parseAdfJson(c.body?.atlas_doc_format?.value)
|
|
2130
|
+
})));
|
|
1572
2131
|
}
|
|
1573
|
-
function
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
return `${indent}${kleur.dim(">")}${gap}${highlightLine(rest, stripPrefix(nextLine, whole))}`;
|
|
2132
|
+
function parseAdfJson(value) {
|
|
2133
|
+
if (!value) return null;
|
|
2134
|
+
try {
|
|
2135
|
+
return JSON.parse(value);
|
|
2136
|
+
} catch {
|
|
2137
|
+
return null;
|
|
1580
2138
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2139
|
+
}
|
|
2140
|
+
var UserNames = class {
|
|
2141
|
+
client;
|
|
2142
|
+
cache = /* @__PURE__ */ new Map();
|
|
2143
|
+
constructor(client) {
|
|
2144
|
+
this.client = client;
|
|
1587
2145
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
return `${indent}${kleur.dim("<details><summary>")}${highlightInline(title)}${kleur.dim("</summary>")}`;
|
|
2146
|
+
async resolve(accountId) {
|
|
2147
|
+
if (!accountId) return "";
|
|
2148
|
+
const cached = this.cache.get(accountId);
|
|
2149
|
+
if (cached !== void 0) return cached;
|
|
2150
|
+
const name = await this.fetchDisplayName(accountId) ?? accountId;
|
|
2151
|
+
this.cache.set(accountId, name);
|
|
2152
|
+
return name;
|
|
1596
2153
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2154
|
+
async fetchDisplayName(accountId) {
|
|
2155
|
+
try {
|
|
2156
|
+
return (await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`)).displayName || null;
|
|
2157
|
+
} catch {
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
//#endregion
|
|
2163
|
+
//#region src/util/html.ts
|
|
2164
|
+
const MAX_CODE_POINT = 1114111;
|
|
2165
|
+
const NAMED = {
|
|
2166
|
+
amp: "&",
|
|
2167
|
+
lt: "<",
|
|
2168
|
+
gt: ">",
|
|
2169
|
+
quot: "\"",
|
|
2170
|
+
apos: "'",
|
|
2171
|
+
nbsp: " "
|
|
2172
|
+
};
|
|
2173
|
+
function decodeEntities(text) {
|
|
2174
|
+
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
|
|
2175
|
+
if (body[0] === "#") {
|
|
2176
|
+
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
2177
|
+
return isCodePoint(code) ? String.fromCodePoint(code) : match;
|
|
2178
|
+
}
|
|
2179
|
+
return NAMED[body.toLowerCase()] ?? match;
|
|
1617
2180
|
});
|
|
1618
2181
|
}
|
|
1619
|
-
function
|
|
1620
|
-
|
|
1621
|
-
const middle = kleur.dim("](");
|
|
1622
|
-
return `${open}${highlightInline(text)}${middle}${kleur.dim().underline(url)}${kleur.dim(")")}`;
|
|
2182
|
+
function isCodePoint(code) {
|
|
2183
|
+
return Number.isFinite(code) && code >= 0 && code <= MAX_CODE_POINT;
|
|
1623
2184
|
}
|
|
1624
2185
|
//#endregion
|
|
1625
|
-
//#region src/
|
|
1626
|
-
function
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2186
|
+
//#region src/api/confluence-search.ts
|
|
2187
|
+
async function searchPages(client, site, params) {
|
|
2188
|
+
const cql = buildCql(params);
|
|
2189
|
+
const query = new URLSearchParams({
|
|
2190
|
+
cql,
|
|
2191
|
+
limit: String(params.limit),
|
|
2192
|
+
expand: "space"
|
|
2193
|
+
});
|
|
2194
|
+
const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
|
|
2195
|
+
const serverPageWasFull = results.length === params.limit;
|
|
2196
|
+
return {
|
|
2197
|
+
pages: results.filter((r) => r.content?.id).map((r) => ({
|
|
2198
|
+
id: r.content?.id ?? "",
|
|
2199
|
+
space: r.space?.key ?? r.resultGlobalContainer?.title ?? "",
|
|
2200
|
+
title: decodeEntities(r.content?.title ?? r.title ?? ""),
|
|
2201
|
+
updated: r.lastModified ?? "",
|
|
2202
|
+
url: r.url ? `${site}/wiki${r.url}` : ""
|
|
2203
|
+
})),
|
|
2204
|
+
hasMore: serverPageWasFull
|
|
2205
|
+
};
|
|
1638
2206
|
}
|
|
1639
|
-
function
|
|
1640
|
-
if (
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
lines.push("", commentHeader(comment), ...body ? [highlightMarkdown(body)] : []);
|
|
1647
|
-
}
|
|
1648
|
-
return lines;
|
|
2207
|
+
function buildCql(params) {
|
|
2208
|
+
if (params.cql) return params.cql;
|
|
2209
|
+
const clauses = ["type = page"];
|
|
2210
|
+
if (params.starred) clauses.push("favourite = currentUser()");
|
|
2211
|
+
if (params.space) clauses.push(`space = ${cqlStringLiteral(params.space)}`);
|
|
2212
|
+
if (params.text) clauses.push(`text ~ ${cqlStringLiteral(params.text)}`);
|
|
2213
|
+
return `${clauses.join(" AND ")} ORDER BY lastmodified DESC`;
|
|
1649
2214
|
}
|
|
1650
|
-
function
|
|
1651
|
-
return
|
|
2215
|
+
function cqlStringLiteral(value) {
|
|
2216
|
+
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
1652
2217
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2218
|
+
//#endregion
|
|
2219
|
+
//#region src/commands/resolve-ref.ts
|
|
2220
|
+
async function resolveRef(ask, arg, kind) {
|
|
2221
|
+
const raw = arg ?? await ask.text({
|
|
2222
|
+
message: kind.message,
|
|
2223
|
+
flag: kind.flag,
|
|
2224
|
+
required: true
|
|
2225
|
+
});
|
|
2226
|
+
const ref = kind.parse(raw);
|
|
2227
|
+
if (!ref) throw new Error(kind.notFound(raw));
|
|
2228
|
+
return ref;
|
|
1660
2229
|
}
|
|
1661
2230
|
//#endregion
|
|
1662
2231
|
//#region src/api/attachments.ts
|
|
@@ -1732,9 +2301,10 @@ function scalarString(value) {
|
|
|
1732
2301
|
}
|
|
1733
2302
|
function frontmatter(fields) {
|
|
1734
2303
|
const lines = ["---"];
|
|
1735
|
-
for (const [key, value] of Object.entries(fields)) if (Array.isArray(value))
|
|
1736
|
-
|
|
1737
|
-
|
|
2304
|
+
for (const [key, value] of Object.entries(fields)) if (Array.isArray(value)) {
|
|
2305
|
+
if (value.length === 0) lines.push(`${key}: []`);
|
|
2306
|
+
else lines.push(`${key}:`, ...value.map((v) => ` - ${quote(v)}`));
|
|
2307
|
+
} else if (typeof value === "number") lines.push(`${key}: ${value}`);
|
|
1738
2308
|
else lines.push(`${key}: ${quote(value)}`);
|
|
1739
2309
|
lines.push("---");
|
|
1740
2310
|
return lines.join("\n");
|
|
@@ -1780,21 +2350,23 @@ function leadingH1(lines) {
|
|
|
1780
2350
|
}
|
|
1781
2351
|
function commentsSection(comments) {
|
|
1782
2352
|
if (comments.length === 0) return "";
|
|
2353
|
+
const blocks = comments.map((c) => {
|
|
2354
|
+
const heading = `### ${c.author || "Unknown"}${c.created ? ` - ${formatDateTime(c.created)}` : ""}`;
|
|
2355
|
+
return c.body ? `${heading}\n\n${c.body}` : heading;
|
|
2356
|
+
});
|
|
1783
2357
|
return [
|
|
1784
2358
|
COMMENTS_HEADING,
|
|
1785
2359
|
"",
|
|
1786
|
-
|
|
1787
|
-
const heading = `### ${c.author || "Unknown"}${c.created ? ` - ${formatDateTime(c.created)}` : ""}`;
|
|
1788
|
-
return c.body ? `${heading}\n\n${c.body}` : heading;
|
|
1789
|
-
}).join("\n\n")
|
|
2360
|
+
blocks.join("\n\n")
|
|
1790
2361
|
].join("\n");
|
|
1791
2362
|
}
|
|
1792
2363
|
function attachmentsSection(attachments) {
|
|
1793
2364
|
if (attachments.length === 0) return "";
|
|
2365
|
+
const items = attachments.map((a) => `- [${a.filename}](${a.relativePath})`);
|
|
1794
2366
|
return [
|
|
1795
2367
|
ATTACHMENTS_HEADING,
|
|
1796
2368
|
"",
|
|
1797
|
-
...
|
|
2369
|
+
...items
|
|
1798
2370
|
].join("\n");
|
|
1799
2371
|
}
|
|
1800
2372
|
//#endregion
|
|
@@ -1859,7 +2431,6 @@ function planCopy(source, out) {
|
|
|
1859
2431
|
return {
|
|
1860
2432
|
...content,
|
|
1861
2433
|
filePath: target.filePath,
|
|
1862
|
-
assetsDir: target.assetsDir,
|
|
1863
2434
|
downloads: planDownloads(attachments, target)
|
|
1864
2435
|
};
|
|
1865
2436
|
}
|
|
@@ -1905,21 +2476,18 @@ function numberedName(name, suffix) {
|
|
|
1905
2476
|
}
|
|
1906
2477
|
//#endregion
|
|
1907
2478
|
//#region src/copy/run.ts
|
|
1908
|
-
async function runCopy(
|
|
1909
|
-
const landed = await download(
|
|
1910
|
-
await
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
}
|
|
1914
|
-
async function download(plan, fetchBytes) {
|
|
1915
|
-
if (plan.downloads.length === 0) return [];
|
|
1916
|
-
await mkdir(plan.assetsDir, { recursive: true });
|
|
2479
|
+
async function runCopy(env, plan) {
|
|
2480
|
+
const landed = await download(env, plan);
|
|
2481
|
+
await env.files.writeText(plan.filePath, renderCopy(plan, landed));
|
|
2482
|
+
env.term.out(wroteLine(plan.filePath, landed.length));
|
|
2483
|
+
}
|
|
2484
|
+
async function download({ term, files, session }, plan) {
|
|
1917
2485
|
const landed = [];
|
|
1918
2486
|
for (const { path, ...attachment } of plan.downloads) try {
|
|
1919
|
-
await
|
|
2487
|
+
await files.writeBytes(path, await session.getBinary(attachment.url));
|
|
1920
2488
|
landed.push(attachment);
|
|
1921
2489
|
} catch (err) {
|
|
1922
|
-
|
|
2490
|
+
term.err(` ! could not download ${attachment.filename}: ${err.message}`);
|
|
1923
2491
|
}
|
|
1924
2492
|
return landed;
|
|
1925
2493
|
}
|
|
@@ -2136,80 +2704,31 @@ function withUploadedId(node, ids) {
|
|
|
2136
2704
|
}
|
|
2137
2705
|
//#endregion
|
|
2138
2706
|
//#region src/update/run.ts
|
|
2139
|
-
async function runPlan(plan, options, push) {
|
|
2707
|
+
async function runPlan(term, plan, options, push) {
|
|
2140
2708
|
if (options.dryRun) {
|
|
2141
|
-
|
|
2709
|
+
term.out(formatPlan(plan));
|
|
2142
2710
|
return;
|
|
2143
2711
|
}
|
|
2144
2712
|
const verdict = plan.verdict;
|
|
2145
2713
|
if (verdict.kind === "refuse") throw new Error(verdict.message);
|
|
2146
2714
|
if (verdict.kind === "confirm") {
|
|
2147
|
-
if (!await confirm({
|
|
2715
|
+
if (!await term.ask.confirm({
|
|
2148
2716
|
message: verdict.message,
|
|
2717
|
+
flag: "--force",
|
|
2149
2718
|
default: false
|
|
2150
2719
|
})) {
|
|
2151
|
-
|
|
2720
|
+
term.out("Aborted.");
|
|
2152
2721
|
return;
|
|
2153
2722
|
}
|
|
2154
2723
|
}
|
|
2155
2724
|
await push();
|
|
2156
2725
|
}
|
|
2157
2726
|
//#endregion
|
|
2158
|
-
//#region src/util/pager.ts
|
|
2159
|
-
const DEFAULT_ROWS = 24;
|
|
2160
|
-
const DEFAULT_COLUMNS = 80;
|
|
2161
|
-
const DEFAULT_PAGER = "less";
|
|
2162
|
-
const DEFAULT_LESS = "FRX";
|
|
2163
|
-
function renderedHeight(text, columns) {
|
|
2164
|
-
const width = Math.max(1, columns);
|
|
2165
|
-
return text.split("\n").map((line) => Math.max(1, Math.ceil(stripVTControlCharacters(line).length / width))).reduce((sum, rows) => sum + rows, 0);
|
|
2166
|
-
}
|
|
2167
|
-
function shouldPage(text, term) {
|
|
2168
|
-
if (!term.isTTY) return false;
|
|
2169
|
-
const rows = term.rows ?? DEFAULT_ROWS;
|
|
2170
|
-
return renderedHeight(text, term.columns ?? DEFAULT_COLUMNS) > rows;
|
|
2171
|
-
}
|
|
2172
|
-
function pagerCommand(env) {
|
|
2173
|
-
const [command = DEFAULT_PAGER, ...args] = (env["PAGER"] ?? "").trim().split(/\s+/).filter(Boolean);
|
|
2174
|
-
return {
|
|
2175
|
-
command,
|
|
2176
|
-
args,
|
|
2177
|
-
env: {
|
|
2178
|
-
...env,
|
|
2179
|
-
LESS: env["LESS"] ?? DEFAULT_LESS
|
|
2180
|
-
}
|
|
2181
|
-
};
|
|
2182
|
-
}
|
|
2183
|
-
async function printPaged(text, options = {}) {
|
|
2184
|
-
const term = options.term ?? process.stdout;
|
|
2185
|
-
if (options.pager === false || !shouldPage(text, term)) {
|
|
2186
|
-
console.log(text);
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
if (!await pipeToPager(text, pagerCommand(options.env ?? process.env))) console.log(text);
|
|
2190
|
-
}
|
|
2191
|
-
function pipeToPager(text, pager) {
|
|
2192
|
-
return new Promise((resolve) => {
|
|
2193
|
-
const child = spawn(pager.command, pager.args, {
|
|
2194
|
-
stdio: [
|
|
2195
|
-
"pipe",
|
|
2196
|
-
"inherit",
|
|
2197
|
-
"inherit"
|
|
2198
|
-
],
|
|
2199
|
-
env: pager.env
|
|
2200
|
-
});
|
|
2201
|
-
child.once("error", () => resolve(false));
|
|
2202
|
-
child.once("close", () => resolve(true));
|
|
2203
|
-
child.stdin.once("error", () => {});
|
|
2204
|
-
child.stdin.end(`${text}\n`);
|
|
2205
|
-
});
|
|
2206
|
-
}
|
|
2207
|
-
//#endregion
|
|
2208
2727
|
//#region src/commands/confluence.ts
|
|
2209
|
-
async function confluenceView(arg, options) {
|
|
2210
|
-
const
|
|
2211
|
-
const
|
|
2212
|
-
await
|
|
2728
|
+
async function confluenceView({ session, term }, arg, options) {
|
|
2729
|
+
const id = await resolveRef(term.ask, arg, PAGE_REF);
|
|
2730
|
+
const lines = formatPageView(await fetchPage(session, session.site, id), Date.now(), options.allComments ?? false);
|
|
2731
|
+
await term.page(lines.join("\n"), { pager: options.pager });
|
|
2213
2732
|
}
|
|
2214
2733
|
function formatPageView(page, nowMs, allComments) {
|
|
2215
2734
|
return [
|
|
@@ -2224,41 +2743,39 @@ function formatPageView(page, nowMs, allComments) {
|
|
|
2224
2743
|
["URL", page.url]
|
|
2225
2744
|
]),
|
|
2226
2745
|
...bodyLines(page.body),
|
|
2227
|
-
...commentSection(page.comments, allComments),
|
|
2746
|
+
...commentSection(renderedComments(page.comments), { allComments }),
|
|
2228
2747
|
...attachmentSection(page.attachments)
|
|
2229
2748
|
];
|
|
2230
2749
|
}
|
|
2231
|
-
async function confluenceCopy(arg, options) {
|
|
2232
|
-
|
|
2233
|
-
const id = await resolveRef(arg, PAGE_REF);
|
|
2234
|
-
await copyPage(new AtlassianClient(auth), auth.site, id, options.out);
|
|
2750
|
+
async function confluenceCopy(env, arg, options) {
|
|
2751
|
+
await copyPage(env, await resolveRef(env.term.ask, arg, PAGE_REF), options.out);
|
|
2235
2752
|
}
|
|
2236
|
-
async function confluenceUpdate(arg, options) {
|
|
2237
|
-
const file = arg ?? await
|
|
2753
|
+
async function confluenceUpdate({ session, term, files }, arg, options) {
|
|
2754
|
+
const file = arg ?? await term.ask.text({
|
|
2238
2755
|
message: "Path to the page Markdown file:",
|
|
2756
|
+
flag: "[file]",
|
|
2239
2757
|
required: true
|
|
2240
2758
|
});
|
|
2241
|
-
const src = parsePageSource(await
|
|
2242
|
-
const
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
await runPlan(plan, options, async () => {
|
|
2759
|
+
const src = parsePageSource(await files.readText(file));
|
|
2760
|
+
const state = await fetchPageState(session, src.id);
|
|
2761
|
+
const plan = planPageUpdate(src, state, await listAttachments(session, src.id), await statLocalImages(files, dirname(resolve(file)), src.body), options);
|
|
2762
|
+
await runPlan(term, plan, options, async () => {
|
|
2246
2763
|
const ids = /* @__PURE__ */ new Map();
|
|
2247
2764
|
for (const upload of plan.uploads) {
|
|
2248
|
-
|
|
2249
|
-
const bytes = await
|
|
2250
|
-
ids.set(upload.href, await uploadAttachment(
|
|
2765
|
+
term.err(`Uploading ${upload.filename} ...`);
|
|
2766
|
+
const bytes = await files.readBytes(upload.path);
|
|
2767
|
+
ids.set(upload.href, await uploadAttachment(session, src.id, upload.filename, bytes));
|
|
2251
2768
|
}
|
|
2252
|
-
const version = await updatePage(
|
|
2769
|
+
const version = await updatePage(session, src.id, {
|
|
2253
2770
|
title: plan.headline.next,
|
|
2254
2771
|
nextVersion: state.version + 1,
|
|
2255
2772
|
body: withUploadedIds(plan.body, ids),
|
|
2256
2773
|
message: options.message ?? "Updated via atlass"
|
|
2257
2774
|
});
|
|
2258
|
-
|
|
2775
|
+
term.out(`Updated page ${src.id} to version ${version}.`);
|
|
2259
2776
|
});
|
|
2260
2777
|
}
|
|
2261
|
-
async function statLocalImages(dir, md) {
|
|
2778
|
+
async function statLocalImages(files, dir, md) {
|
|
2262
2779
|
const hrefs = imageHrefs(md).filter((href) => !isExternalHref(href));
|
|
2263
2780
|
return Promise.all(hrefs.map(async (href) => {
|
|
2264
2781
|
const path = isAbsolute(href) ? href : resolve(dir, href);
|
|
@@ -2266,47 +2783,46 @@ async function statLocalImages(dir, md) {
|
|
|
2266
2783
|
href,
|
|
2267
2784
|
path,
|
|
2268
2785
|
filename: basename(path),
|
|
2269
|
-
...await fileSize(path)
|
|
2786
|
+
...await fileSize(files, path)
|
|
2270
2787
|
};
|
|
2271
2788
|
}));
|
|
2272
2789
|
}
|
|
2273
|
-
async function fileSize(path) {
|
|
2790
|
+
async function fileSize(files, path) {
|
|
2274
2791
|
try {
|
|
2275
|
-
return { size:
|
|
2792
|
+
return { size: await files.size(path) };
|
|
2276
2793
|
} catch {
|
|
2277
2794
|
return {};
|
|
2278
2795
|
}
|
|
2279
2796
|
}
|
|
2280
|
-
async function confluenceSearch(query, options) {
|
|
2797
|
+
async function confluenceSearch(env, query, options) {
|
|
2281
2798
|
if (options.cql && (query || options.space)) throw new Error("--cql cannot be combined with a text query or --space.");
|
|
2282
|
-
await listPages({
|
|
2799
|
+
await listPages(env, {
|
|
2283
2800
|
text: query,
|
|
2284
2801
|
space: options.space,
|
|
2285
2802
|
cql: options.cql
|
|
2286
2803
|
}, "No matching pages.", options);
|
|
2287
2804
|
}
|
|
2288
|
-
async function confluenceList(options) {
|
|
2289
|
-
await listPages({
|
|
2805
|
+
async function confluenceList(env, options) {
|
|
2806
|
+
await listPages(env, {
|
|
2290
2807
|
starred: true,
|
|
2291
2808
|
space: options.space
|
|
2292
2809
|
}, options.space ? `No starred pages in ${options.space}.` : "No starred pages.", options);
|
|
2293
2810
|
}
|
|
2294
|
-
async function listPages(filter, empty, options) {
|
|
2811
|
+
async function listPages(env, filter, empty, options) {
|
|
2812
|
+
const { session, term } = env;
|
|
2295
2813
|
if (options.json && options.copy) throw new Error("--json and --copy cannot be used together.");
|
|
2296
|
-
const auth = await requireAuth();
|
|
2297
|
-
const client = new AtlassianClient(auth);
|
|
2298
2814
|
const limit = parseLimit(options.limit);
|
|
2299
|
-
const { pages, hasMore } = await searchPages(
|
|
2815
|
+
const { pages, hasMore } = await searchPages(session, session.site, {
|
|
2300
2816
|
...filter,
|
|
2301
2817
|
limit
|
|
2302
2818
|
});
|
|
2303
|
-
await runSearch(formatPageRows(pages, Date.now()), {
|
|
2819
|
+
await runSearch(term, formatPageRows(pages, Date.now()), {
|
|
2304
2820
|
json: options.json,
|
|
2305
2821
|
copy: options.copy,
|
|
2306
2822
|
out: options.out,
|
|
2307
2823
|
empty,
|
|
2308
2824
|
footer: hasMore ? searchFooter(limit) : void 0
|
|
2309
|
-
}, PAGE_NOUN, (id) => copyPage(
|
|
2825
|
+
}, PAGE_NOUN, (id) => copyPage(env, id, options.out));
|
|
2310
2826
|
}
|
|
2311
2827
|
function formatPageRows(pages, nowMs) {
|
|
2312
2828
|
return alignedRows(pages, nowMs, (p) => ({
|
|
@@ -2334,12 +2850,13 @@ const PAGE_NOUN = {
|
|
|
2334
2850
|
singular: "page",
|
|
2335
2851
|
plural: "pages"
|
|
2336
2852
|
};
|
|
2337
|
-
async function copyPage(
|
|
2338
|
-
|
|
2339
|
-
await runCopy(planPageCopy(await fetchPage(
|
|
2853
|
+
async function copyPage(env, id, out) {
|
|
2854
|
+
env.term.err(`Fetching page ${id} ...`);
|
|
2855
|
+
await runCopy(env, planPageCopy(await fetchPage(env.session, env.session.site, id), out));
|
|
2340
2856
|
}
|
|
2341
2857
|
const PAGE_REF = {
|
|
2342
2858
|
message: "Confluence page id or URL:",
|
|
2859
|
+
flag: "[page]",
|
|
2343
2860
|
parse: parsePageId,
|
|
2344
2861
|
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2345
2862
|
};
|
|
@@ -2347,11 +2864,11 @@ const PAGE_REF = {
|
|
|
2347
2864
|
//#region src/cli/confluence.ts
|
|
2348
2865
|
function registerConfluence(confluence) {
|
|
2349
2866
|
confluence.description("Confluence commands");
|
|
2350
|
-
confluence.command("view [page]").description("Show a Confluence page (id or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(
|
|
2351
|
-
confluence.command("copy [page]").description("Copy a Confluence page (id or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(
|
|
2352
|
-
confluence.command("update [file]").description("Update a Confluence page from an edited Markdown file").option("--title", "also push the H1 as the page title").option("-m, --message <text>", "version message (default 'Updated via atlass')").option("-f, --force", "skip the stale-version and data-loss checks").option("--dry-run", "show what would change without writing").action(
|
|
2353
|
-
confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(
|
|
2354
|
-
confluence.command("search [query]").description("Search Confluence pages (text query, --space, or --cql)").option("-s, --space <key>", "limit to a space").option("--cql <cql>", "raw CQL query (ignores other filters)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(
|
|
2867
|
+
confluence.command("view [page]").description("Show a Confluence page (id or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(jiraAction(confluenceView));
|
|
2868
|
+
confluence.command("copy [page]").description("Copy a Confluence page (id or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(jiraAction(confluenceCopy));
|
|
2869
|
+
confluence.command("update [file]").description("Update a Confluence page from an edited Markdown file").option("--title", "also push the H1 as the page title").option("-m, --message <text>", "version message (default 'Updated via atlass')").option("-f, --force", "skip the stale-version and data-loss checks").option("--dry-run", "show what would change without writing").action(jiraAction(confluenceUpdate));
|
|
2870
|
+
confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(jiraAction(confluenceList));
|
|
2871
|
+
confluence.command("search [query]").description("Search Confluence pages (text query, --space, or --cql)").option("-s, --space <key>", "limit to a space").option("--cql <cql>", "raw CQL query (ignores other filters)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(jiraAction(confluenceSearch));
|
|
2355
2872
|
return confluence;
|
|
2356
2873
|
}
|
|
2357
2874
|
//#endregion
|
|
@@ -2462,10 +2979,10 @@ function projectSearchQuery(query, startAt) {
|
|
|
2462
2979
|
}
|
|
2463
2980
|
//#endregion
|
|
2464
2981
|
//#region src/commands/jira-create-resolve.ts
|
|
2465
|
-
async function resolveProject(client, site, arg, strict) {
|
|
2982
|
+
async function resolveProject(ask, client, site, arg, strict) {
|
|
2466
2983
|
if (arg) return arg.toUpperCase();
|
|
2467
2984
|
if (strict) throw new Error("A project key is required, e.g. `jira create BSC Bug --summary ...`.");
|
|
2468
|
-
return
|
|
2985
|
+
return ask.searchPick({
|
|
2469
2986
|
message: "Project:",
|
|
2470
2987
|
source: async (term) => (await listProjects(client, site, term)).map((p) => ({
|
|
2471
2988
|
name: `${p.key} ${p.name}`,
|
|
@@ -2474,7 +2991,7 @@ async function resolveProject(client, site, arg, strict) {
|
|
|
2474
2991
|
pageSize: 15
|
|
2475
2992
|
});
|
|
2476
2993
|
}
|
|
2477
|
-
async function resolveType(project, types, arg, strict) {
|
|
2994
|
+
async function resolveType(ask, project, types, arg, strict) {
|
|
2478
2995
|
if (types.length === 0) throw new Error(`You cannot create issues in ${project}.`);
|
|
2479
2996
|
if (arg) {
|
|
2480
2997
|
const found = matchType(types, arg);
|
|
@@ -2483,7 +3000,7 @@ async function resolveType(project, types, arg, strict) {
|
|
|
2483
3000
|
throw new Error(`${project} has no issue type "${arg}"; available: ${names}.`);
|
|
2484
3001
|
}
|
|
2485
3002
|
if (strict) throw new Error(`An issue type is required; ${project} has: ${types.map((t) => t.name).join(", ")}.`);
|
|
2486
|
-
return
|
|
3003
|
+
return ask.pick({
|
|
2487
3004
|
message: "Issue type:",
|
|
2488
3005
|
choices: types.map((t) => ({
|
|
2489
3006
|
name: t.name,
|
|
@@ -2548,7 +3065,7 @@ function matchCascading(field, value) {
|
|
|
2548
3065
|
}
|
|
2549
3066
|
//#endregion
|
|
2550
3067
|
//#region src/create/encode.ts
|
|
2551
|
-
const SERVER_FILLED = new Set([
|
|
3068
|
+
const SERVER_FILLED = /* @__PURE__ */ new Set([
|
|
2552
3069
|
"project",
|
|
2553
3070
|
"issuetype",
|
|
2554
3071
|
"reporter"
|
|
@@ -2628,7 +3145,7 @@ async function encodeField(field, values, resolveUser) {
|
|
|
2628
3145
|
}
|
|
2629
3146
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2630
3147
|
const DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/i;
|
|
2631
|
-
const NAMED_TYPES = new Set([
|
|
3148
|
+
const NAMED_TYPES = /* @__PURE__ */ new Set([
|
|
2632
3149
|
"priority",
|
|
2633
3150
|
"version",
|
|
2634
3151
|
"component",
|
|
@@ -2809,16 +3326,16 @@ function formatIssueTypeRows(types) {
|
|
|
2809
3326
|
//#endregion
|
|
2810
3327
|
//#region src/create/prompt.ts
|
|
2811
3328
|
const PROMPT_SOURCE = "prompt";
|
|
2812
|
-
async function walkFields(meta, deps) {
|
|
3329
|
+
async function walkFields(ask, meta, deps) {
|
|
2813
3330
|
const firstPass = meta.filter((f) => f.required && !SERVER_FILLED.has(f.fieldId));
|
|
2814
3331
|
const inputs = [];
|
|
2815
3332
|
for (const field of firstPass) {
|
|
2816
|
-
const answer = await promptField(field, deps, true);
|
|
3333
|
+
const answer = await promptField(ask, field, deps, true);
|
|
2817
3334
|
if (answer) inputs.push(answer);
|
|
2818
3335
|
}
|
|
2819
3336
|
const optional = meta.filter((f) => !firstPass.includes(f) && !SERVER_FILLED.has(f.fieldId));
|
|
2820
3337
|
if (optional.length === 0) return inputs;
|
|
2821
|
-
const chosen = await
|
|
3338
|
+
const chosen = await ask.pickMany({
|
|
2822
3339
|
message: "Set any optional fields?",
|
|
2823
3340
|
choices: optional.map((f) => ({
|
|
2824
3341
|
name: `${f.name} (${fieldTypeLabel(f.schema)})`,
|
|
@@ -2827,13 +3344,13 @@ async function walkFields(meta, deps) {
|
|
|
2827
3344
|
pageSize: 15
|
|
2828
3345
|
});
|
|
2829
3346
|
for (const field of optional.filter((f) => chosen.includes(f.fieldId))) {
|
|
2830
|
-
const answer = await promptField(field, deps, false);
|
|
3347
|
+
const answer = await promptField(ask, field, deps, false);
|
|
2831
3348
|
if (answer) inputs.push(answer);
|
|
2832
3349
|
}
|
|
2833
3350
|
return inputs;
|
|
2834
3351
|
}
|
|
2835
|
-
async function promptField(field, deps, required) {
|
|
2836
|
-
const answer = await askValues(field, deps, required);
|
|
3352
|
+
async function promptField(ask, field, deps, required) {
|
|
3353
|
+
const answer = await askValues(ask, field, deps, required);
|
|
2837
3354
|
if (answer.values.length === 0) return void 0;
|
|
2838
3355
|
return {
|
|
2839
3356
|
name: field.fieldId,
|
|
@@ -2846,14 +3363,14 @@ const SKIP = {
|
|
|
2846
3363
|
name: "(skip)",
|
|
2847
3364
|
value: ""
|
|
2848
3365
|
};
|
|
2849
|
-
async function askValues(field, deps, required) {
|
|
3366
|
+
async function askValues(ask, field, deps, required) {
|
|
2850
3367
|
const { schema } = field;
|
|
2851
3368
|
const message = `${field.name}:`;
|
|
2852
3369
|
const options = field.allowedValues ?? [];
|
|
2853
|
-
if (schema.type === "option-with-child") return askCascading(field, required);
|
|
3370
|
+
if (schema.type === "option-with-child") return askCascading(ask, field, required);
|
|
2854
3371
|
if (options.length > 0 && schema.type === "array") {
|
|
2855
3372
|
const defaults = new Set(defaultIds(field));
|
|
2856
|
-
return { values: await
|
|
3373
|
+
return { values: await ask.pickMany({
|
|
2857
3374
|
message,
|
|
2858
3375
|
choices: options.map((v) => ({
|
|
2859
3376
|
...choice(v),
|
|
@@ -2865,7 +3382,7 @@ async function askValues(field, deps, required) {
|
|
|
2865
3382
|
}
|
|
2866
3383
|
if (options.length > 0) {
|
|
2867
3384
|
const choices = options.map(choice);
|
|
2868
|
-
const picked = await
|
|
3385
|
+
const picked = await ask.pick({
|
|
2869
3386
|
message,
|
|
2870
3387
|
choices: required ? choices : [SKIP, ...choices],
|
|
2871
3388
|
default: defaultIds(field)[0],
|
|
@@ -2873,9 +3390,9 @@ async function askValues(field, deps, required) {
|
|
|
2873
3390
|
});
|
|
2874
3391
|
return { values: picked ? [picked] : [] };
|
|
2875
3392
|
}
|
|
2876
|
-
if (schema.type === "user") return askUser(field, deps, required);
|
|
3393
|
+
if (schema.type === "user") return askUser(ask, field, deps, required);
|
|
2877
3394
|
if (isMultiLine(schema)) {
|
|
2878
|
-
const text = await
|
|
3395
|
+
const text = await ask.edit({
|
|
2879
3396
|
message: `${field.name} (opens your editor, Markdown):`,
|
|
2880
3397
|
postfix: ".md",
|
|
2881
3398
|
default: primitiveDefault(field),
|
|
@@ -2884,7 +3401,7 @@ async function askValues(field, deps, required) {
|
|
|
2884
3401
|
return { values: text.trim() ? [text] : [] };
|
|
2885
3402
|
}
|
|
2886
3403
|
const hint = inputHint(field);
|
|
2887
|
-
const text = await
|
|
3404
|
+
const text = await ask.text({
|
|
2888
3405
|
message: hint ? `${field.name} (${hint}):` : message,
|
|
2889
3406
|
default: primitiveDefault(field),
|
|
2890
3407
|
validate: async (v) => {
|
|
@@ -2922,10 +3439,10 @@ function primitiveDefault(field) {
|
|
|
2922
3439
|
if (!field.hasDefaultValue || d == null) return void 0;
|
|
2923
3440
|
return typeof d === "string" || typeof d === "number" ? String(d) : void 0;
|
|
2924
3441
|
}
|
|
2925
|
-
async function askCascading(field, required) {
|
|
3442
|
+
async function askCascading(ask, field, required) {
|
|
2926
3443
|
const options = field.allowedValues ?? [];
|
|
2927
3444
|
const parentChoices = options.map(choice);
|
|
2928
|
-
const parentId = await
|
|
3445
|
+
const parentId = await ask.pick({
|
|
2929
3446
|
message: `${field.name}:`,
|
|
2930
3447
|
choices: required ? parentChoices : [SKIP, ...parentChoices],
|
|
2931
3448
|
pageSize: 15
|
|
@@ -2934,7 +3451,7 @@ async function askCascading(field, required) {
|
|
|
2934
3451
|
const parent = options.find((v) => v.id === parentId);
|
|
2935
3452
|
const children = parent?.children ?? [];
|
|
2936
3453
|
if (children.length === 0) return { values: [parentId] };
|
|
2937
|
-
const childId = await
|
|
3454
|
+
const childId = await ask.pick({
|
|
2938
3455
|
message: `${field.name} > ${allowedLabel(parent)}:`,
|
|
2939
3456
|
choices: [{
|
|
2940
3457
|
name: "(none)",
|
|
@@ -2952,10 +3469,10 @@ const UNASSIGNED = {
|
|
|
2952
3469
|
name: "(unassigned)",
|
|
2953
3470
|
value: ""
|
|
2954
3471
|
};
|
|
2955
|
-
async function askUser(field, deps, required) {
|
|
3472
|
+
async function askUser(ask, field, deps, required) {
|
|
2956
3473
|
const fixed = required ? [ME] : [ME, UNASSIGNED];
|
|
2957
3474
|
const seen = /* @__PURE__ */ new Map();
|
|
2958
|
-
const picked = await
|
|
3475
|
+
const picked = await ask.searchPick({
|
|
2959
3476
|
message: `${field.name} (type to search):`,
|
|
2960
3477
|
source: async (term) => {
|
|
2961
3478
|
if (!term) return fixed;
|
|
@@ -2997,7 +3514,7 @@ function summarizeText(text) {
|
|
|
2997
3514
|
}
|
|
2998
3515
|
//#endregion
|
|
2999
3516
|
//#region src/commands/jira-create.ts
|
|
3000
|
-
async function flagInputs(options) {
|
|
3517
|
+
async function flagInputs(files, options) {
|
|
3001
3518
|
const inputs = [];
|
|
3002
3519
|
const one = (name, value, source) => {
|
|
3003
3520
|
if (value !== void 0) inputs.push({
|
|
@@ -3010,7 +3527,7 @@ async function flagInputs(options) {
|
|
|
3010
3527
|
one("description", options.description, "--description");
|
|
3011
3528
|
if (options.descriptionFile !== void 0) {
|
|
3012
3529
|
if (options.description !== void 0) throw new Error("--description and --description-file cannot be used together.");
|
|
3013
|
-
one("description", await
|
|
3530
|
+
one("description", await files.readText(options.descriptionFile), "--description-file");
|
|
3014
3531
|
}
|
|
3015
3532
|
one("assignee", options.assignee, "--assignee");
|
|
3016
3533
|
one("priority", options.priority, "--priority");
|
|
@@ -3037,17 +3554,16 @@ function parseFieldFlag(raw) {
|
|
|
3037
3554
|
source: "--field"
|
|
3038
3555
|
};
|
|
3039
3556
|
}
|
|
3040
|
-
async function jiraCreate(projectArg, typeArg, options) {
|
|
3041
|
-
const inputs = await flagInputs(options);
|
|
3042
|
-
const strict = inputs.length > 0 ||
|
|
3043
|
-
const
|
|
3044
|
-
const
|
|
3045
|
-
const
|
|
3046
|
-
const
|
|
3047
|
-
const
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
searchUsers: (query) => searchAssignableUsers(client, project, query),
|
|
3557
|
+
async function jiraCreate({ session, term, files }, projectArg, typeArg, options) {
|
|
3558
|
+
const inputs = await flagInputs(files, options);
|
|
3559
|
+
const strict = inputs.length > 0 || !term.interactive;
|
|
3560
|
+
const project = await resolveProject(term.ask, session, session.site, projectArg, strict);
|
|
3561
|
+
const types = await fetchCreateIssueTypes(session, project);
|
|
3562
|
+
const type = await resolveType(term.ask, project, types, typeArg, strict);
|
|
3563
|
+
const meta = await fetchCreateFields(session, project, type.id);
|
|
3564
|
+
const resolveUser = userResolver(session, project);
|
|
3565
|
+
if (!strict) inputs.push(...await walkFields(term.ask, meta, {
|
|
3566
|
+
searchUsers: (query) => searchAssignableUsers(session, project, query),
|
|
3051
3567
|
validate: async (field, value) => {
|
|
3052
3568
|
return (await encodeCreate([field], [{
|
|
3053
3569
|
name: field.fieldId,
|
|
@@ -3065,23 +3581,23 @@ async function jiraCreate(projectArg, typeArg, options) {
|
|
|
3065
3581
|
...result.fields
|
|
3066
3582
|
};
|
|
3067
3583
|
if (options.dryRun) {
|
|
3068
|
-
|
|
3584
|
+
term.json({ fields });
|
|
3069
3585
|
return;
|
|
3070
3586
|
}
|
|
3071
3587
|
if (!strict) {
|
|
3072
|
-
|
|
3073
|
-
if (!await confirm({
|
|
3588
|
+
term.out(formatReview(meta, inputs));
|
|
3589
|
+
if (!await term.ask.confirm({
|
|
3074
3590
|
message: `Create ${project} ${type.name}?`,
|
|
3075
3591
|
default: true
|
|
3076
3592
|
})) {
|
|
3077
|
-
|
|
3593
|
+
term.out("Aborted.");
|
|
3078
3594
|
return;
|
|
3079
3595
|
}
|
|
3080
3596
|
}
|
|
3081
3597
|
try {
|
|
3082
|
-
const created = await createIssue(
|
|
3083
|
-
if (options.json)
|
|
3084
|
-
else
|
|
3598
|
+
const created = await createIssue(session, session.site, fields);
|
|
3599
|
+
if (options.json) term.json(created);
|
|
3600
|
+
else term.out(`Created ${created.key} ${created.url}`);
|
|
3085
3601
|
} catch (err) {
|
|
3086
3602
|
throw describeRejection(err, meta);
|
|
3087
3603
|
}
|
|
@@ -3107,19 +3623,18 @@ function describeRejection(err, meta) {
|
|
|
3107
3623
|
...perField
|
|
3108
3624
|
].join("\n"));
|
|
3109
3625
|
}
|
|
3110
|
-
async function jiraFields(projectArg, typeArg, options) {
|
|
3111
|
-
const client = new AtlassianClient(await requireAuth());
|
|
3626
|
+
async function jiraFields({ session, term }, projectArg, typeArg, options) {
|
|
3112
3627
|
const project = projectArg.toUpperCase();
|
|
3113
|
-
const types = await fetchCreateIssueTypes(
|
|
3628
|
+
const types = await fetchCreateIssueTypes(session, project);
|
|
3114
3629
|
if (!typeArg) {
|
|
3115
|
-
if (options.json)
|
|
3116
|
-
else if (types.length === 0)
|
|
3117
|
-
else
|
|
3630
|
+
if (options.json) term.json(types);
|
|
3631
|
+
else if (types.length === 0) term.out(`You cannot create issues in ${project}.`);
|
|
3632
|
+
else term.out(formatIssueTypeRows(types));
|
|
3118
3633
|
return;
|
|
3119
3634
|
}
|
|
3120
|
-
const fields = await fetchCreateFields(
|
|
3121
|
-
if (options.json)
|
|
3122
|
-
else
|
|
3635
|
+
const fields = await fetchCreateFields(session, project, (await resolveType(term.ask, project, types, typeArg, true)).id);
|
|
3636
|
+
if (options.json) term.json(fields);
|
|
3637
|
+
else term.out(formatFieldRows(fields));
|
|
3123
3638
|
}
|
|
3124
3639
|
//#endregion
|
|
3125
3640
|
//#region src/api/jira-search.ts
|
|
@@ -3295,47 +3810,34 @@ function dedupeAndSortStatuses(statuses) {
|
|
|
3295
3810
|
}
|
|
3296
3811
|
//#endregion
|
|
3297
3812
|
//#region src/commands/jira.ts
|
|
3298
|
-
async function jiraProjects(query, options) {
|
|
3299
|
-
const
|
|
3300
|
-
|
|
3301
|
-
if (
|
|
3302
|
-
|
|
3303
|
-
return;
|
|
3304
|
-
}
|
|
3305
|
-
if (projects.length === 0) {
|
|
3306
|
-
console.log("No matching projects.");
|
|
3307
|
-
return;
|
|
3308
|
-
}
|
|
3309
|
-
for (const line of formatProjectRows(projects)) console.log(line);
|
|
3813
|
+
async function jiraProjects({ session, term }, query, options) {
|
|
3814
|
+
const projects = await listProjects(session, session.site, query);
|
|
3815
|
+
if (options.json) term.json(projects);
|
|
3816
|
+
else if (projects.length === 0) term.out("No matching projects.");
|
|
3817
|
+
else term.out(formatProjectRows(projects));
|
|
3310
3818
|
}
|
|
3311
3819
|
function formatProjectRows(projects) {
|
|
3312
3820
|
const width = Math.max(...projects.map((p) => p.key.length));
|
|
3313
3821
|
return projects.map((p) => `${p.key.padEnd(width)} ${p.name}`);
|
|
3314
3822
|
}
|
|
3315
|
-
async function jiraStatuses(query, options) {
|
|
3316
|
-
let statuses = await listStatuses(
|
|
3823
|
+
async function jiraStatuses({ session, term }, query, options) {
|
|
3824
|
+
let statuses = await listStatuses(session, options.project);
|
|
3317
3825
|
if (query) {
|
|
3318
3826
|
const needle = query.toLowerCase();
|
|
3319
3827
|
statuses = statuses.filter((s) => s.name.toLowerCase().includes(needle));
|
|
3320
3828
|
}
|
|
3321
|
-
if (options.json)
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
}
|
|
3325
|
-
if (statuses.length === 0) {
|
|
3326
|
-
console.log("No matching statuses.");
|
|
3327
|
-
return;
|
|
3328
|
-
}
|
|
3329
|
-
for (const line of formatStatusRows(statuses)) console.log(line);
|
|
3829
|
+
if (options.json) term.json(statuses);
|
|
3830
|
+
else if (statuses.length === 0) term.out("No matching statuses.");
|
|
3831
|
+
else term.out(formatStatusRows(statuses));
|
|
3330
3832
|
}
|
|
3331
3833
|
function formatStatusRows(statuses) {
|
|
3332
3834
|
const width = Math.max(...statuses.map((s) => s.name.length));
|
|
3333
3835
|
return statuses.map((s) => `${s.name.padEnd(width)} ${s.category}`);
|
|
3334
3836
|
}
|
|
3335
|
-
async function jiraView(arg, options) {
|
|
3336
|
-
const
|
|
3337
|
-
const
|
|
3338
|
-
await
|
|
3837
|
+
async function jiraView({ session, term }, arg, options) {
|
|
3838
|
+
const key = await resolveRef(term.ask, arg, ISSUE_REF);
|
|
3839
|
+
const lines = formatIssueView(await fetchIssue(session, session.site, key), Date.now(), options.allComments ?? false);
|
|
3840
|
+
await term.page(lines.join("\n"), { pager: options.pager });
|
|
3339
3841
|
}
|
|
3340
3842
|
const CATEGORY_COLORS = {
|
|
3341
3843
|
new: kleur.cyan,
|
|
@@ -3361,50 +3863,48 @@ function formatIssueView(issue, nowMs, allComments) {
|
|
|
3361
3863
|
["URL", issue.url]
|
|
3362
3864
|
]),
|
|
3363
3865
|
...bodyLines(issue.description),
|
|
3364
|
-
...commentSection(issue.comments, allComments),
|
|
3866
|
+
...commentSection(renderedComments(issue.comments), { allComments }),
|
|
3365
3867
|
...attachmentSection(issue.attachments)
|
|
3366
3868
|
];
|
|
3367
3869
|
}
|
|
3368
|
-
async function jiraCopy(arg, options) {
|
|
3369
|
-
|
|
3370
|
-
const key = await resolveRef(arg, ISSUE_REF);
|
|
3371
|
-
await copyIssue(new AtlassianClient(auth), auth.site, key, options.out);
|
|
3870
|
+
async function jiraCopy(env, arg, options) {
|
|
3871
|
+
await copyIssue(env, await resolveRef(env.term.ask, arg, ISSUE_REF), options.out);
|
|
3372
3872
|
}
|
|
3373
|
-
async function jiraUpdate(arg, options) {
|
|
3374
|
-
const
|
|
3873
|
+
async function jiraUpdate({ session, term, files }, arg, options) {
|
|
3874
|
+
const file = arg ?? await term.ask.text({
|
|
3375
3875
|
message: "Path to the issue Markdown file:",
|
|
3876
|
+
flag: "[file]",
|
|
3376
3877
|
required: true
|
|
3377
|
-
})
|
|
3378
|
-
const
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3381
|
-
await runPlan(plan, options, async () => {
|
|
3878
|
+
});
|
|
3879
|
+
const src = parseIssueSource(await files.readText(file));
|
|
3880
|
+
const plan = planIssueUpdate(src, await fetchIssue(session, session.site, src.key), options);
|
|
3881
|
+
await runPlan(term, plan, options, async () => {
|
|
3382
3882
|
const { current, next } = plan.headline;
|
|
3383
|
-
await updateIssue(
|
|
3883
|
+
await updateIssue(session, src.key, {
|
|
3384
3884
|
description: plan.body,
|
|
3385
3885
|
summary: next !== current ? next : void 0
|
|
3386
3886
|
});
|
|
3387
|
-
|
|
3887
|
+
term.out(`Updated ${src.key}.`);
|
|
3388
3888
|
});
|
|
3389
3889
|
}
|
|
3390
|
-
async function copyIssue(
|
|
3391
|
-
|
|
3392
|
-
await runCopy(planIssueCopy(await fetchIssue(
|
|
3890
|
+
async function copyIssue(env, key, out) {
|
|
3891
|
+
env.term.err(`Fetching ${key} ...`);
|
|
3892
|
+
await runCopy(env, planIssueCopy(await fetchIssue(env.session, env.session.site, key), out));
|
|
3393
3893
|
}
|
|
3394
3894
|
const ISSUE_REF = {
|
|
3395
3895
|
message: "Jira issue key or URL:",
|
|
3896
|
+
flag: "[issue]",
|
|
3396
3897
|
parse: parseIssueKey,
|
|
3397
3898
|
notFound: (raw) => `Could not find an issue key in "${raw}" (expected e.g. PROJ-123).`
|
|
3398
3899
|
};
|
|
3399
3900
|
//#endregion
|
|
3400
3901
|
//#region src/commands/jira-search.ts
|
|
3401
|
-
async function jiraSearch(query, options) {
|
|
3902
|
+
async function jiraSearch(env, query, options) {
|
|
3903
|
+
const { session, term } = env;
|
|
3402
3904
|
if (options.jql && (query || options.project || options.assignee || options.status)) throw new Error("--jql cannot be combined with a text query or other filters.");
|
|
3403
3905
|
if (options.json && options.copy) throw new Error("--json and --copy cannot be used together.");
|
|
3404
|
-
const auth = await requireAuth();
|
|
3405
|
-
const client = new AtlassianClient(auth);
|
|
3406
3906
|
const limit = parseLimit(options.limit);
|
|
3407
|
-
const issues = await searchIssues(
|
|
3907
|
+
const issues = await searchIssues(session, session.site, {
|
|
3408
3908
|
text: query,
|
|
3409
3909
|
project: options.project,
|
|
3410
3910
|
assignee: options.assignee,
|
|
@@ -3412,29 +3912,28 @@ async function jiraSearch(query, options) {
|
|
|
3412
3912
|
jql: options.jql,
|
|
3413
3913
|
limit
|
|
3414
3914
|
});
|
|
3415
|
-
await runSearch(formatIssueRows(issues, Date.now()), {
|
|
3915
|
+
await runSearch(term, formatIssueRows(issues, Date.now()), {
|
|
3416
3916
|
json: options.json,
|
|
3417
3917
|
copy: options.copy,
|
|
3418
3918
|
out: options.out,
|
|
3419
3919
|
empty: "No matching issues.",
|
|
3420
3920
|
footer: issues.length === limit ? searchFooter(limit) : void 0
|
|
3421
|
-
}, ISSUE_NOUN, (key) => copyIssue(
|
|
3921
|
+
}, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
3422
3922
|
}
|
|
3423
|
-
async function jiraList(options) {
|
|
3923
|
+
async function jiraList(env, options) {
|
|
3924
|
+
const { session, term } = env;
|
|
3424
3925
|
if (options.json && options.copy) throw new Error("--json and --copy cannot be used together.");
|
|
3425
|
-
const
|
|
3426
|
-
const client = new AtlassianClient(auth);
|
|
3427
|
-
const { issues, truncated } = await listAssignedIssues(client, auth.site, {
|
|
3926
|
+
const { issues, truncated } = await listAssignedIssues(session, session.site, {
|
|
3428
3927
|
all: options.all,
|
|
3429
3928
|
project: options.project
|
|
3430
3929
|
});
|
|
3431
|
-
await runSearch(formatIssueRows(sortByCategoryThenUpdated(issues), Date.now()), {
|
|
3930
|
+
await runSearch(term, formatIssueRows(sortByCategoryThenUpdated(issues), Date.now()), {
|
|
3432
3931
|
json: options.json,
|
|
3433
3932
|
copy: options.copy,
|
|
3434
3933
|
out: options.out,
|
|
3435
3934
|
empty: options.all ? "No issues assigned to you." : "No open issues assigned to you.",
|
|
3436
3935
|
footer: truncated ? `\nShowing the first ${issues.length}; narrow with --project.` : void 0
|
|
3437
|
-
}, ISSUE_NOUN, (key) => copyIssue(
|
|
3936
|
+
}, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
3438
3937
|
}
|
|
3439
3938
|
function formatIssueRows(issues, nowMs) {
|
|
3440
3939
|
return alignedRows(issues, nowMs, (i) => ({
|
|
@@ -3457,20 +3956,20 @@ function collect(value, previous = []) {
|
|
|
3457
3956
|
}
|
|
3458
3957
|
function registerJira(jira) {
|
|
3459
3958
|
jira.description("Jira commands");
|
|
3460
|
-
jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(
|
|
3461
|
-
jira.command("statuses [query]").description("List statuses (optionally filtered by name, scoped with --project)").option("-p, --project <key>", "limit to statuses used by a project").option("--json", "output results as JSON").action(
|
|
3462
|
-
jira.command("create [project] [type]").description("Create a Jira issue, prompting for fields or taking them all as flags").option("-s, --summary <text>", "issue summary").option("-d, --description <markdown>", "description as Markdown").option("--description-file <path>", "read the description from a Markdown file").option("-a, --assignee <who>", "assignee: me, an account id, or a name to look up").option("--priority <name>", "priority name").option("-l, --label <label>", "label (repeatable, or comma separated)", collect).option("-c, --component <name>", "component (repeatable, or comma separated)", collect).option("--parent <key>", "parent issue key for subtasks").option("-f, --field <name=value>", "any other create-screen field (repeatable)", collect).option("--
|
|
3463
|
-
jira.command("fields <project> [type]").description("Show the issue types you can create in a project, or the create form for one type").option("--json", "output results as JSON").action(
|
|
3464
|
-
jira.command("view [issue]").description("Show a Jira issue (key or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(
|
|
3465
|
-
jira.command("copy [issue]").description("Copy a Jira issue (key or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(
|
|
3466
|
-
jira.command("update [file]").description("Update a Jira issue description from an edited Markdown file").option("--summary", "also push the H1 as the issue summary").option("-f, --force", "skip the stale-issue and data-loss checks").option("--dry-run", "show what would change without writing").action(
|
|
3467
|
-
jira.command("list").description("List open issues assigned to you").option("-p, --project <key>", "limit to a project").option("-a, --all", "include Done issues updated in the last 30 days").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(
|
|
3468
|
-
jira.command("search [query]").description("Search Jira issues (text query, filters, or --jql)").option("-p, --project <key>", "limit to a project").option("-a, --assignee <who>", "limit to an assignee (or 'me')").option("-s, --status <status>", "limit to a status").option("--jql <jql>", "raw JQL query (ignores other filters)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(
|
|
3959
|
+
jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(jiraAction(jiraProjects));
|
|
3960
|
+
jira.command("statuses [query]").description("List statuses (optionally filtered by name, scoped with --project)").option("-p, --project <key>", "limit to statuses used by a project").option("--json", "output results as JSON").action(jiraAction(jiraStatuses));
|
|
3961
|
+
jira.command("create [project] [type]").description("Create a Jira issue, prompting for fields or taking them all as flags").option("-s, --summary <text>", "issue summary").option("-d, --description <markdown>", "description as Markdown").option("--description-file <path>", "read the description from a Markdown file").option("-a, --assignee <who>", "assignee: me, an account id, or a name to look up").option("--priority <name>", "priority name").option("-l, --label <label>", "label (repeatable, or comma separated)", collect).option("-c, --component <name>", "component (repeatable, or comma separated)", collect).option("--parent <key>", "parent issue key for subtasks").option("-f, --field <name=value>", "any other create-screen field (repeatable)", collect).option("--dry-run", "print the resolved payload instead of creating").option("--json", "print the created issue as JSON").action(jiraAction(jiraCreate));
|
|
3962
|
+
jira.command("fields <project> [type]").description("Show the issue types you can create in a project, or the create form for one type").option("--json", "output results as JSON").action(jiraAction(jiraFields));
|
|
3963
|
+
jira.command("view [issue]").description("Show a Jira issue (key or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(jiraAction(jiraView));
|
|
3964
|
+
jira.command("copy [issue]").description("Copy a Jira issue (key or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(jiraAction(jiraCopy));
|
|
3965
|
+
jira.command("update [file]").description("Update a Jira issue description from an edited Markdown file").option("--summary", "also push the H1 as the issue summary").option("-f, --force", "skip the stale-issue and data-loss checks").option("--dry-run", "show what would change without writing").action(jiraAction(jiraUpdate));
|
|
3966
|
+
jira.command("list").description("List open issues assigned to you").option("-p, --project <key>", "limit to a project").option("-a, --all", "include Done issues updated in the last 30 days").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(jiraAction(jiraList));
|
|
3967
|
+
jira.command("search [query]").description("Search Jira issues (text query, filters, or --jql)").option("-p, --project <key>", "limit to a project").option("-a, --assignee <who>", "limit to an assignee (or 'me')").option("-s, --status <status>", "limit to a status").option("--jql <jql>", "raw JQL query (ignores other filters)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy").action(jiraAction(jiraSearch));
|
|
3469
3968
|
return jira;
|
|
3470
3969
|
}
|
|
3471
3970
|
//#endregion
|
|
3472
3971
|
//#region package.json
|
|
3473
|
-
var version = "1.
|
|
3972
|
+
var version = "1.14.0";
|
|
3474
3973
|
//#endregion
|
|
3475
3974
|
//#region src/cli/build.ts
|
|
3476
3975
|
const PRODUCTS = {
|
|
@@ -3478,16 +3977,22 @@ const PRODUCTS = {
|
|
|
3478
3977
|
confluence: registerConfluence,
|
|
3479
3978
|
bitbucket: registerBitbucket
|
|
3480
3979
|
};
|
|
3980
|
+
const NO_INPUT = "never prompt; fail if a required value is missing";
|
|
3981
|
+
function withNoInput(command) {
|
|
3982
|
+
command.option("--no-input", NO_INPUT);
|
|
3983
|
+
for (const sub of command.commands) withNoInput(sub);
|
|
3984
|
+
return command;
|
|
3985
|
+
}
|
|
3481
3986
|
function buildAtlass() {
|
|
3482
3987
|
const program = new Command().name("atlass").description("Copy Jira issues and Confluence pages to Markdown.").version(version);
|
|
3483
3988
|
registerAuth(program.command("auth"));
|
|
3484
3989
|
registerJira(program.command("jira"));
|
|
3485
3990
|
registerConfluence(program.command("confluence").alias("conf"));
|
|
3486
3991
|
registerBitbucket(program.command("bitbucket").alias("bb"));
|
|
3487
|
-
return program;
|
|
3992
|
+
return withNoInput(program);
|
|
3488
3993
|
}
|
|
3489
3994
|
function buildStandalone(product) {
|
|
3490
|
-
return PRODUCTS[product](new Command().name(product).version(version));
|
|
3995
|
+
return withNoInput(PRODUCTS[product](new Command().name(product).version(version)));
|
|
3491
3996
|
}
|
|
3492
3997
|
//#endregion
|
|
3493
3998
|
export { buildStandalone as n, fail as r, buildAtlass as t };
|