fluxy-bot 0.12.1 → 0.12.3

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": "fluxy-bot",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "releaseNotes": [
5
5
  "Adding a way for users to claim their fluxies on the fluxy.bot dashboard",
6
6
  "2. ",
@@ -97,31 +97,47 @@ export function spawnBackend(port: number): ChildProcess {
97
97
  }
98
98
 
99
99
  /** Stop the backend and wait for the process to fully exit before resolving.
100
- * This prevents port collisions when restarting (old process must release the port first). */
100
+ * This prevents port collisions when restarting (old process must release the port first).
101
+ * Concurrent calls return the same promise to avoid double-spawn races. */
102
+ let stopPromise: Promise<void> | null = null;
103
+
101
104
  export function stopBackend(): Promise<void> {
102
- return new Promise((resolve) => {
103
- if (!child || child.exitCode !== null) {
104
- child = null;
105
- resolve();
106
- return;
107
- }
108
- intentionallyStopped = true;
109
- const dying = child;
105
+ if (stopPromise) return stopPromise;
106
+
107
+ if (!child || child.exitCode !== null) {
110
108
  child = null;
111
- dying.once('exit', () => resolve());
109
+ return Promise.resolve();
110
+ }
111
+
112
+ intentionallyStopped = true;
113
+ const dying = child;
114
+ child = null;
115
+
116
+ stopPromise = new Promise<void>((resolve) => {
117
+ dying.once('exit', () => {
118
+ stopPromise = null;
119
+ resolve();
120
+ });
112
121
  dying.kill();
113
122
  // Safety: force kill after 3s if SIGTERM doesn't work
114
123
  setTimeout(() => {
115
124
  try { dying.kill('SIGKILL'); } catch {}
125
+ stopPromise = null;
116
126
  resolve();
117
127
  }, 3000);
118
128
  });
129
+
130
+ return stopPromise;
119
131
  }
120
132
 
121
133
  export function isBackendAlive(): boolean {
122
134
  return child !== null && child.exitCode === null;
123
135
  }
124
136
 
137
+ export function isBackendStopping(): boolean {
138
+ return stopPromise !== null;
139
+ }
140
+
125
141
  export function resetBackendRestarts(): void {
126
142
  restarts = 0;
127
143
  }
@@ -11,7 +11,7 @@ import { log } from '../shared/logger.js';
11
11
  import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel, startNamedTunnel, restartNamedTunnel } from './tunnel.js';
12
12
  import { createWorkerApp } from '../worker/index.js';
13
13
  import { closeDb, getSession, getSetting } from '../worker/db.js';
14
- import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
14
+ import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, isBackendStopping, resetBackendRestarts } from './backend.js';
15
15
  import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
16
16
  import { startFluxyAgentQuery, stopFluxyAgentQuery, type RecentMessage } from './fluxy-agent.js';
17
17
  import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
@@ -341,7 +341,7 @@ export async function startSupervisor() {
341
341
 
342
342
  // App API routes → proxy to user's backend server
343
343
  if (req.url?.startsWith('/app/api')) {
344
- const backendPath = req.url.replace(/^\/app\/api/, '') || '/';
344
+ const backendPath = req.url.replace(/^\/app/, '');
345
345
  console.log(`[supervisor] → backend :${backendPort} | ${req.method} ${backendPath}`);
346
346
  if (!isBackendAlive()) {
347
347
  console.log('[supervisor] Backend down — returning 503');
@@ -473,7 +473,7 @@ export async function startSupervisor() {
473
473
  if (msg.type !== 'app:api' || !msg.data) return;
474
474
 
475
475
  const { id, method, path: reqPath, headers: reqHeaders, body } = msg.data;
476
- const backendPath = (reqPath || '').replace(/^\/app\/api/, '') || '/';
476
+ const backendPath = (reqPath || '').replace(/^\/app/, '');
477
477
 
478
478
  console.log(`[supervisor] App WS → backend :${backendPort} | ${method} ${backendPath} (${id})`);
479
479
 
@@ -860,6 +860,7 @@ export async function startSupervisor() {
860
860
  if (eventData.usedFileTools || pendingBackendRestart) {
861
861
  console.log('[supervisor] Agent turn ended — restarting backend');
862
862
  pendingBackendRestart = false;
863
+ if (backendRestartTimer) { clearTimeout(backendRestartTimer); backendRestartTimer = null; }
863
864
  resetBackendRestarts();
864
865
  stopBackend().then(() => spawnBackend(backendPort));
865
866
  }
@@ -1058,8 +1059,11 @@ export async function startSupervisor() {
1058
1059
  pendingBackendRestart = true;
1059
1060
  return;
1060
1061
  }
1062
+ // Skip if a stop/restart is already in progress (bot:done handler owns the restart)
1063
+ if (isBackendStopping()) return;
1061
1064
  if (backendRestartTimer) clearTimeout(backendRestartTimer);
1062
1065
  backendRestartTimer = setTimeout(async () => {
1066
+ if (isBackendStopping()) return; // re-check after delay
1063
1067
  log.info(`[watcher] ${reason} — restarting backend...`);
1064
1068
  resetBackendRestarts();
1065
1069
  await stopBackend();
package/vite.config.ts CHANGED
@@ -25,7 +25,7 @@ export default defineConfig({
25
25
  proxy: {
26
26
  '/app/api': {
27
27
  target: 'http://localhost:3004',
28
- rewrite: (path) => path.replace(/^\/app\/api/, '') || '/',
28
+ rewrite: (path) => path.replace(/^\/app/, ''),
29
29
  },
30
30
  '/api': 'http://localhost:3000',
31
31
  },
@@ -260,16 +260,16 @@ Your working directory is the `workspace/` folder. This is your full-stack works
260
260
 
261
261
  ## Backend Routing (Critical)
262
262
 
263
- A supervisor process sits in front of everything on port 3000. It strips the `/app/api` prefix before forwarding to the backend.
263
+ A supervisor process sits in front of everything on port 3000. It strips the `/app` prefix before forwarding to the backend, preserving the `/api/` path.
264
264
 
265
265
  ```
266
- Browser: GET /app/api/tasks → Supervisor strips prefix → Backend receives: GET /tasks
266
+ Browser: GET /app/api/tasks → Supervisor strips /app → Backend receives: GET /api/tasks
267
267
  ```
268
268
 
269
269
  **The rules:**
270
270
  - **Frontend** fetch calls: use `/app/api/...`
271
- - **Backend** Express routes: register as `/tasks`, `/health` — NO `/app/api` prefix
272
- - No exceptions
271
+ - **Backend** Express routes: register as `/api/tasks`, `/api/health` — standard Express convention with `/api/` prefix
272
+ - The `/app` prefix is what distinguishes workspace backend routes from system routes
273
273
 
274
274
  **Tunnel reliability:** All `/app/api/*` fetch calls from the dashboard are automatically proxied through WebSocket when available. This is transparent — just use `fetch('/app/api/...')` normally. The WebSocket proxy activates automatically and falls back to regular HTTP if unavailable. You don't need to handle this in your code.
275
275
 
@@ -367,6 +367,7 @@ There are TWO completely separate password systems in Fluxy. Understanding the d
367
367
  POST /app/api/workspace/set-password
368
368
  Body: { "password": "the_password" }
369
369
  ```
370
+ (The backend route is `/api/workspace/set-password` — the supervisor strips `/app` from the frontend URL.)
370
371
 
371
372
  **How to remove it:**
372
373
  ```
@@ -28,7 +28,7 @@ const app = express();
28
28
  app.use(express.json());
29
29
 
30
30
  // Health check
31
- app.get('/health', (_req, res) => {
31
+ app.get('/api/health', (_req, res) => {
32
32
  res.json({ status: 'ok' });
33
33
  });
34
34