@virtengine/openfleet 0.25.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 (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. package/worktree-manager.mjs +1266 -0
@@ -0,0 +1,537 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
8
+ const repoRoot = resolve(__dirname, "..", "..");
9
+
10
+ const DEFAULT_LEASE_TTL_MINUTES = 120;
11
+ const DEFAULT_REGISTRY = {
12
+ version: 1,
13
+ registry_name: "shared-cloud-workspaces",
14
+ default_lease_ttl_minutes: DEFAULT_LEASE_TTL_MINUTES,
15
+ workspaces: [],
16
+ };
17
+
18
+ const AVAILABILITY_STATES = new Set([
19
+ "available",
20
+ "leased",
21
+ "maintenance",
22
+ "offline",
23
+ "disabled",
24
+ ]);
25
+
26
+ const AVAILABILITY_ALIASES = {
27
+ idle: "available",
28
+ free: "available",
29
+ busy: "leased",
30
+ inuse: "leased",
31
+ };
32
+
33
+ function normalizeId(value) {
34
+ return String(value || "").trim().toLowerCase();
35
+ }
36
+
37
+ function normalizeAvailability(value) {
38
+ const raw = String(value || "").trim().toLowerCase();
39
+ if (!raw) return "available";
40
+ const aliased = AVAILABILITY_ALIASES[raw] || raw;
41
+ return AVAILABILITY_STATES.has(aliased) ? aliased : "available";
42
+ }
43
+
44
+ function toIso(value) {
45
+ if (!value) return null;
46
+ const ts = Date.parse(value);
47
+ if (!Number.isFinite(ts)) return null;
48
+ return new Date(ts).toISOString();
49
+ }
50
+
51
+ function ensureIso(date) {
52
+ return new Date(date).toISOString();
53
+ }
54
+
55
+ function normalizeLease(lease) {
56
+ if (!lease) return null;
57
+ const owner = String(lease.owner || "").trim();
58
+ const claimedAt = toIso(lease.claimed_at);
59
+ const expiresAt = toIso(lease.lease_expires_at);
60
+ if (!owner || !claimedAt || !expiresAt) return null;
61
+ const ttlMinutes = Number(lease.lease_ttl_minutes || 0);
62
+ return {
63
+ lease_id: lease.lease_id || randomUUID(),
64
+ owner,
65
+ claimed_at: claimedAt,
66
+ lease_expires_at: expiresAt,
67
+ lease_ttl_minutes: Number.isFinite(ttlMinutes) && ttlMinutes > 0 ? ttlMinutes : null,
68
+ last_renewed_at: toIso(lease.last_renewed_at) || claimedAt,
69
+ notes: lease.notes || "",
70
+ };
71
+ }
72
+
73
+ function normalizeWorkspace(workspace) {
74
+ if (!workspace) return null;
75
+ const id = normalizeId(workspace.id);
76
+ if (!id) return null;
77
+ const availability = normalizeAvailability(workspace.availability);
78
+ const lease = normalizeLease(workspace.lease);
79
+ const resolvedAvailability = lease ? "leased" : availability;
80
+ return {
81
+ id,
82
+ name: workspace.name || workspace.id || id,
83
+ provider: workspace.provider || "vibe-kanban",
84
+ region: workspace.region || "",
85
+ owner: workspace.owner || "",
86
+ availability_before_lease: workspace.availability_before_lease || null,
87
+ availability: resolvedAvailability,
88
+ lease,
89
+ lease_ttl_minutes: workspace.lease_ttl_minutes || null,
90
+ metadata: workspace.metadata || {},
91
+ };
92
+ }
93
+
94
+ function normalizeRegistry(raw) {
95
+ const registry = raw && typeof raw === "object" ? raw : {};
96
+ const workspaces = Array.isArray(registry.workspaces)
97
+ ? registry.workspaces.map(normalizeWorkspace).filter(Boolean)
98
+ : [];
99
+ const ttlMinutes = Number(registry.default_lease_ttl_minutes || 0);
100
+ return {
101
+ version: registry.version || DEFAULT_REGISTRY.version,
102
+ registry_name: registry.registry_name || DEFAULT_REGISTRY.registry_name,
103
+ default_lease_ttl_minutes:
104
+ Number.isFinite(ttlMinutes) && ttlMinutes > 0
105
+ ? ttlMinutes
106
+ : DEFAULT_REGISTRY.default_lease_ttl_minutes,
107
+ workspaces,
108
+ };
109
+ }
110
+
111
+ function getRegistryPath(options = {}) {
112
+ if (options.registryPath) {
113
+ return resolve(options.registryPath);
114
+ }
115
+ const envPath =
116
+ process.env.VE_SHARED_WORKSPACE_REGISTRY ||
117
+ process.env.VE_SHARED_WORKSPACE_REGISTRY_PATH ||
118
+ process.env.VK_SHARED_WORKSPACE_REGISTRY_PATH ||
119
+ "";
120
+ if (envPath) {
121
+ return resolve(envPath);
122
+ }
123
+ return resolve(repoRoot, ".cache", "openfleet", "shared-workspaces.json");
124
+ }
125
+
126
+ function getSeedPath(options = {}) {
127
+ if (options.seedPath) {
128
+ return resolve(options.seedPath);
129
+ }
130
+ return resolve(__dirname, "shared-workspaces.json");
131
+ }
132
+
133
+ function getAuditPath(options = {}) {
134
+ if (options.auditPath) {
135
+ return resolve(options.auditPath);
136
+ }
137
+ const envPath =
138
+ process.env.VE_SHARED_WORKSPACE_AUDIT_LOG ||
139
+ process.env.VE_SHARED_WORKSPACE_AUDIT_PATH ||
140
+ process.env.VK_SHARED_WORKSPACE_AUDIT_PATH ||
141
+ "";
142
+ if (envPath) {
143
+ return resolve(envPath);
144
+ }
145
+ return resolve(
146
+ repoRoot,
147
+ ".cache",
148
+ "openfleet",
149
+ "shared-workspace-audit.jsonl",
150
+ );
151
+ }
152
+
153
+ async function writeRegistryFile(path, registry) {
154
+ await mkdir(resolve(path, ".."), { recursive: true });
155
+ const payload = JSON.stringify(registry, null, 2);
156
+ const tempPath = `${path}.tmp-${Date.now()}`;
157
+ await writeFile(tempPath, payload, "utf8");
158
+ await rename(tempPath, path);
159
+ }
160
+
161
+ async function appendAuditEntry(entry, options = {}) {
162
+ const auditPath = getAuditPath(options);
163
+ await mkdir(resolve(auditPath, ".."), { recursive: true });
164
+ const payload = `${JSON.stringify(entry)}\n`;
165
+ await writeFile(auditPath, payload, { encoding: "utf8", flag: "a" });
166
+ }
167
+
168
+ export async function loadSharedWorkspaceRegistry(options = {}) {
169
+ const registryPath = getRegistryPath(options);
170
+ let registry = null;
171
+ if (existsSync(registryPath)) {
172
+ try {
173
+ const raw = await readFile(registryPath, "utf8");
174
+ registry = normalizeRegistry(JSON.parse(raw));
175
+ } catch (err) {
176
+ console.warn(
177
+ `[shared-workspace-registry] failed to read ${registryPath}: ${err.message || err}`,
178
+ );
179
+ }
180
+ }
181
+ if (!registry) {
182
+ const seedPath = getSeedPath(options);
183
+ if (existsSync(seedPath)) {
184
+ try {
185
+ const raw = await readFile(seedPath, "utf8");
186
+ registry = normalizeRegistry(JSON.parse(raw));
187
+ } catch (err) {
188
+ console.warn(
189
+ `[shared-workspace-registry] failed to read seed ${seedPath}: ${err.message || err}`,
190
+ );
191
+ }
192
+ }
193
+ }
194
+ if (!registry) {
195
+ registry = normalizeRegistry(DEFAULT_REGISTRY);
196
+ }
197
+ return {
198
+ ...registry,
199
+ registry_path: registryPath,
200
+ registry_seed_path: getSeedPath(options),
201
+ audit_log_path: getAuditPath(options),
202
+ };
203
+ }
204
+
205
+ export async function saveSharedWorkspaceRegistry(registry, options = {}) {
206
+ if (!registry) return;
207
+ const path = registry.registry_path || getRegistryPath(options);
208
+ const payload = {
209
+ version: registry.version || DEFAULT_REGISTRY.version,
210
+ registry_name: registry.registry_name || DEFAULT_REGISTRY.registry_name,
211
+ default_lease_ttl_minutes:
212
+ registry.default_lease_ttl_minutes || DEFAULT_REGISTRY.default_lease_ttl_minutes,
213
+ workspaces: registry.workspaces || [],
214
+ };
215
+ await writeRegistryFile(path, payload);
216
+ }
217
+
218
+ export function resolveSharedWorkspace(registry, candidateId) {
219
+ if (!registry || !Array.isArray(registry.workspaces)) return null;
220
+ const target = normalizeId(candidateId);
221
+ if (!target) return null;
222
+ return registry.workspaces.find((ws) => ws.id === target) || null;
223
+ }
224
+
225
+ export function isLeaseExpired(lease, now = new Date()) {
226
+ if (!lease || !lease.lease_expires_at) return false;
227
+ const expiry = Date.parse(lease.lease_expires_at);
228
+ if (!Number.isFinite(expiry)) return false;
229
+ return expiry <= now.getTime();
230
+ }
231
+
232
+ function buildLease(owner, ttlMinutes, now, note) {
233
+ const claimedAt = ensureIso(now);
234
+ const expiresAt = ensureIso(now.getTime() + ttlMinutes * 60 * 1000);
235
+ return {
236
+ lease_id: randomUUID(),
237
+ owner,
238
+ claimed_at: claimedAt,
239
+ lease_expires_at: expiresAt,
240
+ lease_ttl_minutes: ttlMinutes,
241
+ last_renewed_at: claimedAt,
242
+ notes: note || "",
243
+ };
244
+ }
245
+
246
+ function restoreAvailability(workspace) {
247
+ const fallback = normalizeAvailability(
248
+ workspace.availability_before_lease || "available",
249
+ );
250
+ const resolved = fallback === "leased" ? "available" : fallback;
251
+ workspace.availability = resolved;
252
+ workspace.availability_before_lease = null;
253
+ }
254
+
255
+ export async function sweepExpiredLeases(options = {}) {
256
+ const now = options.now ? new Date(options.now) : new Date();
257
+ const registry = options.registry
258
+ ? options.registry
259
+ : await loadSharedWorkspaceRegistry(options);
260
+ if (!registry || !Array.isArray(registry.workspaces)) {
261
+ return { registry, expired: [] };
262
+ }
263
+ const expired = [];
264
+ for (const workspace of registry.workspaces) {
265
+ if (!workspace?.lease) continue;
266
+ if (!isLeaseExpired(workspace.lease, now)) continue;
267
+ const lease = workspace.lease;
268
+ workspace.lease = null;
269
+ restoreAvailability(workspace);
270
+ workspace.last_released_at = ensureIso(now);
271
+ expired.push({ workspace, lease });
272
+ await appendAuditEntry(
273
+ {
274
+ ts: ensureIso(now),
275
+ action: "lease_expired",
276
+ workspace_id: workspace.id,
277
+ owner: lease.owner,
278
+ lease_id: lease.lease_id,
279
+ actor: options.actor || "system",
280
+ lease_expires_at: lease.lease_expires_at,
281
+ },
282
+ options,
283
+ );
284
+ }
285
+ if (expired.length > 0) {
286
+ await saveSharedWorkspaceRegistry(registry, options);
287
+ }
288
+ return { registry, expired };
289
+ }
290
+
291
+ export async function claimSharedWorkspace(options = {}) {
292
+ const now = options.now ? new Date(options.now) : new Date();
293
+ let registry = options.registry
294
+ ? options.registry
295
+ : await loadSharedWorkspaceRegistry(options);
296
+ const sweepResult = await sweepExpiredLeases({
297
+ registry,
298
+ now,
299
+ actor: options.actor,
300
+ auditPath: options.auditPath,
301
+ registryPath: options.registryPath,
302
+ });
303
+ registry = sweepResult.registry;
304
+ const workspace = resolveSharedWorkspace(registry, options.workspaceId);
305
+ if (!workspace) {
306
+ return { error: `Unknown shared workspace '${options.workspaceId}'.` };
307
+ }
308
+ if (workspace.lease && !options.force) {
309
+ return {
310
+ error: `Workspace '${workspace.id}' is already leased to ${workspace.lease.owner}.`,
311
+ };
312
+ }
313
+ if (workspace.availability !== "available" && !options.force) {
314
+ return {
315
+ error: `Workspace '${workspace.id}' is not available (state: ${workspace.availability}).`,
316
+ };
317
+ }
318
+ const ttlMinutes = Number(
319
+ options.ttlMinutes || workspace.lease_ttl_minutes || registry.default_lease_ttl_minutes,
320
+ );
321
+ if (!Number.isFinite(ttlMinutes) || ttlMinutes <= 0) {
322
+ return { error: "Invalid lease TTL minutes." };
323
+ }
324
+ const owner = String(options.owner || options.actor || "unknown").trim();
325
+ if (!owner) {
326
+ return { error: "Owner is required to claim a workspace." };
327
+ }
328
+ const previousLease = workspace.lease && options.force ? workspace.lease : null;
329
+ workspace.availability_before_lease =
330
+ workspace.availability === "leased"
331
+ ? workspace.availability_before_lease || "available"
332
+ : workspace.availability;
333
+ workspace.availability = "leased";
334
+ workspace.lease = buildLease(owner, ttlMinutes, now, options.note);
335
+ workspace.last_claimed_at = ensureIso(now);
336
+
337
+ await saveSharedWorkspaceRegistry(registry, options);
338
+ if (previousLease) {
339
+ await appendAuditEntry(
340
+ {
341
+ ts: ensureIso(now),
342
+ action: "force_release",
343
+ workspace_id: workspace.id,
344
+ owner: previousLease.owner,
345
+ lease_id: previousLease.lease_id,
346
+ actor: options.actor || owner,
347
+ reason: "overridden_by_claim",
348
+ },
349
+ options,
350
+ );
351
+ }
352
+ await appendAuditEntry(
353
+ {
354
+ ts: ensureIso(now),
355
+ action: "claim",
356
+ workspace_id: workspace.id,
357
+ owner,
358
+ lease_id: workspace.lease.lease_id,
359
+ lease_expires_at: workspace.lease.lease_expires_at,
360
+ lease_ttl_minutes: ttlMinutes,
361
+ actor: options.actor || owner,
362
+ note: options.note || "",
363
+ },
364
+ options,
365
+ );
366
+
367
+ return { registry, workspace, lease: workspace.lease };
368
+ }
369
+
370
+ export async function releaseSharedWorkspace(options = {}) {
371
+ const now = options.now ? new Date(options.now) : new Date();
372
+ const registry = options.registry
373
+ ? options.registry
374
+ : await loadSharedWorkspaceRegistry(options);
375
+ const workspace = resolveSharedWorkspace(registry, options.workspaceId);
376
+ if (!workspace) {
377
+ return { error: `Unknown shared workspace '${options.workspaceId}'.` };
378
+ }
379
+ if (!workspace.lease) {
380
+ return { error: `Workspace '${workspace.id}' is not leased.` };
381
+ }
382
+ const owner = String(options.owner || "").trim();
383
+ if (owner && normalizeId(owner) !== normalizeId(workspace.lease.owner) && !options.force) {
384
+ return {
385
+ error: `Workspace '${workspace.id}' is leased to ${workspace.lease.owner}. Use --force to release anyway.`,
386
+ };
387
+ }
388
+ const previousLease = workspace.lease;
389
+ workspace.lease = null;
390
+ restoreAvailability(workspace);
391
+ workspace.last_released_at = ensureIso(now);
392
+
393
+ await saveSharedWorkspaceRegistry(registry, options);
394
+ await appendAuditEntry(
395
+ {
396
+ ts: ensureIso(now),
397
+ action: "release",
398
+ workspace_id: workspace.id,
399
+ owner: previousLease.owner,
400
+ lease_id: previousLease.lease_id,
401
+ actor: options.actor || owner || previousLease.owner,
402
+ reason: options.reason || "",
403
+ },
404
+ options,
405
+ );
406
+
407
+ return { registry, workspace, previousLease };
408
+ }
409
+
410
+ export async function renewSharedWorkspaceLease(options = {}) {
411
+ const now = options.now ? new Date(options.now) : new Date();
412
+ const registry = options.registry
413
+ ? options.registry
414
+ : await loadSharedWorkspaceRegistry(options);
415
+ const workspace = resolveSharedWorkspace(registry, options.workspaceId);
416
+ if (!workspace) {
417
+ return { error: `Unknown shared workspace '${options.workspaceId}'.` };
418
+ }
419
+ if (!workspace.lease) {
420
+ return { error: `Workspace '${workspace.id}' is not currently leased.` };
421
+ }
422
+ const owner = String(options.owner || "").trim();
423
+ if (owner && normalizeId(owner) !== normalizeId(workspace.lease.owner)) {
424
+ return {
425
+ error: `Workspace '${workspace.id}' is leased to ${workspace.lease.owner}, cannot renew.`,
426
+ };
427
+ }
428
+ const ttlMinutes = Number(
429
+ options.ttlMinutes || workspace.lease.lease_ttl_minutes || registry.default_lease_ttl_minutes,
430
+ );
431
+ if (!Number.isFinite(ttlMinutes) || ttlMinutes <= 0) {
432
+ return { error: "Invalid lease TTL minutes for renewal." };
433
+ }
434
+ const previousExpiry = workspace.lease.lease_expires_at;
435
+ const newExpiresAt = ensureIso(now.getTime() + ttlMinutes * 60 * 1000);
436
+ workspace.lease.lease_expires_at = newExpiresAt;
437
+ workspace.lease.last_renewed_at = ensureIso(now);
438
+ workspace.lease.lease_ttl_minutes = ttlMinutes;
439
+
440
+ await saveSharedWorkspaceRegistry(registry, options);
441
+ await appendAuditEntry(
442
+ {
443
+ ts: ensureIso(now),
444
+ action: "renew_lease",
445
+ workspace_id: workspace.id,
446
+ owner: workspace.lease.owner,
447
+ lease_id: workspace.lease.lease_id,
448
+ previous_expires_at: previousExpiry,
449
+ new_expires_at: newExpiresAt,
450
+ lease_ttl_minutes: ttlMinutes,
451
+ actor: options.actor || workspace.lease.owner,
452
+ },
453
+ options,
454
+ );
455
+
456
+ return { registry, workspace, lease: workspace.lease };
457
+ }
458
+
459
+ function formatExpiresIn(expiresAt, now) {
460
+ if (!expiresAt) return "unknown";
461
+ const expiry = Date.parse(expiresAt);
462
+ if (!Number.isFinite(expiry)) return "unknown";
463
+ const diffMs = expiry - now.getTime();
464
+ if (diffMs <= 0) return "expired";
465
+ const minutes = Math.round(diffMs / 60000);
466
+ if (minutes < 60) return `${minutes}m`;
467
+ const hours = Math.floor(minutes / 60);
468
+ const remain = minutes % 60;
469
+ if (remain === 0) return `${hours}h`;
470
+ return `${hours}h${remain}m`;
471
+ }
472
+
473
+ export function formatSharedWorkspaceSummary(registry, options = {}) {
474
+ const now = options.now ? new Date(options.now) : new Date();
475
+ const lines = ["Shared Cloud Workspaces"];
476
+ const workspaces = Array.isArray(registry?.workspaces) ? registry.workspaces : [];
477
+ if (workspaces.length === 0) {
478
+ lines.push("No shared workspaces configured.");
479
+ return lines.join("\n");
480
+ }
481
+ for (const workspace of workspaces) {
482
+ const base = `${workspace.id}: ${workspace.name || workspace.id}`;
483
+ const availability = workspace.availability || "available";
484
+ if (workspace.lease) {
485
+ const expiresIn = formatExpiresIn(workspace.lease.lease_expires_at, now);
486
+ lines.push(
487
+ `- ${base} — leased by ${workspace.lease.owner} (expires in ${expiresIn})`,
488
+ );
489
+ continue;
490
+ }
491
+ lines.push(`- ${base} — ${availability}`);
492
+ }
493
+ return lines.join("\n");
494
+ }
495
+
496
+ export function formatSharedWorkspaceDetail(workspace, options = {}) {
497
+ if (!workspace) return "Workspace not found.";
498
+ const now = options.now ? new Date(options.now) : new Date();
499
+ const lines = [`${workspace.id}: ${workspace.name || workspace.id}`];
500
+ lines.push(`provider: ${workspace.provider || "vibe-kanban"}`);
501
+ if (workspace.region) {
502
+ lines.push(`region: ${workspace.region}`);
503
+ }
504
+ lines.push(`availability: ${workspace.availability || "available"}`);
505
+ if (workspace.lease) {
506
+ const lease = workspace.lease;
507
+ lines.push(`lease owner: ${lease.owner}`);
508
+ lines.push(`lease expires: ${lease.lease_expires_at}`);
509
+ lines.push(`lease remaining: ${formatExpiresIn(lease.lease_expires_at, now)}`);
510
+ if (lease.notes) {
511
+ lines.push(`lease notes: ${lease.notes}`);
512
+ }
513
+ }
514
+ return lines.join("\n");
515
+ }
516
+
517
+ export function getSharedAvailabilityMap(registry) {
518
+ const map = new Map();
519
+ const workspaces = Array.isArray(registry?.workspaces)
520
+ ? registry.workspaces
521
+ : [];
522
+ for (const workspace of workspaces) {
523
+ if (!workspace?.id) continue;
524
+ const state = workspace.lease
525
+ ? "leased"
526
+ : workspace.availability || "available";
527
+ map.set(workspace.id, {
528
+ state,
529
+ owner: workspace.lease?.owner || null,
530
+ lease_expires_at: workspace.lease?.lease_expires_at || null,
531
+ });
532
+ }
533
+ return map;
534
+ }
535
+ export function getSharedRegistryTemplate() {
536
+ return JSON.stringify(DEFAULT_REGISTRY, null, 2);
537
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 1,
3
+ "registry_name": "shared-cloud-workspaces",
4
+ "default_lease_ttl_minutes": 120,
5
+ "workspaces": [
6
+ {
7
+ "id": "cloud-01",
8
+ "name": "Cloud Workspace 01",
9
+ "provider": "vibe-kanban",
10
+ "region": "us-west-2",
11
+ "availability": "available",
12
+ "lease_ttl_minutes": 120,
13
+ "metadata": {
14
+ "notes": "Primary shared pool"
15
+ }
16
+ }
17
+ ]
18
+ }