azure-pipelines-tui 0.5.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/dist/tui.js ADDED
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // tui.ts — Unified Azure Pipelines TUI entry point
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ var __importDefault = (this && this.__importDefault) || function (mod) {
38
+ return (mod && mod.__esModule) ? mod : { "default": mod };
39
+ };
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ const os_1 = __importDefault(require("os"));
42
+ const url_1 = require("url");
43
+ const blessed = __importStar(require("blessed"));
44
+ const api_js_1 = require("./lib/api.js");
45
+ const PipelinesScreen_js_1 = require("./screens/PipelinesScreen.js");
46
+ const EnvironmentsScreen_js_1 = require("./screens/EnvironmentsScreen.js");
47
+ const StagesScreen_js_1 = require("./screens/StagesScreen.js");
48
+ const PipelineRunsScreen_js_1 = require("./screens/PipelineRunsScreen.js");
49
+ const PipelineRunScreen_js_1 = require("./screens/PipelineRunScreen.js");
50
+ const MappingScreen_js_1 = require("./screens/MappingScreen.js");
51
+ // ── CLI args ──────────────────────────────────────────────────────────────────
52
+ function showHelp() {
53
+ console.log(`
54
+ Azure Pipelines TUI
55
+
56
+ Usage:
57
+ npx tsx src/tui.ts ORG/PROJECT Pipelines Overview (default)
58
+ npx tsx src/tui.ts ORG/PROJECT --envs Environments Overview
59
+ npx tsx src/tui.ts ORG/PROJECT --stages <id> Stages Dashboard
60
+ npx tsx src/tui.ts ORG/PROJECT --runs <id> Pipeline Runs List
61
+ npx tsx src/tui.ts <build-url> Pipeline Run (single build)
62
+ npx tsx src/tui.ts ORG/PROJECT <buildId> Pipeline Run (single build)
63
+
64
+ Options:
65
+ --config <file> Config file (default: environments-config.json)
66
+ --stages <id> Pipeline ID or name to open stages view directly
67
+ --runs <id> Pipeline ID or name to open runs list directly
68
+ --envs Open environments overview
69
+ --keep-timestamps Keep timestamps in log output
70
+ --help Show this help
71
+
72
+ Cache location: ${os_1.default.homedir()}/.azure-pipelines-tui/cache/
73
+ `);
74
+ process.exit(0);
75
+ }
76
+ const rawArgs = process.argv.slice(2);
77
+ if (rawArgs.includes("--help"))
78
+ showHelp();
79
+ const flagsWithValues = new Set(["--config", "--stages", "--runs"]);
80
+ const positional = [];
81
+ for (let i = 0; i < rawArgs.length; i++) {
82
+ const a = rawArgs[i];
83
+ if (a.startsWith("--")) {
84
+ if (flagsWithValues.has(a))
85
+ i++;
86
+ continue;
87
+ }
88
+ positional.push(a);
89
+ }
90
+ const configIdx = rawArgs.indexOf("--config");
91
+ const CONFIG_FILE = configIdx >= 0 ? rawArgs[configIdx + 1] : "environments-config.json";
92
+ const stagesIdx = rawArgs.indexOf("--stages");
93
+ const STAGES_ARG = stagesIdx >= 0 ? rawArgs[stagesIdx + 1] : undefined;
94
+ const runsIdx = rawArgs.indexOf("--runs");
95
+ const RUNS_ARG = runsIdx >= 0 ? rawArgs[runsIdx + 1] : undefined;
96
+ const ENVS_FLAG = rawArgs.includes("--envs");
97
+ const KEEP_TIMESTAMPS = rawArgs.includes("--keep-timestamps");
98
+ function parseAdoUrl(raw) {
99
+ try {
100
+ const u = new url_1.URL(raw);
101
+ if (u.hostname !== "dev.azure.com")
102
+ return null;
103
+ const parts = u.pathname.split("/").filter(Boolean);
104
+ if (parts.length < 2)
105
+ return null;
106
+ return { org: parts[0], project: parts[1], buildId: u.searchParams.get("buildId") ?? undefined };
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ let ORG = "";
113
+ let PROJECT = "";
114
+ let INITIAL_BUILD_ID;
115
+ let INITIAL_VIEW = "pipelines";
116
+ const parsed = positional.length >= 1 ? parseAdoUrl(positional[0]) : null;
117
+ if (parsed) {
118
+ ORG = parsed.org;
119
+ PROJECT = parsed.project;
120
+ INITIAL_BUILD_ID = parsed.buildId ?? (positional.length >= 2 ? positional[1] : undefined);
121
+ }
122
+ else if (positional.length >= 3) {
123
+ [ORG, PROJECT, INITIAL_BUILD_ID] = positional;
124
+ }
125
+ else if (positional.length >= 1 && positional[0].includes("/")) {
126
+ const [org, ...rest] = positional[0].split("/");
127
+ ORG = org;
128
+ PROJECT = rest.join("/");
129
+ if (positional.length >= 2)
130
+ INITIAL_BUILD_ID = positional[1];
131
+ }
132
+ else if (positional.length >= 2) {
133
+ [ORG, PROJECT] = positional;
134
+ }
135
+ if (INITIAL_BUILD_ID)
136
+ INITIAL_VIEW = "pipelineRun";
137
+ else if (STAGES_ARG)
138
+ INITIAL_VIEW = "stages";
139
+ else if (RUNS_ARG)
140
+ INITIAL_VIEW = "runs";
141
+ else if (ENVS_FLAG)
142
+ INITIAL_VIEW = "environments";
143
+ // ── Main ──────────────────────────────────────────────────────────────────────
144
+ async function main() {
145
+ const config = (0, api_js_1.loadConfig)(CONFIG_FILE);
146
+ if (!ORG)
147
+ ORG = config.org ?? "";
148
+ if (!PROJECT)
149
+ PROJECT = config.project ?? "";
150
+ if (!ORG || !PROJECT) {
151
+ console.error("Error: org/project required. Pass as 'org/project' argument or set in environments-config.json.\n" +
152
+ "Run with --help for usage.");
153
+ process.exit(1);
154
+ }
155
+ config.org = ORG;
156
+ config.project = PROJECT;
157
+ // ── Shared state ──────────────────────────────────────────────────────────
158
+ const state = { pipelines: [] };
159
+ // ── Screen + widgets ──────────────────────────────────────────────────────
160
+ const screen = blessed.screen({
161
+ smartCSR: true, title: "Azure Pipelines TUI",
162
+ fullUnicode: true, forceUnicode: true,
163
+ });
164
+ process.stdout.write("\x1b[?25l");
165
+ const restoreCursor = () => process.stdout.write("\x1b[?25h");
166
+ screen.on("destroy", restoreCursor);
167
+ process.on("exit", restoreCursor);
168
+ process.on("SIGINT", () => { restoreCursor(); process.exit(0); });
169
+ const headerBox = blessed.box({
170
+ parent: screen, top: 0, left: 0, width: "100%", height: 1, tags: true,
171
+ style: { bg: "blue", fg: "white", bold: true },
172
+ content: ` {bold}Azure Pipelines TUI{/bold} ${ORG} / ${PROJECT}`,
173
+ });
174
+ const footerBox = blessed.box({
175
+ parent: screen, bottom: 0, left: 0, width: "100%", height: 1, tags: true,
176
+ style: { bg: "black", fg: "gray" },
177
+ });
178
+ // ── Status bar ─────────────────────────────────────────────────────────────
179
+ let statusMsg = "";
180
+ let statusTimer = null;
181
+ function setStatus(msg, ttlMs = 3000) {
182
+ statusMsg = msg;
183
+ if (statusTimer)
184
+ clearTimeout(statusTimer);
185
+ if (ttlMs > 0 && msg)
186
+ statusTimer = setTimeout(() => { statusMsg = ""; renderFooter(); }, ttlMs);
187
+ renderFooter();
188
+ }
189
+ // ── Navigation ─────────────────────────────────────────────────────────────
190
+ let currentView = INITIAL_VIEW;
191
+ let previousDest = { view: "pipelines" };
192
+ let currentDest = { view: "pipelines" };
193
+ function updateHeader() {
194
+ let content;
195
+ if (currentView === "pipelineRun") {
196
+ const run = screens.pipelineRun;
197
+ const build = run.getBuild();
198
+ const id = run.getBuildId();
199
+ const statusTag = !build
200
+ ? "{yellow-fg}loading…{/}"
201
+ : build.status === "completed"
202
+ ? build.result === "succeeded" ? "{green-fg}succeeded{/}"
203
+ : build.result === "failed" ? "{red-fg}failed{/}"
204
+ : build.result === "canceled" ? "{gray-fg}canceled{/}"
205
+ : (build.result ?? "completed")
206
+ : build.status === "inProgress" ? "{yellow-fg}▶ running{/}"
207
+ : build.status === "notStarted" ? "{gray-fg}waiting…{/}"
208
+ : build.status;
209
+ content = ` {bold}Azure Pipelines{/bold} ${ORG} / ${PROJECT} #{bold}${id}{/bold} ${statusTag}`;
210
+ }
211
+ else if (currentView === "stages") {
212
+ const pip = screens.stages.getPipeline();
213
+ content = ` {bold}Azure Pipelines{/bold} ${ORG} / ${PROJECT} Stages: ${pip?.name ?? ""}`;
214
+ }
215
+ else if (currentView === "runs") {
216
+ const pip = screens.runs.getPipeline();
217
+ content = ` {bold}Azure Pipelines{/bold} ${ORG} / ${PROJECT} Runs: ${pip?.name ?? ""}`;
218
+ }
219
+ else if (currentView === "environments") {
220
+ content = ` {bold}Azure Pipelines Environments{/bold} ${ORG} / ${PROJECT}`;
221
+ }
222
+ else {
223
+ content = ` {bold}Azure Pipelines TUI{/bold} ${ORG} / ${PROJECT}`;
224
+ }
225
+ headerBox.setContent(content);
226
+ screen.render();
227
+ }
228
+ function renderFooter() {
229
+ const s = statusMsg ? ` {yellow-fg}${statusMsg}{/}` : "";
230
+ const screenObj = screens[currentView];
231
+ const base = screenObj.footerText ?? "";
232
+ footerBox.setContent(base + s);
233
+ screen.render();
234
+ }
235
+ function hideAll() {
236
+ for (const s of Object.values(screens))
237
+ s.hide();
238
+ }
239
+ function navigate(dest) {
240
+ previousDest = currentDest;
241
+ currentDest = dest;
242
+ currentView = dest.view;
243
+ hideAll();
244
+ switch (dest.view) {
245
+ case "pipelines":
246
+ screens.pipelines.show();
247
+ break;
248
+ case "environments":
249
+ screens.environments.show();
250
+ break;
251
+ case "mapping":
252
+ screens.mapping.show();
253
+ break;
254
+ case "stages":
255
+ screens.stages.show(dest.pipeline);
256
+ break;
257
+ case "runs":
258
+ screens.runs.show(dest.pipeline);
259
+ break;
260
+ case "pipelineRun":
261
+ screens.pipelineRun.show(dest.buildId);
262
+ break;
263
+ }
264
+ updateHeader();
265
+ renderFooter();
266
+ }
267
+ function goBack() {
268
+ navigate(previousDest);
269
+ }
270
+ // ── App context ────────────────────────────────────────────────────────────
271
+ const ctx = {
272
+ org: ORG,
273
+ project: PROJECT,
274
+ config,
275
+ state,
276
+ getToken: () => (0, api_js_1.getToken)(config.azConfigDir),
277
+ navigate,
278
+ goBack,
279
+ setStatus,
280
+ loadPipelineDefinitions: async () => {
281
+ if (state.pipelines.length > 0)
282
+ return;
283
+ setStatus("Loading pipeline definitions…", 0);
284
+ try {
285
+ const token = await (0, api_js_1.getToken)(config.azConfigDir);
286
+ state.pipelines = await (0, api_js_1.fetchPipelineDefinitions)(ORG, PROJECT, token);
287
+ setStatus("", 0);
288
+ }
289
+ catch (e) {
290
+ setStatus(`Error: ${e.message.slice(0, 80)}`, 10_000);
291
+ }
292
+ },
293
+ };
294
+ // ── Create screens ─────────────────────────────────────────────────────────
295
+ const envScreen = new EnvironmentsScreen_js_1.EnvironmentsScreen(screen, ctx);
296
+ const screens = {
297
+ pipelines: new PipelinesScreen_js_1.PipelinesScreen(screen, ctx),
298
+ environments: envScreen,
299
+ stages: new StagesScreen_js_1.StagesScreen(screen, ctx),
300
+ runs: new PipelineRunsScreen_js_1.PipelineRunsScreen(screen, ctx),
301
+ pipelineRun: new PipelineRunScreen_js_1.PipelineRunScreen(screen, ctx, { keepTimestamps: KEEP_TIMESTAMPS }),
302
+ mapping: new MappingScreen_js_1.MappingScreen(screen, ctx, envScreen, CONFIG_FILE),
303
+ };
304
+ // ── Global keys ────────────────────────────────────────────────────────────
305
+ screen.key(["q", "C-c"], () => { screen.destroy(); process.exit(0); });
306
+ screen.key("tab", () => {
307
+ if (currentView === "pipelineRun") {
308
+ const { treeWidget, logWidget } = screens.pipelineRun;
309
+ if (screen.focused === treeWidget)
310
+ logWidget.focus();
311
+ else
312
+ treeWidget.focus();
313
+ screen.render();
314
+ }
315
+ });
316
+ // ── Initial navigation ────────────────────────────────────────────────────
317
+ if (INITIAL_VIEW === "pipelineRun" && INITIAL_BUILD_ID) {
318
+ navigate({ view: "pipelineRun", buildId: INITIAL_BUILD_ID });
319
+ }
320
+ else if (INITIAL_VIEW === "stages" && STAGES_ARG) {
321
+ navigate({ view: "pipelines" });
322
+ setStatus("Loading pipeline definitions…", 0);
323
+ ctx.loadPipelineDefinitions().then(() => {
324
+ const byId = Number(STAGES_ARG);
325
+ const pip = byId
326
+ ? state.pipelines.find(p => p.id === byId)
327
+ : state.pipelines.find(p => p.name.toLowerCase() === STAGES_ARG.toLowerCase());
328
+ if (!pip)
329
+ setStatus(`Pipeline "${STAGES_ARG}" not found`, 10_000);
330
+ else
331
+ navigate({ view: "stages", pipeline: pip });
332
+ });
333
+ }
334
+ else if (INITIAL_VIEW === "runs" && RUNS_ARG) {
335
+ navigate({ view: "pipelines" });
336
+ setStatus("Loading pipeline definitions…", 0);
337
+ ctx.loadPipelineDefinitions().then(() => {
338
+ const byId = Number(RUNS_ARG);
339
+ const pip = byId
340
+ ? state.pipelines.find(p => p.id === byId)
341
+ : state.pipelines.find(p => p.name.toLowerCase() === RUNS_ARG.toLowerCase());
342
+ if (!pip)
343
+ setStatus(`Pipeline "${RUNS_ARG}" not found`, 10_000);
344
+ else
345
+ navigate({ view: "runs", pipeline: pip });
346
+ });
347
+ }
348
+ else if (INITIAL_VIEW === "environments") {
349
+ navigate({ view: "environments" });
350
+ }
351
+ else {
352
+ navigate({ view: "pipelines" });
353
+ }
354
+ }
355
+ main().catch(err => {
356
+ console.error("Fatal:", err.message);
357
+ process.exit(1);
358
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "azure-pipelines-tui",
3
+ "version": "0.5.0",
4
+ "description": "Azure Pipelines TUI log viewer",
5
+ "bin": {
6
+ "azure-pipelines-tui": "./dist/tui.js"
7
+ },
8
+ "files": [
9
+ "dist/"
10
+ ],
11
+ "scripts": {
12
+ "start": "tsx src/tui.ts",
13
+ "build": "tsc",
14
+ "prepare": "npm run build",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "dependencies": {
21
+ "blessed": "^0.1.81",
22
+ "ws": "^8.20.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/blessed": "^0.1.25",
26
+ "@types/node": "^22.19.19",
27
+ "@types/ws": "^8.18.1",
28
+ "tsx": "^4.19.0",
29
+ "typescript": "^5.7.0"
30
+ }
31
+ }