@tmux-web/ext-git-workflow 0.1.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.
Files changed (30) hide show
  1. package/dist/backend/git.js +214 -0
  2. package/dist/backend/pane-ready.js +37 -0
  3. package/dist/backend/routes/commit-push.js +46 -0
  4. package/dist/backend/routes/handoff.js +85 -0
  5. package/dist/backend/routes/send-keys.js +31 -0
  6. package/dist/backend/routes/status.js +10 -0
  7. package/dist/backend/server.js +25 -0
  8. package/dist/backend/status-service.js +110 -0
  9. package/dist/backend/storage.js +32 -0
  10. package/dist/backend/tmux.js +58 -0
  11. package/dist/ui/app.js +425 -0
  12. package/dist/ui/index.html +334 -0
  13. package/node_modules/@tmux-web/ext-gh-workflow/dist/gh-client.d.ts +24 -0
  14. package/node_modules/@tmux-web/ext-gh-workflow/dist/gh-client.js +177 -0
  15. package/node_modules/@tmux-web/ext-gh-workflow/dist/gh-pr.d.ts +20 -0
  16. package/node_modules/@tmux-web/ext-gh-workflow/dist/gh-pr.js +43 -0
  17. package/node_modules/@tmux-web/ext-gh-workflow/dist/gh-repo.d.ts +14 -0
  18. package/node_modules/@tmux-web/ext-gh-workflow/dist/gh-repo.js +58 -0
  19. package/node_modules/@tmux-web/ext-gh-workflow/dist/index.d.ts +3 -0
  20. package/node_modules/@tmux-web/ext-gh-workflow/dist/index.js +3 -0
  21. package/node_modules/@tmux-web/ext-gh-workflow/package.json +38 -0
  22. package/node_modules/@tmux-web/ext-sdk/dist/bridge.d.ts +31 -0
  23. package/node_modules/@tmux-web/ext-sdk/dist/bridge.js +103 -0
  24. package/node_modules/@tmux-web/ext-sdk/dist/index.d.ts +7 -0
  25. package/node_modules/@tmux-web/ext-sdk/dist/index.js +12 -0
  26. package/node_modules/@tmux-web/ext-sdk/dist/types.d.ts +20 -0
  27. package/node_modules/@tmux-web/ext-sdk/dist/types.js +1 -0
  28. package/node_modules/@tmux-web/ext-sdk/package.json +32 -0
  29. package/package.json +43 -0
  30. package/tmux-extension.json +13 -0
package/dist/ui/app.js ADDED
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // ../../packages/ext-sdk/src/bridge.ts
8
+ var ExtBridge = class {
9
+ constructor() {
10
+ __publicField(this, "extId");
11
+ __publicField(this, "contextCb", null);
12
+ __publicField(this, "configCb", null);
13
+ __publicField(this, "openCb", null);
14
+ __publicField(this, "closeCb", null);
15
+ __publicField(this, "_config", null);
16
+ __publicField(this, "pendingContext", null);
17
+ __publicField(this, "pendingConfig", null);
18
+ __publicField(this, "pendingOpen", false);
19
+ __publicField(this, "pendingClose", false);
20
+ const m = window.location.pathname.match(/^\/ext\/([^/]+)\//);
21
+ if (!m) throw new Error("[ext-sdk] cannot detect extension id from iframe URL");
22
+ this.extId = m[1];
23
+ window.addEventListener("message", this._onMessage.bind(this));
24
+ }
25
+ /** Call after all `on*` handlers are registered so early host messages are not lost. */
26
+ ready() {
27
+ window.parent.postMessage({ type: "ext:ready" }, "*");
28
+ }
29
+ _onMessage(event) {
30
+ const msg = event.data;
31
+ if (!msg?.type) return;
32
+ if (msg.type === "ext:context") {
33
+ if (this.contextCb) this.contextCb(msg.context);
34
+ else this.pendingContext = msg.context;
35
+ } else if (msg.type === "ext:config") {
36
+ this._config = msg.config;
37
+ if (this.configCb) this.configCb(msg.config);
38
+ else this.pendingConfig = msg.config;
39
+ } else if (msg.type === "ext:open") {
40
+ if (this.openCb) void this.openCb();
41
+ else this.pendingOpen = true;
42
+ } else if (msg.type === "ext:close") {
43
+ if (this.closeCb) void this.closeCb();
44
+ else this.pendingClose = true;
45
+ }
46
+ }
47
+ onContext(cb) {
48
+ this.contextCb = cb;
49
+ if (this.pendingContext) {
50
+ cb(this.pendingContext);
51
+ this.pendingContext = null;
52
+ }
53
+ }
54
+ onConfig(cb) {
55
+ this.configCb = cb;
56
+ if (this.pendingConfig !== null) {
57
+ this._config = this.pendingConfig;
58
+ void cb(this.pendingConfig);
59
+ this.pendingConfig = null;
60
+ }
61
+ }
62
+ onOpen(cb) {
63
+ this.openCb = cb;
64
+ if (this.pendingOpen) {
65
+ this.pendingOpen = false;
66
+ void cb();
67
+ }
68
+ }
69
+ onClose(cb) {
70
+ this.closeCb = cb;
71
+ if (this.pendingClose) {
72
+ this.pendingClose = false;
73
+ void cb();
74
+ }
75
+ }
76
+ getConfig() {
77
+ return this._config;
78
+ }
79
+ async request(path, options) {
80
+ const url = `/ext/${this.extId}/api${path}`;
81
+ const init = { method: options?.method ?? "GET" };
82
+ if (options?.body !== void 0) {
83
+ init.body = JSON.stringify(options.body);
84
+ init.headers = { "Content-Type": "application/json" };
85
+ }
86
+ const res = await fetch(url, init);
87
+ if (!res.ok) {
88
+ const text = await res.text().catch(() => res.statusText);
89
+ throw new Error(`[ext-sdk] ${url} \u2192 ${res.status}: ${text}`);
90
+ }
91
+ return res.json();
92
+ }
93
+ resize(height) {
94
+ const msg = { type: "ext:resize", height };
95
+ window.parent.postMessage(msg, "*");
96
+ }
97
+ };
98
+
99
+ // ../../packages/ext-sdk/src/index.ts
100
+ var _bridge = null;
101
+ function createExtension() {
102
+ if (_bridge) throw new Error("[ext-sdk] createExtension() called more than once");
103
+ _bridge = new ExtBridge();
104
+ return _bridge;
105
+ }
106
+
107
+ // ui/app.ts
108
+ var ext = createExtension();
109
+ var _session = "";
110
+ var _pollTimer = 0;
111
+ var _drawerOpen = false;
112
+ var _displayedPaneId = "";
113
+ var _currentData = null;
114
+ var _pendingHandoffBranch = "";
115
+ function esc(s) {
116
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
117
+ }
118
+ function setVisible(id, show) {
119
+ const el = document.getElementById(id);
120
+ if (!el) return;
121
+ if (show) el.removeAttribute("hidden");
122
+ else el.setAttribute("hidden", "");
123
+ }
124
+ function updateTimestamp() {
125
+ const el = document.getElementById("last-refresh");
126
+ if (el) el.textContent = "updated " + (/* @__PURE__ */ new Date()).toLocaleTimeString();
127
+ }
128
+ function populateBranchSelect(branches, current) {
129
+ const sel = document.getElementById("branch-select");
130
+ if (!sel) return;
131
+ sel.innerHTML = branches.map(
132
+ (b) => `<option value="${esc(b)}"${b === current ? " selected" : ""}>${esc(b)}</option>`
133
+ ).join("");
134
+ if (!branches.length) {
135
+ sel.innerHTML = `<option value="${esc(current)}">${esc(current)}</option>`;
136
+ }
137
+ }
138
+ var FAILED_CONCLUSIONS = /* @__PURE__ */ new Set(["failure", "timed_out", "action_required", "cancelled"]);
139
+ function checkStatusClass(check) {
140
+ if (check.status !== "completed") return "pending";
141
+ if (check.conclusion === "success" || check.conclusion === "neutral" || check.conclusion === "skipped") {
142
+ return check.conclusion === "skipped" ? "skipped" : "passing";
143
+ }
144
+ if (check.conclusion && FAILED_CONCLUSIONS.has(check.conclusion)) return "failing";
145
+ return "pending";
146
+ }
147
+ function checkStatusLabel(check) {
148
+ if (check.status === "queued") return "queued";
149
+ if (check.status === "in_progress") return "running";
150
+ return check.conclusion ?? "unknown";
151
+ }
152
+ async function sendCheckError(checkUrl) {
153
+ try {
154
+ await ext.request("/send-keys", {
155
+ method: "POST",
156
+ body: {
157
+ session: _session,
158
+ text: `The check failed with following error here: ${checkUrl}, do check the root cause for this issue`
159
+ }
160
+ });
161
+ } catch (e) {
162
+ console.error("[git-workflow] send-keys failed:", e);
163
+ }
164
+ }
165
+ function renderPrSection(data) {
166
+ const section = document.getElementById("pr-section");
167
+ const checksList = document.getElementById("checks-list");
168
+ checksList.innerHTML = "";
169
+ const src = data.pr ? { labelText: "PR", linkText: `#${data.pr.number}: ${data.pr.title}`, href: data.pr.url, checks: data.pr.checks } : data.branchChecks ? { labelText: "Branch", linkText: data.branchChecks.branch, href: data.branchChecks.url, checks: data.branchChecks.checks } : null;
170
+ if (!src) {
171
+ section.setAttribute("hidden", "");
172
+ return;
173
+ }
174
+ section.removeAttribute("hidden");
175
+ const label = document.getElementById("checks-label");
176
+ if (label) label.textContent = src.labelText;
177
+ const link = document.getElementById("pr-link");
178
+ link.textContent = src.linkText;
179
+ link.href = src.href;
180
+ for (const check of src.checks) {
181
+ const cls = checkStatusClass(check);
182
+ const row = document.createElement("div");
183
+ row.className = "check-row";
184
+ const name = document.createElement("span");
185
+ name.className = "check-name";
186
+ name.textContent = check.name;
187
+ name.title = check.name;
188
+ const badge = document.createElement("span");
189
+ badge.className = `check-status ${cls}`;
190
+ badge.textContent = checkStatusLabel(check);
191
+ row.appendChild(name);
192
+ row.appendChild(badge);
193
+ if (cls === "failing") {
194
+ const btn = document.createElement("button");
195
+ btn.className = "btn danger";
196
+ btn.style.padding = "2px 6px";
197
+ btn.style.fontSize = "10px";
198
+ btn.textContent = "\u21B3 Send";
199
+ btn.title = "Send failure prompt to active tmux window";
200
+ const url = check.url;
201
+ btn.addEventListener("click", () => {
202
+ void sendCheckError(url);
203
+ });
204
+ row.appendChild(btn);
205
+ }
206
+ checksList.appendChild(row);
207
+ }
208
+ }
209
+ function renderPanel(data) {
210
+ _currentData = data;
211
+ _displayedPaneId = data.paneId;
212
+ setVisible("loading", false);
213
+ setVisible("empty-state", false);
214
+ setVisible("panel", true);
215
+ const localBadge = document.getElementById("kind-local");
216
+ const worktreeBadge = document.getElementById("kind-worktree");
217
+ localBadge.classList.toggle("active", data.kind === "local");
218
+ worktreeBadge.classList.toggle("active", data.kind === "worktree");
219
+ const changesEl = document.getElementById("changes");
220
+ changesEl.innerHTML = `<span class="add">+${data.changes.added}</span> <span class="del">-${data.changes.removed}</span>`;
221
+ const branchEl = document.getElementById("branch-display");
222
+ branchEl.textContent = data.branch;
223
+ const sessionEl = document.getElementById("session-label");
224
+ if (sessionEl) {
225
+ sessionEl.textContent = `${data.session} \xB7 win ${data.windowIndex} \xB7 ${data.branch}`;
226
+ }
227
+ populateBranchSelect(data.branches, data.branch);
228
+ const pathEl = document.getElementById("path-display");
229
+ pathEl.textContent = data.panePath;
230
+ const mainRow = document.getElementById("main-repo-row");
231
+ const mainPath = document.getElementById("main-repo-path");
232
+ if (data.kind === "worktree" && data.mainRepoPath) {
233
+ mainRow.style.display = "";
234
+ mainPath.textContent = data.mainRepoPath;
235
+ } else {
236
+ mainRow.style.display = "none";
237
+ mainPath.textContent = "";
238
+ }
239
+ setVisible("branch-select-row", data.kind === "local");
240
+ setVisible("local-actions", data.kind === "local");
241
+ const btn = document.getElementById("commit-push-btn");
242
+ btn.disabled = !data.dirty && data.ahead === 0;
243
+ renderPrSection(data);
244
+ ext.resize(document.body.scrollHeight + 16);
245
+ }
246
+ function renderEmpty(message) {
247
+ _currentData = null;
248
+ setVisible("loading", false);
249
+ setVisible("panel", false);
250
+ setVisible("empty-state", true);
251
+ const empty = document.getElementById("empty-state");
252
+ empty.innerHTML = esc(message).replace(/\n/g, "<br>");
253
+ ext.resize(document.body.scrollHeight + 16);
254
+ }
255
+ async function fetchStatus() {
256
+ if (!_session) {
257
+ renderEmpty("Waiting for session context\u2026");
258
+ return;
259
+ }
260
+ try {
261
+ const res = await ext.request(
262
+ `/status?session=${encodeURIComponent(_session)}`
263
+ );
264
+ if (!res.isRepo || !res.isGithub || !res.data) {
265
+ renderEmpty(res.message ?? "Works with GitHub repositories only");
266
+ return;
267
+ }
268
+ if (_displayedPaneId && res.paneId !== _displayedPaneId) {
269
+ _pendingHandoffBranch = "";
270
+ }
271
+ renderPanel(res.data);
272
+ updateTimestamp();
273
+ } catch (e) {
274
+ console.error("[git-workflow] status failed:", e);
275
+ renderEmpty("Failed to load git status");
276
+ }
277
+ }
278
+ function stopPoll() {
279
+ if (_pollTimer) {
280
+ clearInterval(_pollTimer);
281
+ _pollTimer = 0;
282
+ }
283
+ }
284
+ function startPoll(intervalMs) {
285
+ stopPoll();
286
+ _pollTimer = setInterval(() => {
287
+ void fetchStatus();
288
+ }, intervalMs);
289
+ }
290
+ function openModal(id) {
291
+ document.getElementById(id)?.classList.add("open");
292
+ }
293
+ function closeModal(id) {
294
+ document.getElementById(id)?.classList.remove("open");
295
+ }
296
+ async function submitHandoff(confirmCreate = false) {
297
+ const errEl = document.getElementById("handoff-err");
298
+ errEl.textContent = "";
299
+ const branch = _pendingHandoffBranch || document.getElementById("handoff-branch").value.trim();
300
+ if (!branch) {
301
+ errEl.textContent = "Branch name is required";
302
+ return;
303
+ }
304
+ try {
305
+ await ext.request("/handoff", {
306
+ method: "POST",
307
+ body: { session: _session, branch, confirmCreate }
308
+ });
309
+ closeModal("handoff-modal");
310
+ closeModal("confirm-modal");
311
+ _pendingHandoffBranch = "";
312
+ await fetchStatus();
313
+ } catch (e) {
314
+ const msg = String(e?.message ?? e);
315
+ const jsonStart = msg.indexOf("{");
316
+ if (msg.includes("409") && jsonStart >= 0) {
317
+ try {
318
+ const body = JSON.parse(msg.slice(jsonStart));
319
+ if (body.needsConfirmation) {
320
+ closeModal("handoff-modal");
321
+ _pendingHandoffBranch = branch;
322
+ const confirmText = document.getElementById("confirm-text");
323
+ confirmText.textContent = `Branch "${body.branch ?? branch}" does not exist locally or on origin. Create it and open a worktree?`;
324
+ openModal("confirm-modal");
325
+ return;
326
+ }
327
+ } catch {
328
+ }
329
+ }
330
+ errEl.textContent = msg.replace(/^\[ext-sdk\][^:]+:\s*\d+:\s*/, "");
331
+ }
332
+ }
333
+ async function submitCommitPush() {
334
+ const errEl = document.getElementById("commit-err");
335
+ errEl.textContent = "";
336
+ const message = document.getElementById("commit-message").value.trim();
337
+ const dirty = _currentData?.dirty ?? false;
338
+ if (dirty && !message) {
339
+ errEl.textContent = "Commit message is required";
340
+ return;
341
+ }
342
+ try {
343
+ await ext.request("/commit-push", {
344
+ method: "POST",
345
+ body: { session: _session, message: message || void 0 }
346
+ });
347
+ closeModal("commit-modal");
348
+ document.getElementById("commit-message").value = "";
349
+ await fetchStatus();
350
+ } catch (e) {
351
+ errEl.textContent = String(e?.message ?? e).replace(/^\[ext-sdk\][^:]+:\s*/, "");
352
+ }
353
+ }
354
+ function wireUi() {
355
+ document.getElementById("handoff-btn")?.addEventListener("click", () => {
356
+ const sel = document.getElementById("branch-select");
357
+ const input = document.getElementById("handoff-branch");
358
+ input.value = sel?.value ?? _currentData?.branch ?? "";
359
+ document.getElementById("handoff-err").textContent = "";
360
+ openModal("handoff-modal");
361
+ });
362
+ document.getElementById("handoff-cancel")?.addEventListener("click", () => closeModal("handoff-modal"));
363
+ document.getElementById("handoff-submit")?.addEventListener("click", () => {
364
+ void submitHandoff(false);
365
+ });
366
+ document.getElementById("confirm-cancel")?.addEventListener("click", () => {
367
+ closeModal("confirm-modal");
368
+ _pendingHandoffBranch = "";
369
+ });
370
+ document.getElementById("confirm-submit")?.addEventListener("click", () => {
371
+ void submitHandoff(true);
372
+ });
373
+ document.getElementById("commit-push-btn")?.addEventListener("click", () => {
374
+ const dirty = _currentData?.dirty ?? false;
375
+ const ahead = _currentData?.ahead ?? 0;
376
+ const title = document.getElementById("commit-title");
377
+ const field = document.getElementById("commit-message-field");
378
+ const msg = document.getElementById("commit-message");
379
+ document.getElementById("commit-err").textContent = "";
380
+ if (dirty) {
381
+ title.textContent = "Commit and push";
382
+ field.style.display = "";
383
+ msg.required = true;
384
+ } else if (ahead > 0) {
385
+ title.textContent = "Push commits";
386
+ field.style.display = "none";
387
+ msg.required = false;
388
+ }
389
+ openModal("commit-modal");
390
+ });
391
+ document.getElementById("commit-cancel")?.addEventListener("click", () => closeModal("commit-modal"));
392
+ document.getElementById("commit-submit")?.addEventListener("click", () => {
393
+ void submitCommitPush();
394
+ });
395
+ document.getElementById("copy-main-btn")?.addEventListener("click", async () => {
396
+ const path = _currentData?.mainRepoPath;
397
+ if (path) await navigator.clipboard.writeText(path);
398
+ });
399
+ }
400
+ window.__refresh = () => {
401
+ void fetchStatus();
402
+ };
403
+ ext.onContext((ctx) => {
404
+ _session = ctx.session;
405
+ const el = document.getElementById("session-label");
406
+ if (el) el.textContent = `session: ${ctx.session}`;
407
+ if (_drawerOpen) void fetchStatus();
408
+ });
409
+ ext.onOpen(() => {
410
+ _drawerOpen = true;
411
+ void fetchStatus();
412
+ const cfg = ext.getConfig();
413
+ startPoll(cfg?.pollIntervalMs ?? 1e4);
414
+ });
415
+ ext.onClose(() => {
416
+ _drawerOpen = false;
417
+ stopPoll();
418
+ });
419
+ ext.onConfig((rawCfg) => {
420
+ const cfg = rawCfg;
421
+ if (_pollTimer) startPoll(cfg.pollIntervalMs ?? 1e4);
422
+ });
423
+ document.addEventListener("DOMContentLoaded", wireUi);
424
+ ext.ready();
425
+ })();