@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venturewild/workspace",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Claude Code Web — Replit/Lovable-style chat-first browser UI that wraps the AI agent already installed on your machine.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -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}`);
@@ -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
- return lines.length > 0 ? lines[0].trim() : null;
34
- } catch {
35
- return null;
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) {
@@ -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.) Degrades to `{status:'unsupported'}` if node-pty is
585
- // absent, and the gate falls back to the terminal instruction. The OAuth URL is
586
- // opened on this machine the server runs locally, so it's the user's browser.
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, openImpl: openUrl });
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