@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 +63 -12
- package/dev-ui/css/app.css +16 -0
- package/dev-ui/index.html +15 -0
- package/dev-ui/js/app.js +7 -1
- package/dev-ui/js/serve.js +77 -1
- package/dev-ui/js/settings.js +4 -1
- package/dev-ui/js/state.js +4 -0
- package/package.json +1 -1
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
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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.
|
|
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,
|
|
2392
|
-
const url =
|
|
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
|
|
2687
|
-
|
|
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:
|
|
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];
|
package/dev-ui/css/app.css
CHANGED
|
@@ -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);
|
package/dev-ui/js/serve.js
CHANGED
|
@@ -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
|
}
|
package/dev-ui/js/settings.js
CHANGED
|
@@ -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)
|
|
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
|
}
|
package/dev-ui/js/state.js
CHANGED