@venturewild/workspace 0.1.7 → 0.1.9
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/package.json +1 -1
- package/server/bin/wild-workspace.mjs +18 -0
- package/server/src/agent.mjs +33 -3
- package/server/src/index.mjs +5 -7
package/package.json
CHANGED
|
@@ -684,6 +684,24 @@ async function main() {
|
|
|
684
684
|
return;
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
// If a workspace server is already serving this port — always-on started it at
|
|
688
|
+
// login, or another `wild-workspace` is running — don't fight it for the socket
|
|
689
|
+
// (createServer would reject on EADDRINUSE and crash). Just open the browser to
|
|
690
|
+
// the one already up. This is the common case now that always-on is real.
|
|
691
|
+
{
|
|
692
|
+
const probeCfg = buildConfig(opts);
|
|
693
|
+
if (await probeHealth(probeCfg.port)) {
|
|
694
|
+
const host = probeCfg.host === '0.0.0.0' ? '127.0.0.1' : probeCfg.host;
|
|
695
|
+
const url = `http://${host}:${probeCfg.port}`;
|
|
696
|
+
console.log(`\n wild-workspace is already running at ${url}`);
|
|
697
|
+
if (probeCfg.openBrowser) {
|
|
698
|
+
console.log(' opening it in your browser…');
|
|
699
|
+
try { const open = (await import('open')).default; await open(url); } catch { /* best-effort */ }
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
687
705
|
const server = await createServer(opts);
|
|
688
706
|
const { config } = server;
|
|
689
707
|
console.log(`\n wild-workspace v${APP_VERSION}`);
|
package/server/src/agent.mjs
CHANGED
|
@@ -19,21 +19,51 @@ import { spawn } from 'node:child_process';
|
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
20
|
import { execFile as execFileCb } from 'node:child_process';
|
|
21
21
|
import { EventEmitter } from 'node:events';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import fs from 'node:fs';
|
|
22
24
|
import { DEFAULT_AGENTS } from './config.mjs';
|
|
23
25
|
|
|
24
26
|
const execFile = promisify(execFileCb);
|
|
25
27
|
|
|
26
28
|
const PATH_LOOKUP_TIMEOUT_MS = 1500;
|
|
27
29
|
|
|
30
|
+
// Where `claude` (and other user-installed CLIs) actually live on macOS/Linux.
|
|
31
|
+
// The native installer drops it in ~/.local/bin; Homebrew uses /opt/homebrew/bin
|
|
32
|
+
// (Apple Silicon) or /usr/local/bin; our no-sudo npm prefix is ~/.npm-global/bin.
|
|
33
|
+
function toolDirs(home = os.homedir()) {
|
|
34
|
+
return [`${home}/.local/bin`, '/opt/homebrew/bin', '/usr/local/bin', `${home}/.npm-global/bin`];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// GUI / launchd-launched processes inherit a MINIMAL PATH that omits all of the
|
|
38
|
+
// above and never reads ~/.zshrc — so a server started by the macOS always-on
|
|
39
|
+
// LaunchAgent can't find `claude` (spawn ENOENT). Idempotently add the real tool
|
|
40
|
+
// dirs so detection + spawn work regardless of how the process was launched.
|
|
41
|
+
export function ensureToolPath(env = process.env, { platform = process.platform, home = os.homedir() } = {}) {
|
|
42
|
+
if (platform === 'win32') return env.PATH; // HKCU\Run inherits the full user PATH
|
|
43
|
+
const have = (env.PATH || '').split(':').filter(Boolean);
|
|
44
|
+
const add = toolDirs(home).filter((d) => !have.includes(d));
|
|
45
|
+
if (add.length) env.PATH = [...have, ...add].join(':');
|
|
46
|
+
return env.PATH;
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
async function isOnPath(binary) {
|
|
50
|
+
ensureToolPath(); // make sure the tool dirs are on PATH before we look
|
|
29
51
|
const probe = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
30
52
|
try {
|
|
31
53
|
const { stdout } = await execFile(probe, [binary], { timeout: PATH_LOOKUP_TIMEOUT_MS });
|
|
32
54
|
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
33
|
-
|
|
34
|
-
} catch {
|
|
35
|
-
|
|
55
|
+
if (lines.length > 0) return lines[0].trim();
|
|
56
|
+
} catch { /* fall through to a direct probe */ }
|
|
57
|
+
// which/where can still miss a freshly-installed binary in a stripped launchd
|
|
58
|
+
// environment — probe the known install dirs directly and return an ABSOLUTE
|
|
59
|
+
// path, which spawn uses verbatim (no PATH needed at spawn time).
|
|
60
|
+
if (process.platform !== 'win32') {
|
|
61
|
+
for (const dir of toolDirs()) {
|
|
62
|
+
const candidate = `${dir}/${binary}`;
|
|
63
|
+
try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch { /* next */ }
|
|
64
|
+
}
|
|
36
65
|
}
|
|
66
|
+
return null;
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
export async function detectAgents(candidates = DEFAULT_AGENTS) {
|
package/server/src/index.mjs
CHANGED
|
@@ -581,18 +581,16 @@ export async function createServer(overrides = {}) {
|
|
|
581
581
|
|
|
582
582
|
// In-app "Sign in to Claude" — drives `claude auth login` in a real PTY so the
|
|
583
583
|
// browser OAuth callback auto-completes and the user never touches a terminal.
|
|
584
|
-
// (See agent-login.mjs.)
|
|
585
|
-
//
|
|
586
|
-
//
|
|
584
|
+
// (See agent-login.mjs.) Claude opens the OAuth URL in the user's browser itself
|
|
585
|
+
// (the server is local), so we do NOT open it again here — doing so spawned a
|
|
586
|
+
// duplicate tab; the UI surfaces the captured URL as a "didn't open?" fallback.
|
|
587
|
+
// Degrades to `{status:'unsupported'}` if node-pty is absent (gate → terminal).
|
|
587
588
|
let _loginSession = null;
|
|
588
|
-
const openUrl = async (u) => {
|
|
589
|
-
try { const open = (await import('open')).default; await open(u); } catch { /* best-effort */ }
|
|
590
|
-
};
|
|
591
589
|
const emptyLoginSnap = { status: 'idle', url: null, error: null, verdict: null };
|
|
592
590
|
app.post('/api/agent/login/start', async (c) => {
|
|
593
591
|
const forbidden = require(c, 'chatWrite');
|
|
594
592
|
if (forbidden) return forbidden;
|
|
595
|
-
if (!_loginSession) _loginSession = new ClaudeLoginSession({ agent: activeAgent
|
|
593
|
+
if (!_loginSession) _loginSession = new ClaudeLoginSession({ agent: activeAgent });
|
|
596
594
|
_loginSession.agent = activeAgent; // track the active agent if it changed
|
|
597
595
|
const snap = await _loginSession.start();
|
|
598
596
|
_readinessCache = null; // a sign-in is about to change auth state — don't serve stale
|