@vendian/cli 0.0.37 → 0.0.39

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/cli-wrapper.mjs CHANGED
@@ -494,17 +494,35 @@ function spawnForward(command, args, options = {}) {
494
494
 
495
495
  // src/python.js
496
496
  import fs2 from "node:fs";
497
- function findPython(platform = process.platform) {
498
- const candidates = platform === "win32" ? [
499
- { command: "py", args: ["-3.11"] },
500
- { command: "python", args: [] },
501
- { command: "python3", args: [] }
502
- ] : [
497
+ function pythonCandidates(platform = process.platform) {
498
+ if (platform === "win32") {
499
+ return [
500
+ { command: "py", args: ["-3.11"] },
501
+ { command: "python", args: [] },
502
+ { command: "python3", args: [] }
503
+ ];
504
+ }
505
+ if (platform === "darwin") {
506
+ return [
507
+ { command: "/opt/homebrew/bin/python3.12", args: [] },
508
+ { command: "/opt/homebrew/bin/python3.11", args: [] },
509
+ { command: "/usr/local/bin/python3.12", args: [] },
510
+ { command: "/usr/local/bin/python3.11", args: [] },
511
+ { command: "python3.12", args: [] },
512
+ { command: "python3.11", args: [] },
513
+ { command: "python3", args: [] },
514
+ { command: "python", args: [] }
515
+ ];
516
+ }
517
+ return [
503
518
  { command: "python3.12", args: [] },
504
519
  { command: "python3.11", args: [] },
505
520
  { command: "python3", args: [] },
506
521
  { command: "python", args: [] }
507
522
  ];
523
+ }
524
+ function findPython(platform = process.platform) {
525
+ const candidates = pythonCandidates(platform);
508
526
  for (const candidate of candidates) {
509
527
  const version = runCapture(candidate.command, [
510
528
  ...candidate.args,
@@ -2363,16 +2381,18 @@ function buildLocalServeEventStreamArgs({ agentsDir: agentsDir2 = "./agents", co
2363
2381
  }
2364
2382
 
2365
2383
  // src/version.js
2366
- var CLI_VERSION = true ? "0.0.37" : process.env.npm_package_version || "0.0.0-dev";
2384
+ var CLI_VERSION = true ? "0.0.39" : process.env.npm_package_version || "0.0.0-dev";
2367
2385
 
2368
2386
  // src/dev-server.js
2369
2387
  var __dirname = path8.dirname(fileURLToPath(import.meta.url));
2388
+ var DEV_UI_HOST = "127.0.0.1";
2370
2389
  var UI_DIR = fs11.existsSync(path8.join(__dirname, "dev-ui")) ? path8.join(__dirname, "dev-ui") : fs11.existsSync(path8.join(__dirname, "src", "dev-ui")) ? path8.join(__dirname, "src", "dev-ui") : path8.join(__dirname, "dev-ui");
2371
2390
  var serveChild = null;
2372
2391
  var serveState = initialServeState();
2373
2392
  var serveLogs = [];
2374
2393
  var logStore = null;
2375
2394
  var agentsDir = "";
2395
+ var activeServeAgentsDir = "";
2376
2396
  var collectionId = "";
2377
2397
  async function startDevServer({
2378
2398
  port = 3859,
@@ -2388,8 +2408,8 @@ async function startDevServer({
2388
2408
  agentsDir = candidates[0]?.absolutePath || path8.resolve("./agents");
2389
2409
  }
2390
2410
  const server = http2.createServer(handleRequest);
2391
- server.listen(port, "127.0.0.1", () => {
2392
- const url = `http://localhost:${port}`;
2411
+ server.listen(port, DEV_UI_HOST, () => {
2412
+ const url = devUiUrl(port);
2393
2413
  console.log("");
2394
2414
  console.log(" \x1B[1;36m\u26A1 Vendian Dev UI\x1B[0m");
2395
2415
  console.log(` \x1B[90m${"\u2500".repeat(44)}\x1B[0m`);
@@ -2514,6 +2534,7 @@ function apiStatus(req, res) {
2514
2534
  jsonResponse(res, {
2515
2535
  serving,
2516
2536
  agentsDir,
2537
+ serveAgentsDir: serving ? activeServeAgentsDir || agentsDir : "",
2517
2538
  collectionId,
2518
2539
  connected: serveState.connected,
2519
2540
  activity: serveState.activity,
@@ -2574,6 +2595,8 @@ function apiServeState(req, res) {
2574
2595
  activity.sort((a, b) => (a.timestamp || "").localeCompare(b.timestamp || ""));
2575
2596
  jsonResponse(res, {
2576
2597
  serving,
2598
+ agentsDir,
2599
+ serveAgentsDir: serving ? activeServeAgentsDir || agentsDir : "",
2577
2600
  connected: serveState.connected,
2578
2601
  stopped: serveState.stopped,
2579
2602
  activity: serveState.activity,
@@ -2679,20 +2702,43 @@ async function apiValidate(req, res, body) {
2679
2702
  });
2680
2703
  }
2681
2704
  }
2705
+ function resolveServeStartTarget(body = {}, current = {}) {
2706
+ const targetDir = body.agentsDir || current.agentsDir || "";
2707
+ const targetCollection = body.collectionId || current.collectionId || "";
2708
+ if (!targetCollection) {
2709
+ return { ok: false, error: "Choose a workspace before starting local serve." };
2710
+ }
2711
+ return { ok: true, agentsDir: targetDir, collectionId: targetCollection };
2712
+ }
2713
+ function devServerStateAfterServeStart(current = {}, target = {}) {
2714
+ return {
2715
+ agentsDir: current.agentsDir || "",
2716
+ activeServeAgentsDir: target.agentsDir || current.agentsDir || "",
2717
+ collectionId: target.collectionId || current.collectionId || ""
2718
+ };
2719
+ }
2682
2720
  async function apiServeStart(req, res, body) {
2683
2721
  if (serveChild && !serveChild.killed) {
2684
2722
  return jsonResponse(res, { ok: false, error: "Already serving" });
2685
2723
  }
2686
- const targetDir = body.agentsDir || agentsDir;
2687
- const targetCollection = body.collectionId || collectionId;
2724
+ const target = resolveServeStartTarget(body, { agentsDir, collectionId });
2725
+ if (!target.ok) {
2726
+ return jsonResponse(res, target, 400);
2727
+ }
2688
2728
  try {
2729
+ const targetDir = target.agentsDir;
2730
+ const targetCollection = target.collectionId;
2689
2731
  const invocation = await preparePythonVendianInvocation(
2690
2732
  buildLocalServeEventStreamArgs({ agentsDir: targetDir, collectionId: targetCollection }),
2691
2733
  { onProgress: null }
2692
2734
  );
2735
+ const nextState = devServerStateAfterServeStart({ agentsDir, collectionId }, target);
2736
+ agentsDir = nextState.agentsDir;
2737
+ activeServeAgentsDir = nextState.activeServeAgentsDir;
2738
+ collectionId = nextState.collectionId;
2693
2739
  serveState = initialServeState();
2694
2740
  serveLogs = [];
2695
- logStore = createServeLogStore({ agentsDir: targetDir, collectionId: targetCollection });
2741
+ logStore = createServeLogStore({ agentsDir: activeServeAgentsDir, collectionId });
2696
2742
  serveChild = spawn3(invocation.command, invocation.args, {
2697
2743
  env: invocation.env,
2698
2744
  stdio: ["ignore", "pipe", "pipe"],
@@ -2732,6 +2778,7 @@ async function apiServeStart(req, res, body) {
2732
2778
  serveState = { ...serveState, stopped: true, connected: false };
2733
2779
  serveLogs.push(`[process] Exited code=${code} signal=${signal || "none"}`);
2734
2780
  serveChild = null;
2781
+ activeServeAgentsDir = "";
2735
2782
  try {
2736
2783
  logStore?.compact();
2737
2784
  } catch {
@@ -2744,6 +2791,7 @@ async function apiServeStart(req, res, body) {
2744
2791
  }
2745
2792
  function apiServeStop(req, res) {
2746
2793
  if (!serveChild || serveChild.killed) {
2794
+ activeServeAgentsDir = "";
2747
2795
  return jsonResponse(res, { ok: true, wasRunning: false });
2748
2796
  }
2749
2797
  serveChild.kill("SIGTERM");
@@ -2913,6 +2961,9 @@ function formatDaemonEvent(event) {
2913
2961
  return event.type;
2914
2962
  }
2915
2963
  }
2964
+ function devUiUrl(port, host = DEV_UI_HOST) {
2965
+ return `http://${host}:${port}`;
2966
+ }
2916
2967
  function openUrl(url) {
2917
2968
  const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
2918
2969
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -148,10 +148,26 @@ body { font-family: var(--font); background: var(--bg-page); color: var(--text);
148
148
  .serve-controls { display: flex; align-items: center; gap: 12px; }
149
149
  .serve-status { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 500; padding: 7px 14px; border-radius: 7px; background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary); }
150
150
  .serve-status .dot { width: 9px; height: 9px; border-radius: 50%; }
151
+ .serve-section-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 10px; }
152
+ .serve-section-title { font-size: 11px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
153
+ .serve-section-desc { font-size: 12.5px; color: var(--text-secondary); margin-top: 3px; }
154
+ .serve-section-meta { font-size: 11.5px; color: var(--text-dim); white-space: nowrap; padding-top: 1px; }
155
+ #workspace-picker { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; margin-bottom: 6px; }
151
156
  .serve-select-bar { display: flex; align-items: center; gap: 9px; padding: 10px 14px; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border); box-shadow: var(--shadow); margin-bottom: 14px; font-size: 13px; font-weight: 600; cursor: pointer; user-select: none; }
152
157
  .serve-select-bar:hover { background: var(--bg-hover); }
153
158
  .serve-select-bar input { width: 16px; height: 16px; accent-color: var(--accent-solid); cursor: pointer; }
154
159
  .serve-select-bar .count { margin-left: auto; font-weight: 400; color: var(--text-dim); font-size: 12px; }
160
+ .workspace-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; text-align: left; cursor: pointer; transition: all var(--transition); box-shadow: var(--shadow); font: inherit; color: inherit; }
161
+ .workspace-card:hover { border-color: var(--border-hover); transform: translateY(-1px); box-shadow: var(--shadow-lg); }
162
+ .workspace-card.selected { border-color: var(--accent); background: var(--accent-bg); }
163
+ .workspace-card-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
164
+ .workspace-card-title { font-size: 13.5px; font-weight: 650; color: var(--text); }
165
+ .workspace-card-slug { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
166
+ .workspace-card-id { font-size: 11px; font-family: var(--mono); color: var(--text-dim); margin-top: 10px; overflow: hidden; text-overflow: ellipsis; }
167
+ .workspace-card-badge { font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 4px; border: 1px solid var(--border); color: var(--text-dim); background: var(--bg-elevated); white-space: nowrap; }
168
+ .workspace-card.selected .workspace-card-badge { color: var(--accent); border-color: var(--accent-border); background: white; }
169
+ .workspace-empty { padding: 14px 16px; border-radius: var(--radius); background: var(--bg-card); border: 1px dashed var(--border); color: var(--text-secondary); font-size: 12.5px; }
170
+ .workspace-empty.workspace-error { color: var(--red); border-color: var(--red-border); background: var(--red-bg); }
155
171
  .serve-folder { margin-bottom: 20px; }
156
172
  .serve-folder-title { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 10.5px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
157
173
  .serve-folder-title svg { width: 12px; height: 12px; opacity: 0.5; }
package/dev-ui/index.html CHANGED
@@ -94,6 +94,21 @@
94
94
  </div>
95
95
 
96
96
  <div id="serve-picker-section">
97
+ <div class="serve-section-head">
98
+ <div>
99
+ <div class="serve-section-title">Workspace</div>
100
+ <div class="serve-section-desc">Choose the Vendian workspace that should own this daemon and its local runs.</div>
101
+ </div>
102
+ <div class="serve-section-meta" id="workspace-summary">Loading workspaces…</div>
103
+ </div>
104
+ <div id="workspace-picker"></div>
105
+
106
+ <div class="serve-section-head" style="margin-top:20px">
107
+ <div>
108
+ <div class="serve-section-title">Agents</div>
109
+ <div class="serve-section-desc">Pick which local agents this serve session should expose.</div>
110
+ </div>
111
+ </div>
97
112
  <div class="serve-select-bar" id="serve-select-bar">
98
113
  <input type="checkbox" id="cb-all" checked>
99
114
  <label for="cb-all" style="cursor:pointer">Select all</label>
package/dev-ui/js/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { state, api } from './state.js';
2
2
  import { renderAgents } from './agents.js';
3
3
  import { populateValidateGrid, runValidateAll, triggerValidateAgent } from './validate.js';
4
- import { populateServePicker, toggleAll, startServe, stopServe, setServing, clearLogs } from './serve.js';
4
+ import { populateServePicker, toggleAll, startServe, stopServe, setServing, clearLogs, loadWorkspaces } from './serve.js';
5
5
  import { loadAuth } from './settings.js';
6
6
 
7
7
  // ─── Navigation ───
@@ -20,6 +20,10 @@ window.addEventListener('navigate', (e) => {
20
20
  if (page === 'validate' && path) triggerValidateAgent(path);
21
21
  });
22
22
 
23
+ window.addEventListener('auth-changed', () => {
24
+ loadWorkspaces();
25
+ });
26
+
23
27
  // ─── Load agents ───
24
28
  async function loadAgents() {
25
29
  try {
@@ -92,6 +96,8 @@ document.getElementById('serve-select-bar').addEventListener('click', (e) => {
92
96
  // ─── Init ───
93
97
  loadAgents();
94
98
  loadAuth();
99
+ loadWorkspaces();
95
100
  checkStatus();
96
101
  setInterval(checkStatus, 4000);
97
102
  setInterval(loadAgents, 8000);
103
+ setInterval(loadWorkspaces, 15000);
@@ -1,5 +1,80 @@
1
1
  import { state, api, esc, groupByFolder, formatTime } from './state.js';
2
2
 
3
+ export async function loadWorkspaces() {
4
+ state.workspacesLoading = true;
5
+ state.workspacesError = '';
6
+ renderWorkspacePicker();
7
+ try {
8
+ const result = await api('/workspaces');
9
+ state.workspaces = Array.isArray(result.workspaces) ? result.workspaces : [];
10
+ const hasSelected = state.workspaces.some((workspace) => workspace.id === state.selectedWorkspaceId);
11
+ if (!hasSelected) {
12
+ state.selectedWorkspaceId = state.workspaces[0]?.id || '';
13
+ }
14
+ if (!result.ok) {
15
+ state.workspacesError = result.error || 'Could not load workspaces.';
16
+ }
17
+ } catch (error) {
18
+ state.workspaces = [];
19
+ state.selectedWorkspaceId = '';
20
+ state.workspacesError = error?.message || 'Could not load workspaces.';
21
+ } finally {
22
+ state.workspacesLoading = false;
23
+ renderWorkspacePicker();
24
+ }
25
+ }
26
+
27
+ export function renderWorkspacePicker() {
28
+ const container = document.getElementById('workspace-picker');
29
+ const summary = document.getElementById('workspace-summary');
30
+ if (!container || !summary) return;
31
+
32
+ if (state.workspacesLoading) {
33
+ summary.textContent = 'Loading workspaces…';
34
+ container.innerHTML = '<div class="workspace-empty">Loading workspaces from Vendian…</div>';
35
+ return;
36
+ }
37
+
38
+ if (state.workspacesError) {
39
+ summary.textContent = 'Workspace required';
40
+ container.innerHTML = `<div class="workspace-empty workspace-error">${esc(state.workspacesError)}</div>`;
41
+ return;
42
+ }
43
+
44
+ if (!state.workspaces.length) {
45
+ summary.textContent = 'No workspace available';
46
+ container.innerHTML = '<div class="workspace-empty">Sign in to a Vendian environment to load workspaces.</div>';
47
+ return;
48
+ }
49
+
50
+ const selected = state.workspaces.find((workspace) => workspace.id === state.selectedWorkspaceId) || state.workspaces[0];
51
+ if (selected && state.selectedWorkspaceId !== selected.id) {
52
+ state.selectedWorkspaceId = selected.id || '';
53
+ }
54
+ summary.textContent = `${state.workspaces.length} workspace${state.workspaces.length === 1 ? '' : 's'}`;
55
+ container.innerHTML = state.workspaces.map((workspace) => {
56
+ const selectedClass = workspace.id === state.selectedWorkspaceId ? ' selected' : '';
57
+ const slug = workspace.slug && workspace.slug !== workspace.name ? `<div class="workspace-card-slug">${esc(workspace.slug)}</div>` : '';
58
+ return `<button type="button" class="workspace-card${selectedClass}" data-workspace-id="${esc(workspace.id)}">
59
+ <div class="workspace-card-title-row">
60
+ <div>
61
+ <div class="workspace-card-title">${esc(workspace.name || workspace.id)}</div>
62
+ ${slug}
63
+ </div>
64
+ <div class="workspace-card-badge">${workspace.id === state.selectedWorkspaceId ? 'Selected' : 'Choose'}</div>
65
+ </div>
66
+ <div class="workspace-card-id">${esc(workspace.id)}</div>
67
+ </button>`;
68
+ }).join('');
69
+
70
+ container.querySelectorAll('[data-workspace-id]').forEach((button) => {
71
+ button.addEventListener('click', () => {
72
+ state.selectedWorkspaceId = button.dataset.workspaceId || '';
73
+ renderWorkspacePicker();
74
+ });
75
+ });
76
+ }
77
+
3
78
  // ─── Picker ───
4
79
  let userSelections = null; // null = all selected (default), Set = explicit selections
5
80
 
@@ -85,8 +160,9 @@ export function getSelectedPaths() {
85
160
  // ─── Controls ───
86
161
  export async function startServe() {
87
162
  const selected = getSelectedPaths();
163
+ if (!state.selectedWorkspaceId) { alert('Choose a workspace before starting local serve.'); return; }
88
164
  if (!selected.length) { alert('Select at least one agent.'); return; }
89
- const body = {};
165
+ const body = { collectionId: state.selectedWorkspaceId };
90
166
  if (selected.length < state.agents.length) {
91
167
  body.agentsDir = commonParent(selected);
92
168
  }
@@ -78,7 +78,10 @@ function promptLogin(key) {
78
78
 
79
79
  async function switchBackend(key) {
80
80
  const d = await api('/auth/switch', { method: 'POST', body: JSON.stringify({ backend: key }) });
81
- if (d.ok) await loadAuth();
81
+ if (d.ok) {
82
+ await loadAuth();
83
+ window.dispatchEvent(new Event('auth-changed'));
84
+ }
82
85
  else if (d.needsLogin) promptLogin(key);
83
86
  else alert(d.error || 'Switch failed');
84
87
  }
@@ -2,6 +2,10 @@
2
2
  export const state = {
3
3
  agents: [],
4
4
  authData: {},
5
+ workspaces: [],
6
+ selectedWorkspaceId: '',
7
+ workspacesLoading: false,
8
+ workspacesError: '',
5
9
  logOffset: 0,
6
10
  pollTimer: null,
7
11
  isServing: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vendian/cli",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "description": "Public Vendian CLI bootstrapper and launcher",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,