azure-pipelines-tui 0.5.2 → 0.5.3
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/dist/debug/debugOrgs.js +17 -0
- package/dist/debug/debugRetry.js +150 -0
- package/dist/debug/debugSignalR.js +151 -0
- package/dist/debug/debugWarnings.js +169 -0
- package/dist/lib/api.js +12 -0
- package/dist/lib/format.js +6 -5
- package/dist/screens/EnvironmentsScreen.js +57 -16
- package/dist/screens/OrgsScreen.js +102 -0
- package/dist/screens/ProjectsScreen.js +112 -0
- package/dist/tui.js +44 -14
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_js_1 = require("../lib/api.js");
|
|
4
|
+
async function main() {
|
|
5
|
+
const token = await (0, api_js_1.getToken)();
|
|
6
|
+
console.log("Token OK\n");
|
|
7
|
+
console.log("Fetching orgs…");
|
|
8
|
+
const orgs = await (0, api_js_1.fetchOrgs)(token);
|
|
9
|
+
console.log("Orgs:", orgs.map(o => o.accountName));
|
|
10
|
+
if (orgs.length > 0) {
|
|
11
|
+
const first = orgs[0].accountName;
|
|
12
|
+
console.log(`\nFetching projects for ${first}…`);
|
|
13
|
+
const projects = await (0, api_js_1.fetchProjects)(first, token);
|
|
14
|
+
console.log("Projects:", projects.map(p => p.name));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
main().catch(e => { console.error("ERROR:", e.message); process.exit(1); });
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* debugRetry.ts — standalone debug script for stage retry
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx tsx debugRetry.ts <org>/<project> <buildId> [stageRef]
|
|
8
|
+
* npx tsx debugRetry.ts IGH-Solution/IGH-Platform-Azure 12345
|
|
9
|
+
* npx tsx debugRetry.ts IGH-Solution/IGH-Platform-Azure 12345 MyStage
|
|
10
|
+
*
|
|
11
|
+
* Without stageRef: lists all stages with state/result.
|
|
12
|
+
* With stageRef: tries all state values (1 and 2) and forceRetryAllJobs combos.
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const https_1 = __importDefault(require("https"));
|
|
19
|
+
const child_process_1 = require("child_process");
|
|
20
|
+
const ADO_RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798";
|
|
21
|
+
const API_VER = "api-version=7.1";
|
|
22
|
+
const [orgProject, buildId, stageRef] = process.argv.slice(2);
|
|
23
|
+
if (!orgProject || !buildId) {
|
|
24
|
+
console.error("Usage: npx tsx debugRetry.ts <org>/<project> <buildId> [stageRef]");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const [org, ...rest] = orgProject.split("/");
|
|
28
|
+
const project = rest.join("/");
|
|
29
|
+
const ADO_BASE = `https://dev.azure.com/${encodeURIComponent(org)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}`;
|
|
30
|
+
function getToken() {
|
|
31
|
+
const raw = (0, child_process_1.execSync)(`az account get-access-token --resource ${ADO_RESOURCE} --output json`, { encoding: "utf8", env: { ...process.env, AZURE_CONFIG_DIR: process.env["AZURE_CONFIG_DIR"] } });
|
|
32
|
+
return JSON.parse(raw).accessToken;
|
|
33
|
+
}
|
|
34
|
+
function httpGet(url, token) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const u = new URL(url);
|
|
37
|
+
https_1.default.get({ hostname: u.hostname, path: u.pathname + u.search,
|
|
38
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } }, res => {
|
|
39
|
+
let data = "";
|
|
40
|
+
res.on("data", (c) => (data += c));
|
|
41
|
+
res.on("end", () => {
|
|
42
|
+
console.log(` GET ${url} → ${res.statusCode}`);
|
|
43
|
+
if ((res.statusCode ?? 0) >= 400)
|
|
44
|
+
return reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
45
|
+
resolve(JSON.parse(data));
|
|
46
|
+
});
|
|
47
|
+
}).on("error", reject);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function httpPatch(url, token, body) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const u = new URL(url);
|
|
53
|
+
const payload = JSON.stringify(body);
|
|
54
|
+
console.log(` PATCH ${url}`);
|
|
55
|
+
console.log(` Body: ${payload}`);
|
|
56
|
+
const req = https_1.default.request({
|
|
57
|
+
hostname: u.hostname,
|
|
58
|
+
path: u.pathname + u.search,
|
|
59
|
+
method: "PATCH",
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${token}`,
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
64
|
+
},
|
|
65
|
+
}, res => {
|
|
66
|
+
let data = "";
|
|
67
|
+
res.on("data", (c) => (data += c));
|
|
68
|
+
res.on("end", () => {
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(data);
|
|
72
|
+
}
|
|
73
|
+
catch { /* raw */ }
|
|
74
|
+
resolve({ status: res.statusCode ?? 0, body: data, parsed });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
req.on("error", (e) => resolve({ status: 0, body: String(e) }));
|
|
78
|
+
req.write(payload);
|
|
79
|
+
req.end();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function main() {
|
|
83
|
+
console.log(`\nOrg: ${org} Project: ${project} BuildId: ${buildId}`);
|
|
84
|
+
console.log("Fetching token…");
|
|
85
|
+
const token = getToken();
|
|
86
|
+
console.log("Token OK\n");
|
|
87
|
+
// Fetch timeline
|
|
88
|
+
const timeline = await httpGet(`${ADO_BASE}/timeline?${API_VER}`, token);
|
|
89
|
+
const stages = timeline.records
|
|
90
|
+
.filter(r => r.type === "Stage")
|
|
91
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
92
|
+
console.log("\n── Stages ────────────────────────────────────────────────────────");
|
|
93
|
+
for (const s of stages) {
|
|
94
|
+
const ref = s.identifier ?? s.name;
|
|
95
|
+
console.log(` [order=${s.order ?? "?"}] ${s.name.padEnd(40)} state=${s.state.padEnd(12)} result=${s.result ?? "(none)".padEnd(20)} ref="${ref}"`);
|
|
96
|
+
}
|
|
97
|
+
console.log("");
|
|
98
|
+
if (!stageRef) {
|
|
99
|
+
console.log("Pass a stageRef as third argument to test retry.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const target = stages.find(s => (s.identifier ?? s.name) === stageRef);
|
|
103
|
+
if (!target) {
|
|
104
|
+
console.warn(`Stage "${stageRef}" not found. Check spelling against the list above.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log(`\n── Testing retry on stage "${target.name}" (ref: ${stageRef}) ────`);
|
|
108
|
+
console.log(` Current state: ${target.state}, result: ${target.result ?? "(none)"}\n`);
|
|
109
|
+
const url = `${ADO_BASE}/stages/${encodeURIComponent(stageRef)}?${API_VER}`;
|
|
110
|
+
const combos = [
|
|
111
|
+
{ state: 1, forceRetryAllJobs: true, retryDependencies: true },
|
|
112
|
+
{ state: 1, forceRetryAllJobs: true, retryDependencies: false },
|
|
113
|
+
{ state: 1, forceRetryAllJobs: false, retryDependencies: true },
|
|
114
|
+
{ state: 1, forceRetryAllJobs: false, retryDependencies: false },
|
|
115
|
+
{ state: 2, forceRetryAllJobs: true, retryDependencies: true },
|
|
116
|
+
{ state: 2, forceRetryAllJobs: true, retryDependencies: false },
|
|
117
|
+
{ state: 2, forceRetryAllJobs: false, retryDependencies: true },
|
|
118
|
+
{ state: 2, forceRetryAllJobs: false, retryDependencies: false },
|
|
119
|
+
];
|
|
120
|
+
for (const combo of combos) {
|
|
121
|
+
console.log(`\n[state=${combo.state}, forceRetryAllJobs=${combo.forceRetryAllJobs}, retryDependencies=${combo.retryDependencies}]`);
|
|
122
|
+
const result = await httpPatch(url, token, combo);
|
|
123
|
+
console.log(` Response ${result.status}: ${result.body.slice(0, 300)}`);
|
|
124
|
+
if (result.status < 400) {
|
|
125
|
+
console.log(" ✓ SUCCESS — this combo works!");
|
|
126
|
+
await pollStageState(token, stageRef);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function pollStageState(token, ref, timeoutMs = 60_000, intervalMs = 2_000) {
|
|
132
|
+
console.log(`\n── Polling stage state (timeout ${timeoutMs / 1000}s) ────────────────`);
|
|
133
|
+
const deadline = Date.now() + timeoutMs;
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
const timeline = await httpGet(`${ADO_BASE}/timeline?${API_VER}`, token);
|
|
136
|
+
const stage = timeline.records.find(r => r.type === "Stage" && (r.identifier ?? r.name) === ref);
|
|
137
|
+
if (!stage) {
|
|
138
|
+
console.log(" Stage not found in timeline");
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
142
|
+
console.log(` [${ts}] state=${stage.state} result=${stage.result ?? "(none)"}`);
|
|
143
|
+
if (stage.state === "inProgress" || stage.state === "pending") {
|
|
144
|
+
console.log(" ✓ Stage transitioned — retry confirmed working.");
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Debug script: connect to Azure DevOps SignalR and dump all messages
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// tsx debugSignalR.ts https://dev.azure.com/ORG/PROJECT <buildId>
|
|
7
|
+
// tsx debugSignalR.ts https://dev.azure.com/ORG/PROJECT/_build/results?buildId=<id>
|
|
8
|
+
// tsx debugSignalR.ts https://dev.azure.com/ORG PROJECT <buildId>
|
|
9
|
+
// tsx debugSignalR.ts ORG/PROJECT <buildId>
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
const https_1 = __importDefault(require("https"));
|
|
15
|
+
const ws_1 = __importDefault(require("ws"));
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const url_1 = require("url");
|
|
18
|
+
// ── Arg parsing ───────────────────────────────────────────────────────────────
|
|
19
|
+
function showUsage() {
|
|
20
|
+
console.error("Usage:\n" +
|
|
21
|
+
" tsx debugSignalR.ts https://dev.azure.com/ORG/PROJECT <buildId>\n" +
|
|
22
|
+
" tsx debugSignalR.ts https://dev.azure.com/ORG/PROJECT/_build/results?buildId=<id>\n" +
|
|
23
|
+
" tsx debugSignalR.ts https://dev.azure.com/ORG PROJECT <buildId>\n" +
|
|
24
|
+
" tsx debugSignalR.ts ORG/PROJECT <buildId>");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const positional = process.argv.slice(2).filter(a => !a.startsWith("--"));
|
|
28
|
+
let ORG, PROJECT, BUILD_ID;
|
|
29
|
+
const first = positional[0] ?? "";
|
|
30
|
+
if (first.startsWith("http")) {
|
|
31
|
+
const u = new url_1.URL(first);
|
|
32
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
33
|
+
ORG = parts[0] ?? "";
|
|
34
|
+
const bidParam = u.searchParams.get("buildId");
|
|
35
|
+
if (parts.length >= 2 && !parts[1].startsWith("_")) {
|
|
36
|
+
// https://dev.azure.com/ORG/PROJECT[/...][?buildId=N]
|
|
37
|
+
PROJECT = parts[1];
|
|
38
|
+
BUILD_ID = bidParam ? Number(bidParam) : Number(positional[1] ?? "0");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// https://dev.azure.com/ORG PROJECT buildId
|
|
42
|
+
PROJECT = positional[1] ?? "";
|
|
43
|
+
BUILD_ID = Number(positional[2] ?? "0");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (first.includes("/")) {
|
|
47
|
+
// ORG/PROJECT buildId
|
|
48
|
+
const slash = first.indexOf("/");
|
|
49
|
+
ORG = first.slice(0, slash);
|
|
50
|
+
PROJECT = first.slice(slash + 1);
|
|
51
|
+
BUILD_ID = Number(positional[1] ?? "0");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
showUsage();
|
|
55
|
+
}
|
|
56
|
+
if (!ORG || !PROJECT || !BUILD_ID)
|
|
57
|
+
showUsage();
|
|
58
|
+
const ADO_RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798";
|
|
59
|
+
function getToken() {
|
|
60
|
+
const raw = (0, child_process_1.execSync)(`az account get-access-token --resource ${ADO_RESOURCE} --output json`, { encoding: "utf8" });
|
|
61
|
+
return JSON.parse(raw).accessToken;
|
|
62
|
+
}
|
|
63
|
+
function getProjectId(org, project) {
|
|
64
|
+
const raw = (0, child_process_1.execSync)(`az devops project show --project "${project}" --organization https://dev.azure.com/${org} --query id -o tsv`, { encoding: "utf8" });
|
|
65
|
+
return raw.trim();
|
|
66
|
+
}
|
|
67
|
+
function httpGet(url, token) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const u = new url_1.URL(url);
|
|
70
|
+
const req = https_1.default.get({ hostname: u.hostname, path: u.pathname + u.search,
|
|
71
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } }, res => {
|
|
72
|
+
// Follow redirects
|
|
73
|
+
if ((res.statusCode ?? 0) >= 300 && (res.statusCode ?? 0) < 400) {
|
|
74
|
+
const loc = res.headers["location"];
|
|
75
|
+
res.resume();
|
|
76
|
+
return resolve(httpGet(loc.startsWith("http") ? loc : `https://${u.hostname}${loc}`, token));
|
|
77
|
+
}
|
|
78
|
+
let data = "";
|
|
79
|
+
res.on("data", (c) => (data += c));
|
|
80
|
+
res.on("end", () => resolve(data));
|
|
81
|
+
});
|
|
82
|
+
req.on("error", reject);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function main() {
|
|
86
|
+
const token = getToken();
|
|
87
|
+
const projectId = getProjectId(ORG, PROJECT);
|
|
88
|
+
console.log("projectId:", projectId);
|
|
89
|
+
const orgEncoded = encodeURIComponent(ORG);
|
|
90
|
+
// 1. Get instanceId (org-level, used for negotiate)
|
|
91
|
+
const connData = JSON.parse(await httpGet(`https://dev.azure.com/${orgEncoded}/_apis/connectionData?connectOptions=0&lastChangeId=-1&lastChangeId64=-1`, token));
|
|
92
|
+
console.log("instanceId (for negotiate):", connData.instanceId);
|
|
93
|
+
// 2. Negotiate with instanceId → returns contextToken
|
|
94
|
+
const CONNECTION_DATA = encodeURIComponent(JSON.stringify([
|
|
95
|
+
{ name: "builddetailhub" },
|
|
96
|
+
{ name: "taskagentpoolhub" },
|
|
97
|
+
]));
|
|
98
|
+
const negotiateUrl = `https://dev.azure.com/_signalr/${orgEncoded}/_apis/${connData.instanceId}/signalr/negotiate` +
|
|
99
|
+
`?transport=webSockets&clientProtocol=1.5&connectionData=${CONNECTION_DATA}&_=${Date.now()}`;
|
|
100
|
+
console.log("negotiate URL:", negotiateUrl);
|
|
101
|
+
const negotiated = JSON.parse(await httpGet(negotiateUrl, token));
|
|
102
|
+
console.log("negotiate response:", JSON.stringify(negotiated, null, 2));
|
|
103
|
+
// Extract contextToken from Url field
|
|
104
|
+
const contextToken = negotiated.Url?.match(/\/_apis\/([0-9a-f-]{36})\//i)?.[1];
|
|
105
|
+
console.log("contextToken:", contextToken);
|
|
106
|
+
// 3. Connect WebSocket — projectId in path, contextToken (from negotiate) as query param
|
|
107
|
+
const wsUrl = `wss://dev.azure.com/_signalr/${orgEncoded}/_apis/${projectId}/signalr/connect` +
|
|
108
|
+
`?transport=webSockets&clientProtocol=1.5` +
|
|
109
|
+
`&connectionToken=${encodeURIComponent(negotiated.ConnectionToken)}` +
|
|
110
|
+
`&connectionData=${CONNECTION_DATA}` +
|
|
111
|
+
(contextToken ? `&contextToken=${encodeURIComponent(contextToken)}` : "") +
|
|
112
|
+
`&tid=0`;
|
|
113
|
+
console.log("WebSocket URL:", wsUrl);
|
|
114
|
+
const ws = new ws_1.default(wsUrl, {
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${token}`,
|
|
117
|
+
"Sec-WebSocket-Protocol": `Bearer, ${token}`,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
ws.on("open", async () => {
|
|
121
|
+
console.log("WebSocket open");
|
|
122
|
+
// Start
|
|
123
|
+
try {
|
|
124
|
+
const startResp = await httpGet(`https://dev.azure.com/_signalr/${orgEncoded}/_apis/${connData.instanceId}/signalr/start` +
|
|
125
|
+
`?transport=webSockets&clientProtocol=1.5` +
|
|
126
|
+
`&connectionToken=${encodeURIComponent(negotiated.ConnectionToken)}` +
|
|
127
|
+
`&connectionData=${CONNECTION_DATA}&_=${Date.now()}`, token);
|
|
128
|
+
console.log("start response:", startResp);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
console.log("start error:", e.message);
|
|
132
|
+
}
|
|
133
|
+
// WatchBuild(projectId: Guid, buildId: Int32) — confirmed signature
|
|
134
|
+
const msg = JSON.stringify({ H: "builddetailhub", M: "WatchBuild", A: [projectId, BUILD_ID], I: "1" });
|
|
135
|
+
console.log("→ sending:", msg);
|
|
136
|
+
ws.send(msg);
|
|
137
|
+
});
|
|
138
|
+
ws.on("message", (raw) => {
|
|
139
|
+
const text = raw.toString();
|
|
140
|
+
if (text === "{}") {
|
|
141
|
+
process.stdout.write(".");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
console.log("\n← received:", text);
|
|
145
|
+
});
|
|
146
|
+
ws.on("close", () => { console.log("\nWebSocket closed"); process.exit(0); });
|
|
147
|
+
ws.on("error", (e) => { console.log("WebSocket error:", e.message); });
|
|
148
|
+
// Run for 2 minutes
|
|
149
|
+
setTimeout(() => { console.log("\nTimeout — closing"); ws.close(); }, 120_000);
|
|
150
|
+
}
|
|
151
|
+
main().catch(e => { console.error("Fatal:", e.message); process.exit(1); });
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* debugWarnings.ts — report warning counts per stage/job/task for a build
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx tsx debugWarnings.ts <org>/<project> <buildId> [--logs]
|
|
8
|
+
*
|
|
9
|
+
* Without --logs: prints a tree of stages/jobs/tasks with warning counts.
|
|
10
|
+
* With --logs: also fetches log content and prints each ##[warning] line.
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const https_1 = __importDefault(require("https"));
|
|
17
|
+
const child_process_1 = require("child_process");
|
|
18
|
+
const ADO_RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798";
|
|
19
|
+
const API_VER = "api-version=7.1";
|
|
20
|
+
const rawArgs = process.argv.slice(2);
|
|
21
|
+
const [orgProject, buildId] = rawArgs.filter(a => !a.startsWith("--"));
|
|
22
|
+
const showLogs = rawArgs.includes("--logs");
|
|
23
|
+
if (!orgProject || !buildId) {
|
|
24
|
+
console.error("Usage: npx tsx debugWarnings.ts <org>/<project> <buildId> [--logs]");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const [org, ...rest] = orgProject.split("/");
|
|
28
|
+
const project = rest.join("/");
|
|
29
|
+
const enc = encodeURIComponent;
|
|
30
|
+
const ADO_BASE = `https://dev.azure.com/${enc(org)}/${enc(project)}/_apis/build/builds/${buildId}`;
|
|
31
|
+
function getToken() {
|
|
32
|
+
const raw = (0, child_process_1.execSync)(`az account get-access-token --resource ${ADO_RESOURCE} --output json`, { encoding: "utf8", env: { ...process.env, AZURE_CONFIG_DIR: process.env["AZURE_CONFIG_DIR"] } });
|
|
33
|
+
return JSON.parse(raw).accessToken;
|
|
34
|
+
}
|
|
35
|
+
function httpGet(url, token) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const u = new URL(url);
|
|
38
|
+
https_1.default.get({ hostname: u.hostname, path: u.pathname + u.search,
|
|
39
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } }, res => {
|
|
40
|
+
let data = "";
|
|
41
|
+
res.on("data", (c) => (data += c));
|
|
42
|
+
res.on("end", () => {
|
|
43
|
+
if ((res.statusCode ?? 0) >= 400)
|
|
44
|
+
return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 300)}`));
|
|
45
|
+
resolve(JSON.parse(data));
|
|
46
|
+
});
|
|
47
|
+
}).on("error", reject);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function httpGetText(url, token) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const u = new URL(url);
|
|
53
|
+
https_1.default.get({ hostname: u.hostname, path: u.pathname + u.search,
|
|
54
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "text/plain" } }, res => {
|
|
55
|
+
let data = "";
|
|
56
|
+
res.on("data", (c) => (data += c));
|
|
57
|
+
res.on("end", () => {
|
|
58
|
+
if ((res.statusCode ?? 0) >= 400)
|
|
59
|
+
return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 300)}`));
|
|
60
|
+
resolve(data);
|
|
61
|
+
});
|
|
62
|
+
}).on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function badge(warnings, errors) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (warnings > 0)
|
|
68
|
+
parts.push(`⚠ ${warnings}w`);
|
|
69
|
+
if (errors > 0)
|
|
70
|
+
parts.push(`✗ ${errors}e`);
|
|
71
|
+
return parts.length ? ` [${parts.join(" ")}]` : "";
|
|
72
|
+
}
|
|
73
|
+
async function main() {
|
|
74
|
+
console.log(`\nOrg: ${org} Project: ${project} BuildId: ${buildId}`);
|
|
75
|
+
console.log("Fetching token…");
|
|
76
|
+
const token = getToken();
|
|
77
|
+
console.log("Token OK\n");
|
|
78
|
+
const timeline = await httpGet(`${ADO_BASE}/timeline?${API_VER}`, token);
|
|
79
|
+
const records = timeline.records ?? [];
|
|
80
|
+
// Build parent → children index
|
|
81
|
+
const children = new Map();
|
|
82
|
+
for (const r of records) {
|
|
83
|
+
const p = r.parentId ?? null;
|
|
84
|
+
if (!children.has(p))
|
|
85
|
+
children.set(p, []);
|
|
86
|
+
children.get(p).push(r);
|
|
87
|
+
}
|
|
88
|
+
// Stages = top-level records of type Stage
|
|
89
|
+
const stages = (children.get(null) ?? [])
|
|
90
|
+
.filter(r => r.type === "Stage")
|
|
91
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
92
|
+
// Totals from leaf records only (Tasks have no children) to avoid double-counting.
|
|
93
|
+
// Stage/Phase/Job warningCount is often 0 even when children have warnings.
|
|
94
|
+
const hasChildren = new Set(records.map(r => r.parentId).filter(Boolean));
|
|
95
|
+
let totalWarnings = 0;
|
|
96
|
+
let totalErrors = 0;
|
|
97
|
+
for (const r of records) {
|
|
98
|
+
if (!hasChildren.has(r.id)) {
|
|
99
|
+
totalWarnings += r.warningCount ?? 0;
|
|
100
|
+
totalErrors += r.errorCount ?? 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function sumDescendants(nodeId) {
|
|
104
|
+
const kids = children.get(nodeId) ?? [];
|
|
105
|
+
if (kids.length === 0)
|
|
106
|
+
return { warnings: 0, errors: 0 };
|
|
107
|
+
let w = 0, e = 0;
|
|
108
|
+
for (const kid of kids) {
|
|
109
|
+
if (!hasChildren.has(kid.id)) {
|
|
110
|
+
w += kid.warningCount ?? 0;
|
|
111
|
+
e += kid.errorCount ?? 0;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const s = sumDescendants(kid.id);
|
|
115
|
+
w += s.warnings;
|
|
116
|
+
e += s.errors;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { warnings: w, errors: e };
|
|
120
|
+
}
|
|
121
|
+
// Collect tasks with logs that have warnings (for --logs mode)
|
|
122
|
+
const warningTasks = [];
|
|
123
|
+
function printTree(nodes, indent) {
|
|
124
|
+
const sorted = [...nodes].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
125
|
+
for (const node of sorted) {
|
|
126
|
+
const w = node.warningCount ?? 0;
|
|
127
|
+
const e = node.errorCount ?? 0;
|
|
128
|
+
const b = badge(w, e);
|
|
129
|
+
console.log(`${indent}${node.type.padEnd(6)} ${node.name}${b}`);
|
|
130
|
+
if (showLogs && w > 0 && node.log?.url) {
|
|
131
|
+
warningTasks.push({ name: node.name, logUrl: node.log.url });
|
|
132
|
+
}
|
|
133
|
+
const kids = children.get(node.id) ?? [];
|
|
134
|
+
if (kids.length)
|
|
135
|
+
printTree(kids, indent + " ");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
console.log("── Warning/Error tree ────────────────────────────────────────────");
|
|
139
|
+
for (const stage of stages) {
|
|
140
|
+
const { warnings: sw, errors: se } = sumDescendants(stage.id);
|
|
141
|
+
console.log(`\nSTAGE ${stage.name} (state=${stage.state} result=${stage.result ?? "—"})${badge(sw, se)}`);
|
|
142
|
+
const kids = children.get(stage.id) ?? [];
|
|
143
|
+
printTree(kids, " ");
|
|
144
|
+
}
|
|
145
|
+
console.log(`\n── Totals ────────────────────────────────────────────────────────`);
|
|
146
|
+
console.log(` Warnings: ${totalWarnings}`);
|
|
147
|
+
console.log(` Errors : ${totalErrors}`);
|
|
148
|
+
if (!showLogs || warningTasks.length === 0)
|
|
149
|
+
return;
|
|
150
|
+
console.log(`\n── Warning log lines (${warningTasks.length} tasks) ──────────────────────────`);
|
|
151
|
+
for (const task of warningTasks) {
|
|
152
|
+
console.log(`\n [${task.name}]`);
|
|
153
|
+
try {
|
|
154
|
+
const text = await httpGetText(`${task.logUrl}?${API_VER}`, token);
|
|
155
|
+
const lines = text.split("\n").filter(l => /##\[warning\]/i.test(l));
|
|
156
|
+
if (lines.length === 0) {
|
|
157
|
+
console.log(" (no ##[warning] lines found in log)");
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
for (const line of lines)
|
|
161
|
+
console.log(` ${line.trim()}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.log(` Error fetching log: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
package/dist/lib/api.js
CHANGED
|
@@ -21,6 +21,8 @@ exports.buildBase = buildBase;
|
|
|
21
21
|
exports.fetchBuild = fetchBuild;
|
|
22
22
|
exports.fetchTimeline = fetchTimeline;
|
|
23
23
|
exports.fetchLogLines = fetchLogLines;
|
|
24
|
+
exports.fetchOrgs = fetchOrgs;
|
|
25
|
+
exports.fetchProjects = fetchProjects;
|
|
24
26
|
const https_1 = __importDefault(require("https"));
|
|
25
27
|
const child_process_1 = require("child_process");
|
|
26
28
|
const url_1 = require("url");
|
|
@@ -254,3 +256,13 @@ async function fetchTimeline(org, project, buildId, token) {
|
|
|
254
256
|
async function fetchLogLines(org, project, buildId, logId, startLine, token) {
|
|
255
257
|
return httpGet(`${buildBase(org, project, buildId)}/logs/${logId}?startLine=${startLine}&${exports.API_VER}`, token).catch(() => null);
|
|
256
258
|
}
|
|
259
|
+
async function fetchOrgs(token) {
|
|
260
|
+
const conn = await httpGet(`https://app.vssps.visualstudio.com/_apis/connectionData`, token);
|
|
261
|
+
const memberId = conn.authenticatedUser.id;
|
|
262
|
+
const data = await httpGet(`https://app.vssps.visualstudio.com/_apis/accounts?memberId=${memberId}&api-version=7.1-preview.1`, token);
|
|
263
|
+
return (data.value ?? []).sort((a, b) => a.accountName.localeCompare(b.accountName));
|
|
264
|
+
}
|
|
265
|
+
async function fetchProjects(org, token) {
|
|
266
|
+
const data = await httpGet(`https://dev.azure.com/${(0, exports.enc)(org)}/_apis/projects?$top=200&stateFilter=wellFormed&${exports.API_VER}`, token);
|
|
267
|
+
return (data.value ?? []).sort((a, b) => a.name.localeCompare(b.name));
|
|
268
|
+
}
|
package/dist/lib/format.js
CHANGED
|
@@ -191,15 +191,16 @@ function formatEnvItem(item) {
|
|
|
191
191
|
}
|
|
192
192
|
let stats = "";
|
|
193
193
|
if (item.ok > 0)
|
|
194
|
-
stats += `
|
|
194
|
+
stats += `{green-fg}${item.ok}✓{/} `;
|
|
195
195
|
if (item.fail > 0)
|
|
196
|
-
stats += `
|
|
196
|
+
stats += `{red-fg}${item.fail}✗{/} `;
|
|
197
197
|
const other = item.total - item.ok - item.fail;
|
|
198
198
|
if (other > 0)
|
|
199
|
-
stats += `
|
|
199
|
+
stats += `{gray-fg}${other}…{/}`;
|
|
200
200
|
if (!stats && item.total === 0)
|
|
201
|
-
stats = `
|
|
202
|
-
|
|
201
|
+
stats = `{gray-fg}empty{/}`;
|
|
202
|
+
const pipeBlank = " " + " ".repeat(36);
|
|
203
|
+
return `{bold}${left}{/}${pipeBlank} ${stats.trim()}`;
|
|
203
204
|
}
|
|
204
205
|
const pfx = indent + (item.isLast ? "└─ " : "├─ ");
|
|
205
206
|
const label = padEnd(item.label, Math.max(1, exports.LEFT_COL - 1 - pfx.length));
|
|
@@ -47,9 +47,14 @@ class EnvironmentsScreen {
|
|
|
47
47
|
rows = [];
|
|
48
48
|
flatItems = [];
|
|
49
49
|
collapsed = new Set();
|
|
50
|
+
helpVisible = false;
|
|
50
51
|
get footerText() {
|
|
51
|
-
|
|
52
|
-
"{cyan-fg}
|
|
52
|
+
if (this.helpVisible) {
|
|
53
|
+
return (" {cyan-fg}↑↓{/} Navigate {cyan-fg}Enter{/} Run/Expand {cyan-fg}s{/} Stages {cyan-fg}←→{/} Collapse/Expand" +
|
|
54
|
+
" {cyan-fg}b{/} Browser {cyan-fg}p{/} Pipelines {cyan-fg}m{/} Mapping" +
|
|
55
|
+
" {cyan-fg}r{/} Refresh {cyan-fg}c{/} Clear cache {cyan-fg}h{/} Close help {cyan-fg}q{/} Quit");
|
|
56
|
+
}
|
|
57
|
+
return " {cyan-fg}↑↓{/} Navigate {cyan-fg}Enter{/} Run/Expand {cyan-fg}s{/} Stages {cyan-fg}p{/} Pipelines {cyan-fg}←→{/} Collapse/Expand {cyan-fg}b{/} Browser {cyan-fg}h{/} Help {cyan-fg}q{/} Quit";
|
|
53
58
|
}
|
|
54
59
|
constructor(screen, ctx) {
|
|
55
60
|
this.screen = screen;
|
|
@@ -98,24 +103,25 @@ class EnvironmentsScreen {
|
|
|
98
103
|
const idx = this.widget.selected ?? 0;
|
|
99
104
|
return this.flatItems[idx];
|
|
100
105
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
navigateRunForRow(row) {
|
|
107
|
+
const bid = row.deploy?.owner?.id ? Number(row.deploy.owner.id) : 0;
|
|
108
|
+
if (bid)
|
|
109
|
+
this.ctx.navigate({ view: "pipelineRun", buildId: String(bid) });
|
|
110
|
+
else
|
|
111
|
+
this.ctx.setStatus("No deployment found for this environment");
|
|
112
|
+
}
|
|
113
|
+
async navigateStagesForRow(row) {
|
|
114
|
+
const pipelineId = row.mapping?.pipelineId ?? row.deploy?.definition?.id;
|
|
115
|
+
if (!pipelineId) {
|
|
116
|
+
this.ctx.setStatus("No pipeline found for this environment");
|
|
111
117
|
return;
|
|
112
118
|
}
|
|
113
|
-
let pip = this.ctx.state.pipelines.find(p => p.id ===
|
|
119
|
+
let pip = this.ctx.state.pipelines.find(p => p.id === pipelineId);
|
|
114
120
|
if (!pip) {
|
|
115
121
|
this.ctx.setStatus("Loading pipeline definitions…", 0);
|
|
116
122
|
try {
|
|
117
123
|
await this.ctx.loadPipelineDefinitions();
|
|
118
|
-
pip = this.ctx.state.pipelines.find(p => p.id ===
|
|
124
|
+
pip = this.ctx.state.pipelines.find(p => p.id === pipelineId);
|
|
119
125
|
}
|
|
120
126
|
catch (e) {
|
|
121
127
|
this.ctx.setStatus(`Error: ${e.message.slice(0, 80)}`, 5000);
|
|
@@ -125,7 +131,19 @@ class EnvironmentsScreen {
|
|
|
125
131
|
if (pip)
|
|
126
132
|
this.ctx.navigate({ view: "stages", pipeline: pip });
|
|
127
133
|
else
|
|
128
|
-
this.ctx.setStatus(`Pipeline ${
|
|
134
|
+
this.ctx.setStatus(`Pipeline ${pipelineId} not found`);
|
|
135
|
+
}
|
|
136
|
+
openBrowserForRow(row) {
|
|
137
|
+
const bid = row.deploy?.owner?.id ? Number(row.deploy.owner.id) : 0;
|
|
138
|
+
if (!bid) {
|
|
139
|
+
this.ctx.setStatus("No deployment found for this environment");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const url = `https://dev.azure.com/${this.ctx.org}/${this.ctx.project}/_build/results?buildId=${bid}`;
|
|
143
|
+
try {
|
|
144
|
+
(0, child_process_1.spawn)("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
129
147
|
}
|
|
130
148
|
async loadData() {
|
|
131
149
|
this.ctx.setStatus("Loading environments…", 0);
|
|
@@ -181,6 +199,22 @@ class EnvironmentsScreen {
|
|
|
181
199
|
this.loadData();
|
|
182
200
|
});
|
|
183
201
|
this.widget.key("c", () => { (0, cache_js_1.clearAllCache)(); this.ctx.setStatus("All caches cleared"); });
|
|
202
|
+
this.widget.key("h", () => {
|
|
203
|
+
this.helpVisible = !this.helpVisible;
|
|
204
|
+
this.ctx.setStatus("", 0);
|
|
205
|
+
});
|
|
206
|
+
this.widget.key("b", () => {
|
|
207
|
+
const item = this.selected();
|
|
208
|
+
if (!item || item.kind !== "leaf")
|
|
209
|
+
return;
|
|
210
|
+
this.openBrowserForRow(item.row);
|
|
211
|
+
});
|
|
212
|
+
this.widget.key("s", () => {
|
|
213
|
+
const item = this.selected();
|
|
214
|
+
if (!item || item.kind !== "leaf")
|
|
215
|
+
return;
|
|
216
|
+
this.navigateStagesForRow(item.row);
|
|
217
|
+
});
|
|
184
218
|
this.widget.key("enter", () => {
|
|
185
219
|
const item = this.selected();
|
|
186
220
|
if (!item)
|
|
@@ -193,7 +227,7 @@ class EnvironmentsScreen {
|
|
|
193
227
|
this.refresh();
|
|
194
228
|
}
|
|
195
229
|
else {
|
|
196
|
-
this.
|
|
230
|
+
this.navigateRunForRow(item.row);
|
|
197
231
|
}
|
|
198
232
|
});
|
|
199
233
|
this.widget.key("left", () => {
|
|
@@ -215,6 +249,13 @@ class EnvironmentsScreen {
|
|
|
215
249
|
}
|
|
216
250
|
}
|
|
217
251
|
});
|
|
252
|
+
this.widget.key("right", () => {
|
|
253
|
+
const item = this.selected();
|
|
254
|
+
if (!item || item.kind !== "group" || item.isExpanded)
|
|
255
|
+
return;
|
|
256
|
+
this.collapsed.delete(item.key);
|
|
257
|
+
this.refresh();
|
|
258
|
+
});
|
|
218
259
|
}
|
|
219
260
|
}
|
|
220
261
|
exports.EnvironmentsScreen = EnvironmentsScreen;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.OrgsScreen = void 0;
|
|
37
|
+
const blessed = __importStar(require("blessed"));
|
|
38
|
+
const api_js_1 = require("../lib/api.js");
|
|
39
|
+
class OrgsScreen {
|
|
40
|
+
screen;
|
|
41
|
+
ctx;
|
|
42
|
+
widget;
|
|
43
|
+
orgs = [];
|
|
44
|
+
get footerText() {
|
|
45
|
+
return " {cyan-fg}↑↓{/} Navigate {cyan-fg}Enter{/} Select org {cyan-fg}r{/} Refresh {cyan-fg}q{/} Quit";
|
|
46
|
+
}
|
|
47
|
+
constructor(screen, ctx) {
|
|
48
|
+
this.screen = screen;
|
|
49
|
+
this.ctx = ctx;
|
|
50
|
+
this.widget = blessed.list({
|
|
51
|
+
parent: screen, top: 1, left: 0, width: "100%", height: "100%-3",
|
|
52
|
+
border: { type: "line" }, label: " Azure DevOps Organizations ",
|
|
53
|
+
tags: true, keys: true, vi: true, scrollable: true,
|
|
54
|
+
scrollbar: { ch: "│", style: { fg: "blue" } },
|
|
55
|
+
style: {
|
|
56
|
+
border: { fg: "cyan" }, selected: { bg: "blue", fg: "white" },
|
|
57
|
+
focus: { border: { fg: "white" } },
|
|
58
|
+
},
|
|
59
|
+
items: [], hidden: true,
|
|
60
|
+
});
|
|
61
|
+
this.registerKeys();
|
|
62
|
+
}
|
|
63
|
+
show() {
|
|
64
|
+
this.widget.show();
|
|
65
|
+
this.widget.focus();
|
|
66
|
+
if (this.orgs.length === 0)
|
|
67
|
+
this.load();
|
|
68
|
+
else
|
|
69
|
+
this.render();
|
|
70
|
+
}
|
|
71
|
+
hide() { this.widget.hide(); }
|
|
72
|
+
async load() {
|
|
73
|
+
this.ctx.setStatus("Loading organizations…", 0);
|
|
74
|
+
try {
|
|
75
|
+
const token = await this.ctx.getToken();
|
|
76
|
+
this.orgs = await (0, api_js_1.fetchOrgs)(token);
|
|
77
|
+
this.ctx.setStatus("", 0);
|
|
78
|
+
this.render();
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
this.ctx.setStatus(`Error: ${e.message.slice(0, 80)}`, 10_000);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
render() {
|
|
85
|
+
this.widget.setItems(this.orgs.map(o => o.accountName));
|
|
86
|
+
this.screen.render();
|
|
87
|
+
}
|
|
88
|
+
registerKeys() {
|
|
89
|
+
this.widget.key("enter", () => {
|
|
90
|
+
const idx = this.widget.selected ?? 0;
|
|
91
|
+
const org = this.orgs[idx];
|
|
92
|
+
if (!org)
|
|
93
|
+
return;
|
|
94
|
+
this.ctx.navigate({ view: "projects", org: org.accountName });
|
|
95
|
+
});
|
|
96
|
+
this.widget.key("r", () => {
|
|
97
|
+
this.orgs = [];
|
|
98
|
+
this.load();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.OrgsScreen = OrgsScreen;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ProjectsScreen = void 0;
|
|
37
|
+
const blessed = __importStar(require("blessed"));
|
|
38
|
+
const api_js_1 = require("../lib/api.js");
|
|
39
|
+
class ProjectsScreen {
|
|
40
|
+
screen;
|
|
41
|
+
ctx;
|
|
42
|
+
widget;
|
|
43
|
+
org = "";
|
|
44
|
+
projects = [];
|
|
45
|
+
get footerText() {
|
|
46
|
+
return " {cyan-fg}↑↓{/} Navigate {cyan-fg}Enter{/} Select project {cyan-fg}Esc{/} Back {cyan-fg}r{/} Refresh {cyan-fg}q{/} Quit";
|
|
47
|
+
}
|
|
48
|
+
constructor(screen, ctx) {
|
|
49
|
+
this.screen = screen;
|
|
50
|
+
this.ctx = ctx;
|
|
51
|
+
this.widget = blessed.list({
|
|
52
|
+
parent: screen, top: 1, left: 0, width: "100%", height: "100%-3",
|
|
53
|
+
border: { type: "line" }, label: " Projects ",
|
|
54
|
+
tags: true, keys: true, vi: true, scrollable: true,
|
|
55
|
+
scrollbar: { ch: "│", style: { fg: "blue" } },
|
|
56
|
+
style: {
|
|
57
|
+
border: { fg: "cyan" }, selected: { bg: "blue", fg: "white" },
|
|
58
|
+
focus: { border: { fg: "white" } },
|
|
59
|
+
},
|
|
60
|
+
items: [], hidden: true,
|
|
61
|
+
});
|
|
62
|
+
this.registerKeys();
|
|
63
|
+
}
|
|
64
|
+
show(org) {
|
|
65
|
+
if (org !== this.org) {
|
|
66
|
+
this.org = org;
|
|
67
|
+
this.projects = [];
|
|
68
|
+
}
|
|
69
|
+
this.widget.setLabel(` Projects — ${org} `);
|
|
70
|
+
this.widget.show();
|
|
71
|
+
this.widget.focus();
|
|
72
|
+
if (this.projects.length === 0)
|
|
73
|
+
this.load();
|
|
74
|
+
else
|
|
75
|
+
this.render();
|
|
76
|
+
}
|
|
77
|
+
hide() { this.widget.hide(); }
|
|
78
|
+
async load() {
|
|
79
|
+
this.ctx.setStatus(`Loading projects for ${this.org}…`, 0);
|
|
80
|
+
try {
|
|
81
|
+
const token = await this.ctx.getToken();
|
|
82
|
+
this.projects = await (0, api_js_1.fetchProjects)(this.org, token);
|
|
83
|
+
this.ctx.setStatus("", 0);
|
|
84
|
+
this.render();
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
this.ctx.setStatus(`Error: ${e.message.slice(0, 80)}`, 10_000);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
render() {
|
|
91
|
+
this.widget.setItems(this.projects.map(p => p.name));
|
|
92
|
+
this.screen.render();
|
|
93
|
+
}
|
|
94
|
+
registerKeys() {
|
|
95
|
+
this.widget.key("enter", () => {
|
|
96
|
+
const idx = this.widget.selected ?? 0;
|
|
97
|
+
const project = this.projects[idx];
|
|
98
|
+
if (!project)
|
|
99
|
+
return;
|
|
100
|
+
this.ctx.setOrgProject(this.org, project.name);
|
|
101
|
+
this.ctx.navigate({ view: "pipelines" });
|
|
102
|
+
});
|
|
103
|
+
this.widget.key(["escape", "backspace"], () => {
|
|
104
|
+
this.ctx.navigate({ view: "orgs" });
|
|
105
|
+
});
|
|
106
|
+
this.widget.key("r", () => {
|
|
107
|
+
this.projects = [];
|
|
108
|
+
this.load();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.ProjectsScreen = ProjectsScreen;
|
package/dist/tui.js
CHANGED
|
@@ -48,18 +48,20 @@ const StagesScreen_js_1 = require("./screens/StagesScreen.js");
|
|
|
48
48
|
const PipelineRunsScreen_js_1 = require("./screens/PipelineRunsScreen.js");
|
|
49
49
|
const PipelineRunScreen_js_1 = require("./screens/PipelineRunScreen.js");
|
|
50
50
|
const MappingScreen_js_1 = require("./screens/MappingScreen.js");
|
|
51
|
+
const OrgsScreen_js_1 = require("./screens/OrgsScreen.js");
|
|
52
|
+
const ProjectsScreen_js_1 = require("./screens/ProjectsScreen.js");
|
|
51
53
|
// ── CLI args ──────────────────────────────────────────────────────────────────
|
|
52
54
|
function showHelp() {
|
|
53
55
|
console.log(`
|
|
54
56
|
Azure Pipelines TUI
|
|
55
57
|
|
|
56
58
|
Usage:
|
|
57
|
-
npx
|
|
58
|
-
npx
|
|
59
|
-
npx
|
|
60
|
-
npx
|
|
61
|
-
npx
|
|
62
|
-
npx
|
|
59
|
+
npx azure-pipelines-tui ORG/PROJECT Pipelines Overview (default)
|
|
60
|
+
npx azure-pipelines-tui ORG/PROJECT --envs Environments Overview
|
|
61
|
+
npx azure-pipelines-tui ORG/PROJECT --stages <id> Stages Dashboard
|
|
62
|
+
npx azure-pipelines-tui ORG/PROJECT --runs <id> Pipeline Runs List
|
|
63
|
+
npx azure-pipelines-tui <build-url> Pipeline Run (single build)
|
|
64
|
+
npx azure-pipelines-tui ORG/PROJECT <buildId> Pipeline Run (single build)
|
|
63
65
|
|
|
64
66
|
Options:
|
|
65
67
|
--config <file> Config file (default: environments-config.json)
|
|
@@ -140,6 +142,8 @@ else if (RUNS_ARG)
|
|
|
140
142
|
INITIAL_VIEW = "runs";
|
|
141
143
|
else if (ENVS_FLAG)
|
|
142
144
|
INITIAL_VIEW = "environments";
|
|
145
|
+
else if (!ORG)
|
|
146
|
+
INITIAL_VIEW = "orgs";
|
|
143
147
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
144
148
|
async function main() {
|
|
145
149
|
const config = (0, api_js_1.loadConfig)(CONFIG_FILE);
|
|
@@ -147,13 +151,15 @@ async function main() {
|
|
|
147
151
|
ORG = config.org ?? "";
|
|
148
152
|
if (!PROJECT)
|
|
149
153
|
PROJECT = config.project ?? "";
|
|
150
|
-
if (!ORG || !PROJECT) {
|
|
154
|
+
if ((!ORG || !PROJECT) && INITIAL_VIEW !== "orgs") {
|
|
151
155
|
console.error("Error: org/project required. Pass as 'org/project' argument or set in environments-config.json.\n" +
|
|
152
156
|
"Run with --help for usage.");
|
|
153
157
|
process.exit(1);
|
|
154
158
|
}
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
if (ORG)
|
|
160
|
+
config.org = ORG;
|
|
161
|
+
if (PROJECT)
|
|
162
|
+
config.project = PROJECT;
|
|
157
163
|
// ── Shared state ──────────────────────────────────────────────────────────
|
|
158
164
|
const state = { pipelines: [] };
|
|
159
165
|
// ── Screen + widgets ──────────────────────────────────────────────────────
|
|
@@ -169,7 +175,7 @@ async function main() {
|
|
|
169
175
|
const headerBox = blessed.box({
|
|
170
176
|
parent: screen, top: 0, left: 0, width: "100%", height: 1, tags: true,
|
|
171
177
|
style: { bg: "blue", fg: "white", bold: true },
|
|
172
|
-
content: ` {bold}Azure Pipelines TUI{/bold}
|
|
178
|
+
content: ` {bold}Azure Pipelines TUI{/bold}`,
|
|
173
179
|
});
|
|
174
180
|
const footerBox = blessed.box({
|
|
175
181
|
parent: screen, bottom: 0, left: 0, width: "100%", height: 1, tags: true,
|
|
@@ -192,7 +198,13 @@ async function main() {
|
|
|
192
198
|
let currentDest = { view: "pipelines" };
|
|
193
199
|
function updateHeader() {
|
|
194
200
|
let content;
|
|
195
|
-
if (currentView === "
|
|
201
|
+
if (currentView === "orgs") {
|
|
202
|
+
content = ` {bold}Azure Pipelines TUI{/bold} Select organization`;
|
|
203
|
+
}
|
|
204
|
+
else if (currentView === "projects") {
|
|
205
|
+
content = ` {bold}Azure Pipelines TUI{/bold} ${ORG} — Select project`;
|
|
206
|
+
}
|
|
207
|
+
else if (currentView === "pipelineRun") {
|
|
196
208
|
const run = screens.pipelineRun;
|
|
197
209
|
const build = run.getBuild();
|
|
198
210
|
const id = run.getBuildId();
|
|
@@ -242,6 +254,12 @@ async function main() {
|
|
|
242
254
|
currentView = dest.view;
|
|
243
255
|
hideAll();
|
|
244
256
|
switch (dest.view) {
|
|
257
|
+
case "orgs":
|
|
258
|
+
screens.orgs.show();
|
|
259
|
+
break;
|
|
260
|
+
case "projects":
|
|
261
|
+
screens.projects.show(dest.org);
|
|
262
|
+
break;
|
|
245
263
|
case "pipelines":
|
|
246
264
|
screens.pipelines.show();
|
|
247
265
|
break;
|
|
@@ -269,14 +287,21 @@ async function main() {
|
|
|
269
287
|
}
|
|
270
288
|
// ── App context ────────────────────────────────────────────────────────────
|
|
271
289
|
const ctx = {
|
|
272
|
-
org
|
|
273
|
-
project
|
|
290
|
+
get org() { return ORG; },
|
|
291
|
+
get project() { return PROJECT; },
|
|
274
292
|
config,
|
|
275
293
|
state,
|
|
276
294
|
getToken: () => (0, api_js_1.getToken)(config.azConfigDir),
|
|
277
295
|
navigate,
|
|
278
296
|
goBack,
|
|
279
297
|
setStatus,
|
|
298
|
+
setOrgProject(org, project) {
|
|
299
|
+
ORG = org;
|
|
300
|
+
PROJECT = project;
|
|
301
|
+
config.org = org;
|
|
302
|
+
config.project = project;
|
|
303
|
+
state.pipelines = [];
|
|
304
|
+
},
|
|
280
305
|
loadPipelineDefinitions: async () => {
|
|
281
306
|
if (state.pipelines.length > 0)
|
|
282
307
|
return;
|
|
@@ -294,6 +319,8 @@ async function main() {
|
|
|
294
319
|
// ── Create screens ─────────────────────────────────────────────────────────
|
|
295
320
|
const envScreen = new EnvironmentsScreen_js_1.EnvironmentsScreen(screen, ctx);
|
|
296
321
|
const screens = {
|
|
322
|
+
orgs: new OrgsScreen_js_1.OrgsScreen(screen, ctx),
|
|
323
|
+
projects: new ProjectsScreen_js_1.ProjectsScreen(screen, ctx),
|
|
297
324
|
pipelines: new PipelinesScreen_js_1.PipelinesScreen(screen, ctx),
|
|
298
325
|
environments: envScreen,
|
|
299
326
|
stages: new StagesScreen_js_1.StagesScreen(screen, ctx),
|
|
@@ -314,7 +341,10 @@ async function main() {
|
|
|
314
341
|
}
|
|
315
342
|
});
|
|
316
343
|
// ── Initial navigation ────────────────────────────────────────────────────
|
|
317
|
-
if (INITIAL_VIEW === "
|
|
344
|
+
if (INITIAL_VIEW === "orgs") {
|
|
345
|
+
navigate({ view: "orgs" });
|
|
346
|
+
}
|
|
347
|
+
else if (INITIAL_VIEW === "pipelineRun" && INITIAL_BUILD_ID) {
|
|
318
348
|
navigate({ view: "pipelineRun", buildId: INITIAL_BUILD_ID });
|
|
319
349
|
}
|
|
320
350
|
else if (INITIAL_VIEW === "stages" && STAGES_ARG) {
|