@triflux/remote 10.0.0 → 10.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.
package/hub/team/tui.mjs CHANGED
@@ -5,7 +5,6 @@
5
5
  import {
6
6
  RESET,
7
7
  FG,
8
- BG,
9
8
  MOCHA,
10
9
  color,
11
10
  dim,
@@ -18,7 +17,6 @@ import {
18
17
  wcswidth,
19
18
  progressBar,
20
19
  statusBadge,
21
- STATUS_ICON,
22
20
  altScreenOn,
23
21
  altScreenOff,
24
22
  clearScreen,
@@ -27,7 +25,6 @@ import {
27
25
  cursorShow,
28
26
  moveTo,
29
27
  clearLine,
30
- clearToEnd,
31
28
  } from "./ansi.mjs";
32
29
 
33
30
  import { execFile as _execFile } from "node:child_process";
@@ -52,7 +49,7 @@ const SPINNER_FRAMES = [...SPINNER_FRAMES_RAW, ...[...SPINNER_FRAMES_RAW].revers
52
49
  const SPINNER_CYCLE_MS = 2000;
53
50
  const SPINNER_BASE_COLOR = { r: 203, g: 166, b: 247 }; // Catppuccin Mocha mauve
54
51
  const SPINNER_SHIMMER = { r: 171, g: 43, b: 63 }; // Claude shimmer #ab2b3f
55
- let spinnerStart = Date.now();
52
+ const spinnerStart = Date.now();
56
53
  let spinnerTick = 0;
57
54
 
58
55
  function lerpRgb(a, b, t) {
@@ -119,7 +116,7 @@ function activityWave(tick, count = 4) {
119
116
  const GRID_GAP = 2;
120
117
  const DEFAULT_DETAIL_LINES = 10;
121
118
  // Tier1 상단 고정 행 수
122
- const TIER1_ROWS = 2;
119
+ const _TIER1_ROWS = 2;
123
120
 
124
121
  const SUMMARY_KEYS = [
125
122
  "status", "lead_action", "verdict", "files_changed",
@@ -128,14 +125,14 @@ const SUMMARY_KEYS = [
128
125
 
129
126
  // ── 레이아웃 브레이크포인트 ──────────────────────────────────────────────
130
127
  // 80-119: 28col rail, 120-159: 36col rail, 160+: 균등
131
- function resolveRailWidth(totalCols, columnCount) {
128
+ function _resolveRailWidth(totalCols, columnCount) {
132
129
  if (columnCount <= 1) return totalCols;
133
130
  if (totalCols >= 160) return Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount);
134
131
  if (totalCols >= 120) return Math.min(36, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
135
132
  return Math.min(28, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
136
133
  }
137
134
 
138
- function autoColumnCount(totalCols, workerCount) {
135
+ function _autoColumnCount(totalCols, workerCount) {
139
136
  if (workerCount <= 1) return 1;
140
137
  if (totalCols >= 160) return Math.min(workerCount, 3);
141
138
  if (totalCols >= 120) return Math.min(workerCount, 2);
@@ -154,7 +151,7 @@ function stripCodeBlocks(text) {
154
151
  .replace(/```[\s\S]*?(?:```|$)/g, "\n")
155
152
  .replace(/^\s*```.*$/gm, "")
156
153
  // indented code blocks (4+ spaces or tab at line start)
157
- .replace(/^(?: |\t).+$/gm, "")
154
+ .replace(/^(?: {4}|\t).+$/gm, "")
158
155
  // shell prompts: PS C:\...>, >, $
159
156
  .replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "")
160
157
  .trim();
@@ -275,7 +272,7 @@ function gradientBorderFn(topRgb, bottomRgb) {
275
272
  }
276
273
 
277
274
  // Effect 3: Flash-fade border — 상태 변경 시 백색 플래시 → 페이드아웃
278
- function flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
275
+ function _flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
279
276
  const elapsed = Date.now() - (changedAt || 0);
280
277
  if (elapsed >= FADE_DURATION_MS || !prevStatus) return null;
281
278
  const statusRgb = statusToRgb(currentStatus);
@@ -319,11 +316,11 @@ function dedupeRole(role, name, cli) {
319
316
  r = r.replace(new RegExp(esc(cli), "gi"), "").trim();
320
317
  r = r.replace(new RegExp(esc(name), "gi"), "").trim();
321
318
  // CLI indicator emojis 제거
322
- r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/g, "").trim();
319
+ r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/gu, "").trim();
323
320
  // 빈 괄호 제거 + 중첩 괄호 정리
324
321
  r = r.replace(/\(\s*\)/g, "").trim();
325
322
  r = r.replace(/^\(([^()]+)\)$/, "$1").trim();
326
- r = r.replace(/^\s*[•·\-]\s*/, "").trim();
323
+ r = r.replace(/^\s*[•·-]\s*/, "").trim();
327
324
  return r;
328
325
  }
329
326
 
@@ -350,7 +347,7 @@ function wrapLine(text, width) {
350
347
  return lines.length > 0 ? lines : [source.slice(0, limit)];
351
348
  }
352
349
 
353
- function wrapText(text, width, maxLines = DEFAULT_DETAIL_LINES, rawMode = false) {
350
+ function _wrapText(text, width, maxLines = DEFAULT_DETAIL_LINES, rawMode = false) {
354
351
  if (maxLines <= 0) return [];
355
352
  const input = sanitizeTextBlock(text, rawMode);
356
353
  if (!input) return [];
@@ -465,7 +462,7 @@ function buildWorkerRail(name, st, opts = {}) {
465
462
  selected = false,
466
463
  focused = false, // rail 포커스 여부
467
464
  previousSelected = false,
468
- rawMode = false,
465
+ _rawMode = false,
469
466
  compact = false,
470
467
  time = Date.now(),
471
468
  } = opts;
@@ -713,7 +710,7 @@ function buildHelpOverlay(width, height) {
713
710
  }
714
711
 
715
712
  // ── joinColumns ───────────────────────────────────────────────────────────
716
- function joinColumns(blocks, gap = GRID_GAP) {
713
+ function _joinColumns(blocks, gap = GRID_GAP) {
717
714
  const maxHeight = Math.max(...blocks.map((b) => b.length));
718
715
  return Array.from({ length: maxHeight }, (_, rowIdx) =>
719
716
  blocks
@@ -1418,7 +1415,7 @@ export function renderConductorTier(snapshot, cols = 100) {
1418
1415
  const dataLines = [];
1419
1416
  if (!snapshot || snapshot.length === 0) {
1420
1417
  const emptyMsg = color('(no sessions)', FG.muted);
1421
- const emptyPad = clip(stripAnsi(emptyMsg) === '(no sessions)' ? emptyMsg : emptyMsg, inner);
1418
+ const _emptyPad = clip(stripAnsi(emptyMsg) === '(no sessions)' ? emptyMsg : emptyMsg, inner);
1422
1419
  dataLines.push(`${borderSeq}│${RESET} ${padRight(emptyMsg, inner - 2)} ${borderSeq}│${RESET}`);
1423
1420
  } else {
1424
1421
  for (const s of snapshot) {
@@ -0,0 +1,229 @@
1
+ // hub/team/worktree-lifecycle.mjs — Git worktree lifecycle management
2
+ // Replaces shell commands in tfx-codex-swarm Step 5 + merge-worktree Phase 6.
3
+ // Convention: .codex-swarm/wt-{slug} paths, swarm/{runId}/{slug} branches.
4
+ // Remote support: host option → SSH-based git operations via remote-session.mjs.
5
+
6
+ import { execFile } from 'node:child_process';
7
+ import { resolve, normalize } from 'node:path';
8
+ import { mkdir, rm, access } from 'node:fs/promises';
9
+ import { remoteGit, validateHost } from './remote-session.mjs';
10
+
11
+ const SWARM_ROOT = '.codex-swarm';
12
+ const SLEEP_MS = 2000; // WT race-guard (MEMORY.md: wt-attach-spacing)
13
+
14
+ function git(args, cwd) {
15
+ return new Promise((res, rej) => {
16
+ execFile('git', args, { cwd, windowsHide: true, timeout: 30_000 }, (err, stdout, stderr) => {
17
+ if (err) {
18
+ const msg = `git ${args[0]} failed: ${stderr?.trim() || err.message}`;
19
+ rej(new Error(msg));
20
+ } else {
21
+ res(stdout.trim());
22
+ }
23
+ });
24
+ });
25
+ }
26
+
27
+ function sleep(ms) {
28
+ return new Promise((r) => setTimeout(r, ms));
29
+ }
30
+
31
+ /** Normalize path for Windows compatibility. */
32
+ function normPath(p) {
33
+ return normalize(p).replace(/\\/g, '/');
34
+ }
35
+
36
+ /**
37
+ * Create a worktree for a shard.
38
+ *
39
+ * @param {object} opts
40
+ * @param {string} opts.slug — shard identifier (e.g. "issue-42" or "auth-refactor")
41
+ * @param {string} opts.runId — swarm run ID
42
+ * @param {string} [opts.rootDir=process.cwd()] — repo root
43
+ * @param {string} [opts.baseBranch='main'] — base branch to branch from
44
+ * @param {string} [opts.host] — SSH host for remote worktree creation
45
+ * @param {object} [opts.remoteEnv] — remote environment from probeRemoteEnv()
46
+ * @returns {Promise<{ worktreePath: string, branchName: string, remote: boolean }>}
47
+ */
48
+ export async function ensureWorktree({ slug, runId, rootDir = process.cwd(), baseBranch = 'main', host, remoteEnv }) {
49
+ const branchName = `swarm/${runId}/${slug}`;
50
+
51
+ // ── Remote path: SSH-based worktree creation ──
52
+ if (host && remoteEnv) {
53
+ const remoteRoot = rootDir.replace(/\\/g, '/');
54
+ const remoteWtDir = `${remoteRoot}/${SWARM_ROOT}/wt-${slug}`;
55
+
56
+ try { remoteGit(host, remoteEnv, ['worktree', 'prune'], remoteRoot); } catch { /* best-effort */ }
57
+
58
+ try {
59
+ remoteGit(host, remoteEnv, ['worktree', 'add', remoteWtDir, '-b', branchName, baseBranch], remoteRoot);
60
+ } catch {
61
+ try {
62
+ remoteGit(host, remoteEnv, ['worktree', 'add', remoteWtDir, branchName], remoteRoot);
63
+ } catch { /* already exists — acceptable */ }
64
+ }
65
+
66
+ return { worktreePath: remoteWtDir, branchName, remote: true };
67
+ }
68
+
69
+ // ── Local path (existing logic) ──
70
+ const wtDir = resolve(rootDir, SWARM_ROOT, `wt-${slug}`);
71
+
72
+ await mkdir(resolve(rootDir, SWARM_ROOT), { recursive: true });
73
+
74
+ // Check if worktree already exists
75
+ try {
76
+ await access(wtDir);
77
+ await git(['rev-parse', '--is-inside-work-tree'], wtDir);
78
+ return { worktreePath: normPath(wtDir), branchName, remote: false };
79
+ } catch {
80
+ // Doesn't exist or invalid — create fresh
81
+ }
82
+
83
+ try {
84
+ await git(['worktree', 'prune'], rootDir);
85
+ } catch { /* best-effort */ }
86
+
87
+ await sleep(SLEEP_MS);
88
+
89
+ try {
90
+ await git(['worktree', 'add', wtDir, '-b', branchName, baseBranch], rootDir);
91
+ } catch {
92
+ await git(['worktree', 'add', wtDir, branchName], rootDir);
93
+ }
94
+
95
+ return { worktreePath: normPath(wtDir), branchName, remote: false };
96
+ }
97
+
98
+ /**
99
+ * Create a temporary integration branch for merge operations.
100
+ *
101
+ * @param {object} opts
102
+ * @param {string} opts.runId
103
+ * @param {string} opts.baseBranch
104
+ * @param {string} [opts.rootDir=process.cwd()]
105
+ * @returns {Promise<{ integrationBranch: string, baseCommit: string }>}
106
+ */
107
+ export async function prepareIntegrationBranch({ runId, baseBranch, rootDir = process.cwd() }) {
108
+ const integrationBranch = `swarm/${runId}/merge`;
109
+
110
+ // Record base commit for rollback
111
+ const baseCommit = await git(['rev-parse', baseBranch], rootDir);
112
+
113
+ // Create integration branch from base
114
+ try {
115
+ await git(['branch', integrationBranch, baseBranch], rootDir);
116
+ } catch {
117
+ // Branch may already exist — reset to base
118
+ await git(['branch', '-f', integrationBranch, baseBranch], rootDir);
119
+ }
120
+
121
+ return { integrationBranch, baseCommit };
122
+ }
123
+
124
+ /**
125
+ * Rebase a shard branch onto the integration branch.
126
+ * Uses rebase + fast-forward only (no merge commits).
127
+ *
128
+ * @param {object} opts
129
+ * @param {string} opts.shardBranch — the shard's branch name
130
+ * @param {string} opts.integrationBranch — target integration branch
131
+ * @param {string} [opts.rootDir=process.cwd()]
132
+ * @returns {Promise<{ ok: boolean, headCommit?: string, error?: string }>}
133
+ */
134
+ export async function rebaseShardOntoIntegration({ shardBranch, integrationBranch, rootDir = process.cwd() }) {
135
+ // Backup integration HEAD for rollback
136
+ const backupCommit = await git(['rev-parse', integrationBranch], rootDir);
137
+
138
+ try {
139
+ // Rebase shard onto integration
140
+ await git(['rebase', integrationBranch, shardBranch], rootDir);
141
+
142
+ // Fast-forward integration to include shard changes
143
+ await git(['checkout', integrationBranch], rootDir);
144
+ await git(['merge', '--ff-only', shardBranch], rootDir);
145
+
146
+ const headCommit = await git(['rev-parse', 'HEAD'], rootDir);
147
+ return { ok: true, headCommit };
148
+ } catch (err) {
149
+ // Abort rebase and restore integration branch
150
+ try { await git(['rebase', '--abort'], rootDir); } catch { /* already clean */ }
151
+ try { await git(['checkout', integrationBranch], rootDir); } catch { /* best-effort */ }
152
+ try { await git(['reset', '--hard', backupCommit], rootDir); } catch { /* best-effort */ }
153
+
154
+ return { ok: false, error: err.message };
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Remove a worktree and its branch.
160
+ * Follows WT race-guard: sleep between operations.
161
+ *
162
+ * @param {object} opts
163
+ * @param {string} opts.worktreePath
164
+ * @param {string} [opts.branchName] — optional branch to delete
165
+ * @param {string} [opts.rootDir=process.cwd()]
166
+ * @param {boolean} [opts.force=false]
167
+ */
168
+ export async function pruneWorktree({ worktreePath, branchName, rootDir = process.cwd(), force = false }) {
169
+ const forceFlag = force ? '--force' : '';
170
+
171
+ // Remove worktree (with retry for Windows file handle issues — E5)
172
+ for (let attempt = 0; attempt < 3; attempt++) {
173
+ try {
174
+ await git(['worktree', 'remove', worktreePath, ...(forceFlag ? [forceFlag] : [])], rootDir);
175
+ break;
176
+ } catch (_err) {
177
+ if (attempt === 2) {
178
+ // Last resort: rm the directory and prune
179
+ try { await rm(worktreePath, { recursive: true, force: true }); } catch { /* ignore */ }
180
+ }
181
+ await sleep(SLEEP_MS);
182
+ }
183
+ }
184
+
185
+ // Prune stale worktree references
186
+ await sleep(SLEEP_MS);
187
+ try { await git(['worktree', 'prune'], rootDir); } catch { /* best-effort */ }
188
+
189
+ // Delete branch if specified
190
+ if (branchName) {
191
+ try { await git(['branch', '-D', branchName], rootDir); } catch { /* may not exist */ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Fetch a remote shard's branch to the local repo via SSH.
197
+ * Workaround for hosts that cannot push to GitHub (e.g. Ultra4).
198
+ *
199
+ * Flow: add temp remote → fetch branch → remove remote.
200
+ *
201
+ * @param {object} opts
202
+ * @param {string} opts.host — SSH host (e.g. "ultra4")
203
+ * @param {string} opts.sshUser — SSH user (e.g. "SSAFY")
204
+ * @param {string} opts.remoteRepoPath — absolute path on remote (e.g. "/c/Users/SSAFY/Desktop/Projects/cli/triflux")
205
+ * @param {string} opts.branchName — branch to fetch (e.g. "swarm/run123/auth")
206
+ * @param {string} [opts.rootDir=process.cwd()] — local repo root
207
+ * @returns {Promise<{ ok: boolean, localRef?: string, error?: string }>}
208
+ */
209
+ export async function fetchRemoteShard({ host, sshUser, remoteRepoPath, branchName, rootDir = process.cwd() }) {
210
+ validateHost(host);
211
+
212
+ const remoteName = `_swarm-${host}-${Date.now()}`;
213
+ const sshUrl = `ssh://${sshUser}@${host}${remoteRepoPath}`;
214
+
215
+ try {
216
+ await git(['remote', 'add', remoteName, sshUrl], rootDir);
217
+
218
+ await git(['fetch', remoteName, branchName, '--no-tags'], rootDir);
219
+
220
+ const localRef = `${remoteName}/${branchName}`;
221
+ const headCommit = await git(['rev-parse', `FETCH_HEAD`], rootDir);
222
+
223
+ return { ok: true, localRef, headCommit };
224
+ } catch (err) {
225
+ return { ok: false, error: err.message };
226
+ } finally {
227
+ try { await git(['remote', 'remove', remoteName], rootDir); } catch { /* cleanup */ }
228
+ }
229
+ }