@proletariat/cli 0.3.110 → 0.3.112

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 (86) hide show
  1. package/dist/commands/gateway/connect.d.ts +33 -0
  2. package/dist/commands/gateway/connect.js +130 -0
  3. package/dist/commands/gateway/connect.js.map +1 -0
  4. package/dist/commands/gateway/disconnect.d.ts +21 -0
  5. package/dist/commands/gateway/disconnect.js +69 -0
  6. package/dist/commands/gateway/disconnect.js.map +1 -0
  7. package/dist/commands/gateway/start.d.ts +23 -0
  8. package/dist/commands/gateway/start.js +133 -0
  9. package/dist/commands/gateway/start.js.map +1 -0
  10. package/dist/commands/gateway/status.d.ts +16 -0
  11. package/dist/commands/gateway/status.js +76 -0
  12. package/dist/commands/gateway/status.js.map +1 -0
  13. package/dist/commands/gateway/test.d.ts +22 -0
  14. package/dist/commands/gateway/test.js +83 -0
  15. package/dist/commands/gateway/test.js.map +1 -0
  16. package/dist/commands/orchestrator/attach.d.ts +2 -0
  17. package/dist/commands/orchestrator/attach.js +80 -118
  18. package/dist/commands/orchestrator/attach.js.map +1 -1
  19. package/dist/commands/orchestrator/start.js +21 -0
  20. package/dist/commands/orchestrator/start.js.map +1 -1
  21. package/dist/commands/orchestrator/status.d.ts +3 -0
  22. package/dist/commands/orchestrator/status.js +104 -130
  23. package/dist/commands/orchestrator/status.js.map +1 -1
  24. package/dist/commands/orchestrator/stop.d.ts +2 -0
  25. package/dist/commands/orchestrator/stop.js +105 -107
  26. package/dist/commands/orchestrator/stop.js.map +1 -1
  27. package/dist/commands/reconcile.d.ts +29 -0
  28. package/dist/commands/reconcile.js +140 -0
  29. package/dist/commands/reconcile.js.map +1 -0
  30. package/dist/commands/session/attach.d.ts +2 -6
  31. package/dist/commands/session/attach.js +68 -97
  32. package/dist/commands/session/attach.js.map +1 -1
  33. package/dist/commands/session/list.d.ts +4 -1
  34. package/dist/commands/session/list.js +160 -326
  35. package/dist/commands/session/list.js.map +1 -1
  36. package/dist/commands/work/ship.js +131 -61
  37. package/dist/commands/work/ship.js.map +1 -1
  38. package/dist/commands/work/start.js +104 -49
  39. package/dist/commands/work/start.js.map +1 -1
  40. package/dist/lib/execution/session-utils.d.ts +4 -1
  41. package/dist/lib/execution/session-utils.js +3 -0
  42. package/dist/lib/execution/session-utils.js.map +1 -1
  43. package/dist/lib/gateway/channel-factory.d.ts +13 -0
  44. package/dist/lib/gateway/channel-factory.js +37 -0
  45. package/dist/lib/gateway/channel-factory.js.map +1 -0
  46. package/dist/lib/gateway/channels/telegram.d.ts +115 -0
  47. package/dist/lib/gateway/channels/telegram.js +215 -0
  48. package/dist/lib/gateway/channels/telegram.js.map +1 -0
  49. package/dist/lib/gateway/router.d.ts +84 -0
  50. package/dist/lib/gateway/router.js +140 -0
  51. package/dist/lib/gateway/router.js.map +1 -0
  52. package/dist/lib/gateway/session-poker.d.ts +35 -0
  53. package/dist/lib/gateway/session-poker.js +85 -0
  54. package/dist/lib/gateway/session-poker.js.map +1 -0
  55. package/dist/lib/gateway/types.d.ts +124 -0
  56. package/dist/lib/gateway/types.js +17 -0
  57. package/dist/lib/gateway/types.js.map +1 -0
  58. package/dist/lib/machine-db-mirror.d.ts +64 -0
  59. package/dist/lib/machine-db-mirror.js +82 -0
  60. package/dist/lib/machine-db-mirror.js.map +1 -0
  61. package/dist/lib/machine-db.d.ts +98 -0
  62. package/dist/lib/machine-db.js +152 -0
  63. package/dist/lib/machine-db.js.map +1 -1
  64. package/dist/lib/orchestrate/prompt-chain.d.ts +19 -4
  65. package/dist/lib/orchestrate/prompt-chain.js +19 -4
  66. package/dist/lib/orchestrate/prompt-chain.js.map +1 -1
  67. package/dist/lib/pr/index.d.ts +34 -2
  68. package/dist/lib/pr/index.js +95 -4
  69. package/dist/lib/pr/index.js.map +1 -1
  70. package/dist/lib/reconcile/core.d.ts +62 -0
  71. package/dist/lib/reconcile/core.js +137 -0
  72. package/dist/lib/reconcile/core.js.map +1 -0
  73. package/dist/lib/reconcile/index.d.ts +54 -0
  74. package/dist/lib/reconcile/index.js +377 -0
  75. package/dist/lib/reconcile/index.js.map +1 -0
  76. package/dist/lib/reconcile/types.d.ts +133 -0
  77. package/dist/lib/reconcile/types.js +16 -0
  78. package/dist/lib/reconcile/types.js.map +1 -0
  79. package/dist/lib/session/renderer.d.ts +121 -0
  80. package/dist/lib/session/renderer.js +547 -0
  81. package/dist/lib/session/renderer.js.map +1 -0
  82. package/dist/lib/update-check.d.ts +64 -7
  83. package/dist/lib/update-check.js +164 -20
  84. package/dist/lib/update-check.js.map +1 -1
  85. package/oclif.manifest.json +1203 -750
  86. package/package.json +1 -1
@@ -0,0 +1,547 @@
1
+ /**
2
+ * Session Renderer (PRLT-1272)
3
+ *
4
+ * Unified machine-wide session collection and grouped rendering.
5
+ *
6
+ * The principle: `prlt session list`, `prlt orchestrator status`, and related
7
+ * commands should ALWAYS query machine-wide for all sessions, regardless of
8
+ * whether the user is inside an HQ. Sessions are then visually grouped by
9
+ * their originating HQ — the current HQ bubbles to the top, other locations
10
+ * are still visible but de-emphasized.
11
+ *
12
+ * Sources collected:
13
+ * 1. machine.db `executions` table — cross-repo ticketless and ticketed work
14
+ * 2. Each registered HQ's workspace.db `agent_work` table — per-HQ workers
15
+ * 3. Discovered tmux/Docker orchestrator sessions — detected by name prefix
16
+ *
17
+ * Each entry is annotated with its hqPath/hqName/role so the renderer can
18
+ * group them.
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as os from 'node:os';
22
+ import * as path from 'node:path';
23
+ import { execSync } from 'node:child_process';
24
+ import { ExecutionStorage } from '../execution/storage.js';
25
+ import { openWorkspaceDatabase } from '../database/index.js';
26
+ import { MachineDB } from '../machine-db.js';
27
+ import { getRegisteredHeadquarters, getHeadquartersNameFromPath, normalizePath, } from '../machine-config.js';
28
+ import { checkContainerLiveness, discoverSessionId, findContainerSessionsByPrefix, getContainerTmuxSessionMap, getHostTmuxSessionNames, isContainerEnvironment, isSessionAlive, } from '../execution/session-utils.js';
29
+ import { findHQRoot } from '../workspace.js';
30
+ function captureRuntimeSnapshot() {
31
+ return {
32
+ hostTmuxSessions: getHostTmuxSessionNames(),
33
+ containerTmuxSessions: getContainerTmuxSessionMap(),
34
+ };
35
+ }
36
+ function findRunningOrchestratorContainers() {
37
+ try {
38
+ const output = execSync('docker ps --filter "name=prlt-orchestrator-" --format "{{.Names}}\t{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
39
+ if (!output)
40
+ return [];
41
+ return output
42
+ .split('\n')
43
+ .filter(Boolean)
44
+ .map(line => {
45
+ const [name, id] = line.split('\t');
46
+ return { name, containerId: id };
47
+ });
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ // =============================================================================
54
+ // HQ resolution helpers
55
+ // =============================================================================
56
+ /**
57
+ * Sanitize a name segment for use in tmux session names.
58
+ * Mirror of orchestrator/start.ts sanitizeName() — duplicated here to avoid
59
+ * importing a command file from a lib helper (cycle risk).
60
+ */
61
+ function sanitizeName(name) {
62
+ return name
63
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
64
+ .replace(/-+/g, '-')
65
+ .replace(/^-|-$/g, '');
66
+ }
67
+ /**
68
+ * Build the orchestrator session prefix used by `prlt orchestrator start`.
69
+ * Format: `prlt-orchestrator-{sanitizedHqName}-`
70
+ */
71
+ function orchestratorPrefixForHq(hqName) {
72
+ return `prlt-orchestrator-${sanitizeName(hqName) || 'default'}-`;
73
+ }
74
+ /**
75
+ * Try to attribute an orchestrator session/container name to one of the
76
+ * registered HQs. Returns the matching HQ entry, or null if no match.
77
+ */
78
+ function attributeOrchestratorToHq(sessionName, registered) {
79
+ for (const hq of registered) {
80
+ const hqName = getHeadquartersNameFromPath(hq.path);
81
+ const prefix = orchestratorPrefixForHq(hqName);
82
+ if (sessionName.startsWith(prefix)) {
83
+ return {
84
+ hqPath: hq.path,
85
+ hqName,
86
+ orchestratorName: sessionName.slice(prefix.length),
87
+ };
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ /**
93
+ * Replace the user's home directory in a path with `~` for display.
94
+ */
95
+ export function tildifyPath(p) {
96
+ if (!p)
97
+ return p;
98
+ const home = process.env.HOME || os.homedir();
99
+ if (p === home)
100
+ return '~';
101
+ if (p.startsWith(home + path.sep))
102
+ return '~' + p.slice(home.length);
103
+ return p;
104
+ }
105
+ // =============================================================================
106
+ // Workspace.db (per-HQ) collection
107
+ // =============================================================================
108
+ function collectFromWorkspaceDb(hqPath, hqName, snapshot, includeAll) {
109
+ const sessions = [];
110
+ let db = null;
111
+ try {
112
+ db = openWorkspaceDatabase(hqPath);
113
+ }
114
+ catch {
115
+ return sessions;
116
+ }
117
+ try {
118
+ const storage = new ExecutionStorage(db);
119
+ // Active executions (running + starting)
120
+ const active = [
121
+ ...storage.listExecutions({ status: 'running' }),
122
+ ...storage.listExecutions({ status: 'starting' }),
123
+ ];
124
+ // Self-healing: also check stopped records that may have a live tmux session
125
+ if (includeAll) {
126
+ active.push(...storage.listExecutions({ status: 'stopped' }));
127
+ }
128
+ else {
129
+ // Promote any stopped executions whose tmux sessions are still alive.
130
+ const stopped = storage.listExecutions({ status: 'stopped' });
131
+ for (const exec of stopped) {
132
+ if (isSessionAlive(exec)) {
133
+ active.push(exec);
134
+ }
135
+ }
136
+ }
137
+ for (const exec of active) {
138
+ const isContainer = isContainerEnvironment(exec.environment);
139
+ let exists = false;
140
+ let actualSessionId = discoverSessionId(exec, snapshot.hostTmuxSessions, snapshot.containerTmuxSessions) ||
141
+ exec.sessionId;
142
+ if (isContainer && exec.containerId) {
143
+ const containerStatus = checkContainerLiveness(exec.containerId);
144
+ if (containerStatus === 'running') {
145
+ if (actualSessionId) {
146
+ const containerSessions = findContainerSessionsByPrefix(snapshot.containerTmuxSessions, exec.containerId);
147
+ exists = containerSessions.includes(actualSessionId);
148
+ }
149
+ if (!exists) {
150
+ // Container running but no tmux session — still counts as alive.
151
+ exists = true;
152
+ actualSessionId =
153
+ actualSessionId || `container:${exec.containerId.substring(0, 12)}`;
154
+ }
155
+ }
156
+ }
157
+ else if (actualSessionId) {
158
+ exists = snapshot.hostTmuxSessions.includes(actualSessionId);
159
+ }
160
+ if (!actualSessionId)
161
+ continue;
162
+ if (!exists && !includeAll)
163
+ continue;
164
+ // Determine role: agentName 'orchestrator-*' or ticketId 'ORCH'
165
+ let role = 'worker';
166
+ if (exec.agentName.startsWith('orchestrator') ||
167
+ exec.ticketId === 'ORCH' ||
168
+ exec.ticketId === 'orchestrator') {
169
+ role = 'orchestrator';
170
+ }
171
+ sessions.push({
172
+ id: exec.id,
173
+ sessionId: actualSessionId,
174
+ ticketId: exec.ticketId,
175
+ agentName: exec.agentName,
176
+ status: deriveStatus(exec.status, exists, exec.lifecycleState),
177
+ role,
178
+ environment: isContainer ? 'container' : 'host',
179
+ containerId: exec.containerId,
180
+ exists,
181
+ source: 'db',
182
+ startedAt: exec.startedAt,
183
+ lastHeartbeat: exec.lastHeartbeat,
184
+ lifecycleState: exec.lifecycleState,
185
+ hqPath,
186
+ hqName,
187
+ repoPath: hqPath,
188
+ });
189
+ }
190
+ }
191
+ finally {
192
+ db.close();
193
+ }
194
+ return sessions;
195
+ }
196
+ function deriveStatus(rawStatus, exists, lifecycleState) {
197
+ if (lifecycleState === 'died')
198
+ return 'died';
199
+ if (lifecycleState === 'idle')
200
+ return 'idle';
201
+ if (!exists)
202
+ return 'stale';
203
+ return rawStatus || 'running';
204
+ }
205
+ // =============================================================================
206
+ // machine.db collection
207
+ // =============================================================================
208
+ function collectFromMachineDb(snapshot, registered, alreadySeenSessionIds, includeAll) {
209
+ const sessions = [];
210
+ let db = null;
211
+ try {
212
+ db = new MachineDB();
213
+ }
214
+ catch {
215
+ return sessions;
216
+ }
217
+ try {
218
+ const machineExecs = includeAll
219
+ ? db.listExecutions()
220
+ : db.getActiveExecutions();
221
+ for (const exec of machineExecs) {
222
+ // Skip duplicates already collected from a workspace.db.
223
+ if (exec.sessionId && alreadySeenSessionIds.has(exec.sessionId))
224
+ continue;
225
+ const isContainer = exec.environment === 'docker' || exec.environment === 'devcontainer';
226
+ let exists = false;
227
+ let actualSessionId = exec.sessionId;
228
+ if (isContainer && exec.containerId) {
229
+ const status = checkContainerLiveness(exec.containerId);
230
+ if (status === 'running') {
231
+ if (exec.sessionId) {
232
+ const containerSessions = findContainerSessionsByPrefix(snapshot.containerTmuxSessions, exec.containerId);
233
+ exists = containerSessions.includes(exec.sessionId);
234
+ }
235
+ if (!exists) {
236
+ exists = true;
237
+ actualSessionId =
238
+ actualSessionId || `container:${exec.containerId.substring(0, 12)}`;
239
+ }
240
+ }
241
+ }
242
+ else if (exec.sessionId) {
243
+ exists = snapshot.hostTmuxSessions.includes(exec.sessionId);
244
+ }
245
+ if (!actualSessionId)
246
+ continue;
247
+ if (!exists && !includeAll)
248
+ continue;
249
+ // Attribute repoPath to a registered HQ if possible.
250
+ const matchedHq = matchRepoToHq(exec.repoPath, registered);
251
+ // Detect role
252
+ let role = exec.ticketId ? 'worker' : 'headless';
253
+ if (exec.agentName.startsWith('orchestrator') ||
254
+ exec.ticketId === 'ORCH' ||
255
+ exec.ticketId === 'orchestrator') {
256
+ role = 'orchestrator';
257
+ }
258
+ sessions.push({
259
+ id: exec.id,
260
+ sessionId: actualSessionId,
261
+ ticketId: exec.ticketId || exec.id,
262
+ agentName: exec.agentName,
263
+ status: deriveStatus(exec.status, exists),
264
+ role,
265
+ environment: isContainer ? 'container' : 'host',
266
+ containerId: exec.containerId,
267
+ exists,
268
+ source: 'db',
269
+ startedAt: exec.startedAt,
270
+ hqPath: matchedHq?.hqPath,
271
+ hqName: matchedHq?.hqName,
272
+ repoPath: exec.repoPath,
273
+ });
274
+ }
275
+ }
276
+ finally {
277
+ db.close();
278
+ }
279
+ return sessions;
280
+ }
281
+ /**
282
+ * Match a repo path to one of the registered HQs.
283
+ * A repo is considered to belong to an HQ if it lives inside the HQ tree
284
+ * (e.g. an agent worktree under `<hqPath>/agents/...`).
285
+ */
286
+ function matchRepoToHq(repoPath, registered) {
287
+ if (!repoPath)
288
+ return null;
289
+ let normalized;
290
+ try {
291
+ normalized = normalizePath(repoPath);
292
+ }
293
+ catch {
294
+ normalized = repoPath;
295
+ }
296
+ for (const hq of registered) {
297
+ let hqNormalized;
298
+ try {
299
+ hqNormalized = normalizePath(hq.path);
300
+ }
301
+ catch {
302
+ hqNormalized = hq.path;
303
+ }
304
+ if (normalized === hqNormalized || normalized.startsWith(hqNormalized + path.sep)) {
305
+ return { hqPath: hq.path, hqName: getHeadquartersNameFromPath(hq.path) };
306
+ }
307
+ }
308
+ return null;
309
+ }
310
+ // =============================================================================
311
+ // Orchestrator discovery (tmux + Docker)
312
+ // =============================================================================
313
+ function collectOrchestrators(snapshot, registered, alreadySeenSessionIds) {
314
+ const sessions = [];
315
+ // Host tmux orchestrator sessions
316
+ for (const sessionName of snapshot.hostTmuxSessions) {
317
+ if (!sessionName.startsWith('prlt-orchestrator-'))
318
+ continue;
319
+ if (alreadySeenSessionIds.has(sessionName))
320
+ continue;
321
+ const attribution = attributeOrchestratorToHq(sessionName, registered);
322
+ sessions.push({
323
+ id: sessionName,
324
+ sessionId: sessionName,
325
+ ticketId: 'orchestrator',
326
+ agentName: attribution?.orchestratorName || sessionName,
327
+ status: 'running',
328
+ role: 'orchestrator',
329
+ environment: 'host',
330
+ exists: true,
331
+ source: 'discovered',
332
+ hqPath: attribution?.hqPath,
333
+ hqName: attribution?.hqName,
334
+ repoPath: attribution?.hqPath,
335
+ });
336
+ }
337
+ // Docker orchestrator containers
338
+ const dockerOrchestrators = findRunningOrchestratorContainers();
339
+ for (const { name, containerId } of dockerOrchestrators) {
340
+ if (alreadySeenSessionIds.has(name))
341
+ continue;
342
+ const attribution = attributeOrchestratorToHq(name, registered);
343
+ sessions.push({
344
+ id: name,
345
+ sessionId: name,
346
+ ticketId: 'orchestrator',
347
+ agentName: attribution?.orchestratorName || name,
348
+ status: 'running',
349
+ role: 'orchestrator',
350
+ environment: 'container',
351
+ containerId,
352
+ exists: true,
353
+ source: 'discovered',
354
+ hqPath: attribution?.hqPath,
355
+ hqName: attribution?.hqName,
356
+ repoPath: attribution?.hqPath,
357
+ });
358
+ }
359
+ return sessions;
360
+ }
361
+ // =============================================================================
362
+ // Public API: collect + group
363
+ // =============================================================================
364
+ /**
365
+ * Collect all sessions on the machine, regardless of cwd.
366
+ *
367
+ * Queries:
368
+ * 1. machine.db (cross-repo executions, source of truth for ticketless work)
369
+ * 2. Every registered HQ's workspace.db (workers per HQ)
370
+ * 3. tmux/Docker (orchestrators by name prefix)
371
+ *
372
+ * Filters are applied at the end so the underlying machine query path is
373
+ * the same in every command.
374
+ */
375
+ export function collectAllSessions(options = {}) {
376
+ const snapshot = captureRuntimeSnapshot();
377
+ const registered = getRegisteredHeadquarters().filter(hq => fs.existsSync(hq.path));
378
+ // Include the current cwd HQ even when it isn't in the machine registry
379
+ // (e.g. e2e tests that set up a throwaway HQ without registering it).
380
+ const cwdHq = findHQRoot(process.cwd());
381
+ const hqsToQuery = [...registered];
382
+ if (cwdHq) {
383
+ let cwdNormalized;
384
+ try {
385
+ cwdNormalized = normalizePath(cwdHq);
386
+ }
387
+ catch {
388
+ cwdNormalized = cwdHq;
389
+ }
390
+ const alreadyRegistered = registered.some(hq => {
391
+ try {
392
+ return normalizePath(hq.path) === cwdNormalized;
393
+ }
394
+ catch {
395
+ return hq.path === cwdNormalized;
396
+ }
397
+ });
398
+ if (!alreadyRegistered) {
399
+ hqsToQuery.push({
400
+ name: getHeadquartersNameFromPath(cwdHq),
401
+ path: cwdHq,
402
+ registeredAt: new Date().toISOString(),
403
+ });
404
+ }
405
+ }
406
+ const seenSessionIds = new Set();
407
+ const all = [];
408
+ // 1) Per-HQ workspace databases (workers + locally tracked orchestrators).
409
+ for (const hq of hqsToQuery) {
410
+ const hqName = getHeadquartersNameFromPath(hq.path);
411
+ const fromHq = collectFromWorkspaceDb(hq.path, hqName, snapshot, options.includeAll === true);
412
+ for (const s of fromHq) {
413
+ if (s.sessionId)
414
+ seenSessionIds.add(s.sessionId);
415
+ all.push(s);
416
+ }
417
+ }
418
+ // 2) machine.db ticketless / cross-repo executions
419
+ const fromMachine = collectFromMachineDb(snapshot, hqsToQuery, seenSessionIds, options.includeAll === true);
420
+ for (const s of fromMachine) {
421
+ if (s.sessionId)
422
+ seenSessionIds.add(s.sessionId);
423
+ all.push(s);
424
+ }
425
+ // 3) Discovered orchestrator tmux/Docker sessions not yet covered by a DB
426
+ const discoveredOrchestrators = collectOrchestrators(snapshot, hqsToQuery, seenSessionIds);
427
+ for (const s of discoveredOrchestrators) {
428
+ if (s.sessionId)
429
+ seenSessionIds.add(s.sessionId);
430
+ all.push(s);
431
+ }
432
+ // Apply filters
433
+ let filtered = all;
434
+ if (options.hqPathFilter) {
435
+ let normalized;
436
+ try {
437
+ normalized = normalizePath(options.hqPathFilter);
438
+ }
439
+ catch {
440
+ normalized = options.hqPathFilter;
441
+ }
442
+ filtered = filtered.filter(s => {
443
+ if (!s.hqPath)
444
+ return false;
445
+ try {
446
+ return normalizePath(s.hqPath) === normalized;
447
+ }
448
+ catch {
449
+ return s.hqPath === normalized;
450
+ }
451
+ });
452
+ }
453
+ if (options.roleFilter) {
454
+ filtered = filtered.filter(s => s.role === options.roleFilter);
455
+ }
456
+ return filtered;
457
+ }
458
+ /**
459
+ * Group sessions into "current HQ" vs "other locations" buckets.
460
+ *
461
+ * The current HQ is resolved from `process.cwd()` (or an explicit override).
462
+ * Sessions whose hqPath matches the current HQ go into `here`; everything
463
+ * else goes into `elsewhere`.
464
+ */
465
+ export function groupSessionsByHQ(sessions, currentHqOverride) {
466
+ const currentHq = currentHqOverride === null
467
+ ? undefined
468
+ : currentHqOverride ?? findHQRoot(process.cwd()) ?? undefined;
469
+ let normalizedCurrent;
470
+ if (currentHq) {
471
+ try {
472
+ normalizedCurrent = normalizePath(currentHq);
473
+ }
474
+ catch {
475
+ normalizedCurrent = currentHq;
476
+ }
477
+ }
478
+ const here = [];
479
+ const elsewhere = [];
480
+ for (const s of sessions) {
481
+ if (!normalizedCurrent || !s.hqPath) {
482
+ elsewhere.push(s);
483
+ continue;
484
+ }
485
+ let normalizedSession;
486
+ try {
487
+ normalizedSession = normalizePath(s.hqPath);
488
+ }
489
+ catch {
490
+ normalizedSession = s.hqPath;
491
+ }
492
+ if (normalizedSession === normalizedCurrent) {
493
+ here.push(s);
494
+ }
495
+ else {
496
+ elsewhere.push(s);
497
+ }
498
+ }
499
+ return {
500
+ currentHq,
501
+ currentHqName: currentHq ? getHeadquartersNameFromPath(currentHq) : undefined,
502
+ here,
503
+ elsewhere,
504
+ };
505
+ }
506
+ // =============================================================================
507
+ // Pretty rendering helpers (text)
508
+ // =============================================================================
509
+ /** Format a Date into a compact relative duration like "~15m", "~3h", "~2d". */
510
+ export function formatRelativeAge(start, now = new Date()) {
511
+ if (!start)
512
+ return '';
513
+ const diffMs = Math.max(0, now.getTime() - start.getTime());
514
+ const sec = Math.floor(diffMs / 1000);
515
+ if (sec < 60)
516
+ return `~${sec}s`;
517
+ const min = Math.floor(sec / 60);
518
+ if (min < 60)
519
+ return `~${min}m`;
520
+ const hr = Math.floor(min / 60);
521
+ if (hr < 48)
522
+ return `~${hr}h`;
523
+ const day = Math.floor(hr / 24);
524
+ return `~${day}d`;
525
+ }
526
+ /**
527
+ * Group elsewhere sessions by hqPath/hqName for nicer rendering.
528
+ * Sessions without an hqPath are bucketed under "(no HQ)".
529
+ */
530
+ export function bucketElsewhereByHq(elsewhere) {
531
+ const buckets = new Map();
532
+ for (const s of elsewhere) {
533
+ const key = s.hqPath ?? '__none__';
534
+ let bucket = buckets.get(key);
535
+ if (!bucket) {
536
+ bucket = {
537
+ hqPath: s.hqPath ?? null,
538
+ hqName: s.hqName ?? '(no HQ)',
539
+ sessions: [],
540
+ };
541
+ buckets.set(key, bucket);
542
+ }
543
+ bucket.sessions.push(s);
544
+ }
545
+ return Array.from(buckets.values());
546
+ }
547
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../../../src/lib/session/renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAEjC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,SAAS,EAAyB,MAAM,kBAAkB,CAAA;AACnE,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,aAAa,GAEd,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,6BAA6B,EAC7B,0BAA0B,EAC1B,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,GACf,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAiG5C,SAAS,sBAAsB;IAC7B,OAAO;QACL,gBAAgB,EAAE,uBAAuB,EAAE;QAC3C,qBAAqB,EAAE,0BAA0B,EAAE;KACpD,CAAA;AACH,CAAC;AAWD,SAAS,iCAAiC;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CACrB,6EAA6E,EAC7E,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CACvD,CAAC,IAAI,EAAE,CAAA;QACR,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,CAAA;QACtB,OAAO,MAAM;aACV,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC;aACf,GAAG,CAAC,IAAI,CAAC,EAAE;YACV,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACnC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,CAAA;QAClC,CAAC,CAAC,CAAA;IACN,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,wBAAwB;AACxB,gFAAgF;AAEhF;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI;SACR,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC;SAChC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED;;;GAGG;AACH,SAAS,uBAAuB,CAAC,MAAc;IAC7C,OAAO,qBAAqB,YAAY,CAAC,MAAM,CAAC,IAAI,SAAS,GAAG,CAAA;AAClE,CAAC;AAED;;;GAGG;AACH,SAAS,yBAAyB,CAChC,WAAmB,EACnB,UAAoC;IAEpC,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,2BAA2B,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;QACnD,MAAM,MAAM,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAA;QAC9C,IAAI,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACnC,OAAO;gBACL,MAAM,EAAE,EAAE,CAAC,IAAI;gBACf,MAAM;gBACN,gBAAgB,EAAE,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;aACnD,CAAA;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAA;IAChB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,CAAA;IAC7C,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,GAAG,CAAA;IAC1B,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACpE,OAAO,CAAC,CAAA;AACV,CAAC;AAED,gFAAgF;AAChF,mCAAmC;AACnC,gFAAgF;AAEhF,SAAS,sBAAsB,CAC7B,MAAc,EACd,MAAc,EACd,QAAyB,EACzB,UAAmB;IAEnB,MAAM,QAAQ,GAAqB,EAAE,CAAA;IACrC,IAAI,EAAE,GAA6B,IAAI,CAAA;IAEvC,IAAI,CAAC;QACH,EAAE,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,EAAE,CAAC,CAAA;QAExC,yCAAyC;QACzC,MAAM,MAAM,GAAgB;YAC1B,GAAG,OAAO,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YAChD,GAAG,OAAO,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;SAClD,CAAA;QAED,6EAA6E;QAC7E,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;QAC/D,CAAC;aAAM,CAAC;YACN,sEAAsE;YACtE,MAAM,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;YAC7D,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC3B,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;oBACzB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACnB,CAAC;YACH,CAAC;QACH,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,MAAM,WAAW,GAAG,sBAAsB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC5D,IAAI,MAAM,GAAG,KAAK,CAAA;YAClB,IAAI,eAAe,GACjB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC,qBAAqB,CAAC;gBAClF,IAAI,CAAC,SAAS,CAAA;YAEhB,IAAI,WAAW,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACpC,MAAM,eAAe,GAAG,sBAAsB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;gBAChE,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;oBAClC,IAAI,eAAe,EAAE,CAAC;wBACpB,MAAM,iBAAiB,GAAG,6BAA6B,CACrD,QAAQ,CAAC,qBAAqB,EAC9B,IAAI,CAAC,WAAW,CACjB,CAAA;wBACD,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAA;oBACtD,CAAC;oBACD,IAAI,CAAC,MAAM,EAAE,CAAC;wBACZ,iEAAiE;wBACjE,MAAM,GAAG,IAAI,CAAA;wBACb,eAAe;4BACb,eAAe,IAAI,aAAa,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAA;oBACvE,CAAC;gBACH,CAAC;YACH,CAAC;iBAAM,IAAI,eAAe,EAAE,CAAC;gBAC3B,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAA;YAC9D,CAAC;YAED,IAAI,CAAC,eAAe;gBAAE,SAAQ;YAC9B,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU;gBAAE,SAAQ;YAEpC,gEAAgE;YAChE,IAAI,IAAI,GAAgB,QAAQ,CAAA;YAChC,IACE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,cAAc,CAAC;gBACzC,IAAI,CAAC,QAAQ,KAAK,MAAM;gBACxB,IAAI,CAAC,QAAQ,KAAK,cAAc,EAChC,CAAC;gBACD,IAAI,GAAG,cAAc,CAAA;YACvB,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,IAAI,CAAC,EAAE;gBACX,SAAS,EAAE,eAAe;gBAC1B,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,CAAC;gBAC9D,IAAI;gBACJ,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM;gBAC/C,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,MAAM;gBACN,MAAM,EAAE,IAAI;gBACZ,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,MAAM;gBACN,MAAM;gBACN,QAAQ,EAAE,MAAM;aACjB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAA;IACZ,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,YAAY,CACnB,SAAiB,EACjB,MAAe,EACf,cAAuB;IAEvB,IAAI,cAAc,KAAK,MAAM;QAAE,OAAO,MAAM,CAAA;IAC5C,IAAI,cAAc,KAAK,MAAM;QAAE,OAAO,MAAM,CAAA;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAA;IAC3B,OAAQ,SAA2B,IAAI,SAAS,CAAA;AAClD,CAAC;AAED,gFAAgF;AAChF,wBAAwB;AACxB,gFAAgF;AAEhF,SAAS,oBAAoB,CAC3B,QAAyB,EACzB,UAAoC,EACpC,qBAAkC,EAClC,UAAmB;IAEnB,MAAM,QAAQ,GAAqB,EAAE,CAAA;IACrC,IAAI,EAAE,GAAqB,IAAI,CAAA;IAE/B,IAAI,CAAC;QACH,EAAE,GAAG,IAAI,SAAS,EAAE,CAAA;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,YAAY,GAAuB,UAAU;YACjD,CAAC,CAAC,EAAE,CAAC,cAAc,EAAE;YACrB,CAAC,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAA;QAE5B,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;YAChC,yDAAyD;YACzD,IAAI,IAAI,CAAC,SAAS,IAAI,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;gBAAE,SAAQ;YAEzE,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,KAAK,cAAc,CAAA;YACxF,IAAI,MAAM,GAAG,KAAK,CAAA;YAClB,IAAI,eAAe,GAAG,IAAI,CAAC,SAAS,CAAA;YAEpC,IAAI,WAAW,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACpC,MAAM,MAAM,GAAG,sBAAsB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;gBACvD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;wBACnB,MAAM,iBAAiB,GAAG,6BAA6B,CACrD,QAAQ,CAAC,qBAAqB,EAC9B,IAAI,CAAC,WAAW,CACjB,CAAA;wBACD,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;oBACrD,CAAC;oBACD,IAAI,CAAC,MAAM,EAAE,CAAC;wBACZ,MAAM,GAAG,IAAI,CAAA;wBACb,eAAe;4BACb,eAAe,IAAI,aAAa,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAA;oBACvE,CAAC;gBACH,CAAC;YACH,CAAC;iBAAM,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBAC1B,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAC7D,CAAC;YAED,IAAI,CAAC,eAAe;gBAAE,SAAQ;YAC9B,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU;gBAAE,SAAQ;YAEpC,qDAAqD;YACrD,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;YAE1D,cAAc;YACd,IAAI,IAAI,GAAgB,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAA;YAC7D,IACE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,cAAc,CAAC;gBACzC,IAAI,CAAC,QAAQ,KAAK,MAAM;gBACxB,IAAI,CAAC,QAAQ,KAAK,cAAc,EAChC,CAAC;gBACD,IAAI,GAAG,cAAc,CAAA;YACvB,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,IAAI,CAAC,EAAE;gBACX,SAAS,EAAE,eAAe;gBAC1B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,EAAE;gBAClC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;gBACzC,IAAI;gBACJ,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM;gBAC/C,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,MAAM;gBACN,MAAM,EAAE,IAAI;gBACZ,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,SAAS,EAAE,MAAM;gBACzB,MAAM,EAAE,SAAS,EAAE,MAAM;gBACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;aACxB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAA;IACZ,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CACpB,QAA4B,EAC5B,UAAoC;IAEpC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC1B,IAAI,UAAkB,CAAA;IACtB,IAAI,CAAC;QACH,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,UAAU,GAAG,QAAQ,CAAA;IACvB,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,IAAI,YAAoB,CAAA;QACxB,IAAI,CAAC;YACH,YAAY,GAAG,aAAa,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,YAAY,GAAG,EAAE,CAAC,IAAI,CAAA;QACxB,CAAC;QACD,IAAI,UAAU,KAAK,YAAY,IAAI,UAAU,CAAC,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAClF,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,2BAA2B,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAA;QAC1E,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,gFAAgF;AAChF,yCAAyC;AACzC,gFAAgF;AAEhF,SAAS,oBAAoB,CAC3B,QAAyB,EACzB,UAAoC,EACpC,qBAAkC;IAElC,MAAM,QAAQ,GAAqB,EAAE,CAAA;IAErC,kCAAkC;IAClC,KAAK,MAAM,WAAW,IAAI,QAAQ,CAAC,gBAAgB,EAAE,CAAC;QACpD,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,oBAAoB,CAAC;YAAE,SAAQ;QAC3D,IAAI,qBAAqB,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,SAAQ;QAEpD,MAAM,WAAW,GAAG,yBAAyB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;QACtE,QAAQ,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,WAAW;YACf,SAAS,EAAE,WAAW;YACtB,QAAQ,EAAE,cAAc;YACxB,SAAS,EAAE,WAAW,EAAE,gBAAgB,IAAI,WAAW;YACvD,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,cAAc;YACpB,WAAW,EAAE,MAAM;YACnB,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,YAAY;YACpB,MAAM,EAAE,WAAW,EAAE,MAAM;YAC3B,MAAM,EAAE,WAAW,EAAE,MAAM;YAC3B,QAAQ,EAAE,WAAW,EAAE,MAAM;SAC9B,CAAC,CAAA;IACJ,CAAC;IAED,iCAAiC;IACjC,MAAM,mBAAmB,GAAG,iCAAiC,EAAE,CAAA;IAC/D,KAAK,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,mBAAmB,EAAE,CAAC;QACxD,IAAI,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAQ;QAE7C,MAAM,WAAW,GAAG,yBAAyB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;QAC/D,QAAQ,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,IAAI;YACR,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,cAAc;YACxB,SAAS,EAAE,WAAW,EAAE,gBAAgB,IAAI,IAAI;YAChD,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,cAAc;YACpB,WAAW,EAAE,WAAW;YACxB,WAAW;YACX,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,YAAY;YACpB,MAAM,EAAE,WAAW,EAAE,MAAM;YAC3B,MAAM,EAAE,WAAW,EAAE,MAAM;YAC3B,QAAQ,EAAE,WAAW,EAAE,MAAM;SAC9B,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,gFAAgF;AAChF,8BAA8B;AAC9B,gFAAgF;AAEhF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAA0B,EAAE;IAC7D,MAAM,QAAQ,GAAG,sBAAsB,EAAE,CAAA;IACzC,MAAM,UAAU,GAAG,yBAAyB,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEnF,wEAAwE;IACxE,sEAAsE;IACtE,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IACvC,MAAM,UAAU,GAA6B,CAAC,GAAG,UAAU,CAAC,CAAA;IAC5D,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,aAAqB,CAAA;QACzB,IAAI,CAAC;YACH,aAAa,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,aAAa,GAAG,KAAK,CAAA;QACvB,CAAC;QACD,MAAM,iBAAiB,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;YAC7C,IAAI,CAAC;gBACH,OAAO,aAAa,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,aAAa,CAAA;YACjD,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,CAAC,IAAI,KAAK,aAAa,CAAA;YAClC,CAAC;QACH,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACvB,UAAU,CAAC,IAAI,CAAC;gBACd,IAAI,EAAE,2BAA2B,CAAC,KAAK,CAAC;gBACxC,IAAI,EAAE,KAAK;gBACX,YAAY,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACvC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAA;IACxC,MAAM,GAAG,GAAqB,EAAE,CAAA;IAEhC,2EAA2E;IAC3E,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,2BAA2B,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;QACnD,MAAM,MAAM,GAAG,sBAAsB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,KAAK,IAAI,CAAC,CAAA;QAC7F,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,IAAI,CAAC,CAAC,SAAS;gBAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;YAChD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACb,CAAC;IACH,CAAC;IAED,mDAAmD;IACnD,MAAM,WAAW,GAAG,oBAAoB,CACtC,QAAQ,EACR,UAAU,EACV,cAAc,EACd,OAAO,CAAC,UAAU,KAAK,IAAI,CAC5B,CAAA;IACD,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,SAAS;YAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAChD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACb,CAAC;IAED,0EAA0E;IAC1E,MAAM,uBAAuB,GAAG,oBAAoB,CAAC,QAAQ,EAAE,UAAU,EAAE,cAAc,CAAC,CAAA;IAC1F,KAAK,MAAM,CAAC,IAAI,uBAAuB,EAAE,CAAC;QACxC,IAAI,CAAC,CAAC,SAAS;YAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAChD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACb,CAAC;IAED,gBAAgB;IAChB,IAAI,QAAQ,GAAG,GAAG,CAAA;IAClB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACzB,IAAI,UAAkB,CAAA;QACtB,IAAI,CAAC;YACH,UAAU,GAAG,aAAa,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,GAAG,OAAO,CAAC,YAAY,CAAA;QACnC,CAAC;QACD,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;YAC7B,IAAI,CAAC,CAAC,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAA;YAC3B,IAAI,CAAC;gBACH,OAAO,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,UAAU,CAAA;YAC/C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,CAAC,MAAM,KAAK,UAAU,CAAA;YAChC,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;IAChE,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAA0B,EAC1B,iBAAiC;IAEjC,MAAM,SAAS,GACb,iBAAiB,KAAK,IAAI;QACxB,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,iBAAiB,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,SAAS,CAAA;IAEjE,IAAI,iBAAqC,CAAA;IACzC,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC;YACH,iBAAiB,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB,GAAG,SAAS,CAAA;QAC/B,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAqB,EAAE,CAAA;IACjC,MAAM,SAAS,GAAqB,EAAE,CAAA;IAEtC,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,iBAAiB,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;YACpC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,SAAQ;QACV,CAAC;QACD,IAAI,iBAAyB,CAAA;QAC7B,IAAI,CAAC;YACH,iBAAiB,GAAG,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAA;QAC9B,CAAC;QACD,IAAI,iBAAiB,KAAK,iBAAiB,EAAE,CAAC;YAC5C,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACd,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;IAED,OAAO;QACL,SAAS;QACT,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC,2BAA2B,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS;QAC7E,IAAI;QACJ,SAAS;KACV,CAAA;AACH,CAAC;AAED,gFAAgF;AAChF,kCAAkC;AAClC,gFAAgF;AAEhF,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAAC,KAAY,EAAE,MAAY,IAAI,IAAI,EAAE;IACpE,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IACrB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACrC,IAAI,GAAG,GAAG,EAAE;QAAE,OAAO,IAAI,GAAG,GAAG,CAAA;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAA;IAChC,IAAI,GAAG,GAAG,EAAE;QAAE,OAAO,IAAI,GAAG,GAAG,CAAA;IAC/B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAA;IAC/B,IAAI,EAAE,GAAG,EAAE;QAAE,OAAO,IAAI,EAAE,GAAG,CAAA;IAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;IAC/B,OAAO,IAAI,GAAG,GAAG,CAAA;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAA2B;IAE3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAiF,CAAA;IACxG,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,IAAI,UAAU,CAAA;QAClC,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG;gBACP,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI;gBACxB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;gBAC7B,QAAQ,EAAE,EAAE;aACb,CAAA;YACD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QAC1B,CAAC;QACD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;AACrC,CAAC"}
@@ -84,23 +84,79 @@ export declare function isNewerVersion(current: string, latest: string): boolean
84
84
  /**
85
85
  * Get the npm global node_modules directory.
86
86
  * Returns null if it cannot be determined.
87
+ *
88
+ * Honours the `PRLT_NPM_MODULES_DIR_OVERRIDE` env var for tests so the
89
+ * backup/restore logic in {@link runNpmInstallWithRetry} can be exercised
90
+ * against a temp directory instead of the user's real npm prefix.
87
91
  */
88
92
  export declare function getNpmGlobalModulesDir(): string | null;
89
93
  /**
90
- * Remove the existing @proletariat/cli directory from npm global node_modules.
91
- * This prevents ENOTEMPTY errors during `npm install -g`, which is a known npm
92
- * bug where renaming the old package directory fails.
94
+ * Captured state of a bin symlink used so we can restore the symlink
95
+ * if npm removed it during a failed retry install (PRLT-1276).
96
+ */
97
+ interface BinSymlinkRecord {
98
+ /** Absolute path to the bin entry (e.g. `<prefix>/bin/prlt`) */
99
+ path: string;
100
+ /** Original link target, as returned by `readlink` (may be relative) */
101
+ target: string;
102
+ }
103
+ /**
104
+ * State captured before we disturb the current @proletariat/cli install,
105
+ * so it can be restored if the retry install fails.
106
+ */
107
+ export interface NpmPackageBackup {
108
+ /** Absolute path to the original package directory */
109
+ originalPath: string;
110
+ /**
111
+ * Absolute path to the backup (rename-aside) location.
112
+ * Empty string when there was nothing to back up (package dir did not exist).
113
+ */
114
+ backupPath: string;
115
+ /** Bin symlinks that existed before the backup was taken */
116
+ binSymlinks: BinSymlinkRecord[];
117
+ }
118
+ /**
119
+ * Back up the existing @proletariat/cli package directory via atomic rename
120
+ * so an install retry can run against a clean slate while preserving the
121
+ * ability to restore the previous installation if the retry fails.
93
122
  *
94
- * Safe to call even if the directory doesn't exist.
123
+ * The backup is placed adjacent to the original in the same directory, so
124
+ * the rename is guaranteed to be atomic. Returns null if the rename fails
125
+ * (e.g. permission error) — in that case the caller must not proceed,
126
+ * because destroying the package dir without a backup is exactly the
127
+ * PRLT-1276 regression we are trying to avoid.
128
+ *
129
+ * When there is no existing package dir to back up, this still returns a
130
+ * valid `NpmPackageBackup` with an empty `backupPath` — the bin symlink
131
+ * records are still captured so they can be restored on failure.
132
+ */
133
+ export declare function backupNpmPackageDir(): NpmPackageBackup | null;
134
+ /**
135
+ * Restore the backup created by {@link backupNpmPackageDir} after a failed
136
+ * retry. Removes any partially-installed content at the original path,
137
+ * renames the backup back into place, and re-creates bin symlinks that npm
138
+ * may have removed during the failed install.
95
139
  */
96
- export declare function cleanNpmPackageDir(): boolean;
140
+ export declare function restoreNpmPackageBackup(info: NpmPackageBackup): boolean;
141
+ /**
142
+ * Remove the backup created by {@link backupNpmPackageDir} after a
143
+ * successful retry. Best-effort — failures are not fatal.
144
+ */
145
+ export declare function discardNpmPackageBackup(info: NpmPackageBackup): void;
97
146
  /**
98
147
  * Run an npm global install command with automatic ENOTEMPTY retry.
99
148
  *
100
149
  * npm global installs frequently fail with ENOTEMPTY when renaming the
101
150
  * old package directory (a known npm bug). When this error is detected
102
- * in stderr, the old package directory is removed and the install is
103
- * retried transparently.
151
+ * in stderr, the existing @proletariat/cli directory is moved aside via
152
+ * an atomic rename, the install is retried against a clean slate, and
153
+ * the backup is either discarded (on success) or restored (on failure).
154
+ *
155
+ * PRLT-1276: this is the restore-on-failure path that fixes the "prlt
156
+ * intermittently disappears from PATH" bug. Previously we `rm -rf`'d the
157
+ * package directory before retrying, so a retry that failed for any reason
158
+ * (network glitch, timeout, nvm mid-switch) left the user with a deleted
159
+ * package and a dangling `bin/prlt` symlink.
104
160
  *
105
161
  * Throws on failure (both non-ENOTEMPTY errors and failed retries).
106
162
  */
@@ -152,3 +208,4 @@ export declare function dismissVersion(version: string): void;
152
208
  * This is the default "Skip" behavior — just don't persist anything.
153
209
  */
154
210
  export declare function dismissSession(): void;
211
+ export {};