orchestrating 0.1.25 → 0.1.27

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.
Files changed (2) hide show
  1. package/bin/orch +76 -26
  2. package/package.json +1 -1
package/bin/orch CHANGED
@@ -179,12 +179,20 @@ if (firstArg === "logout") {
179
179
  }
180
180
 
181
181
  if (firstArg === "daemon") {
182
- await handleDaemon();
182
+ // Parse daemon flags: orch daemon [--projects <path>]
183
+ const daemonArgs = process.argv.slice(3);
184
+ let projectsDir = null;
185
+ for (let di = 0; di < daemonArgs.length; di++) {
186
+ if ((daemonArgs[di] === "--projects" || daemonArgs[di] === "-d") && daemonArgs[di + 1]) {
187
+ projectsDir = daemonArgs[++di];
188
+ }
189
+ }
190
+ await handleDaemon(projectsDir);
183
191
  // handleDaemon runs forever (or exits on fatal error)
184
192
  }
185
193
 
186
194
  // --- Daemon mode ---
187
- async function handleDaemon() {
195
+ async function handleDaemon(projectsDir) {
188
196
  const DIM = "\x1b[2m";
189
197
  const GREEN = "\x1b[32m";
190
198
  const RED = "\x1b[31m";
@@ -218,9 +226,39 @@ async function handleDaemon() {
218
226
  let pongTimer = null;
219
227
  let reconnectTimer = null;
220
228
 
229
+ // Scan project directories
230
+ const homeDir = os.homedir();
231
+ const scanRoot = projectsDir || path.join(homeDir, "projects");
232
+ let directories = [];
233
+ try {
234
+ const { readdirSync, statSync } = await import("fs");
235
+ const entries = readdirSync(scanRoot);
236
+ directories = entries
237
+ .filter((e) => !e.startsWith("."))
238
+ .map((e) => path.join(scanRoot, e))
239
+ .filter((p) => { try { return statSync(p).isDirectory(); } catch { return false; } })
240
+ .sort();
241
+ } catch {
242
+ // scanRoot doesn't exist — fall back to home dir
243
+ directories = [homeDir];
244
+ }
245
+
221
246
  process.stderr.write(`${GREEN}${PREFIX} Listening for remote sessions as "${hostname}"${RESET}\n`);
247
+ process.stderr.write(`${DIM}${PREFIX} Projects: ${scanRoot} (${directories.length} dirs)${RESET}\n`);
222
248
  process.stderr.write(`${DIM}${PREFIX} Tip: Use 'nohup orch daemon &' to run in background${RESET}\n`);
223
249
 
250
+ let reconnecting = false;
251
+
252
+ function scheduleReconnect(delaySec) {
253
+ if (reconnecting) return;
254
+ reconnecting = true;
255
+ if (reconnectTimer) clearTimeout(reconnectTimer);
256
+ reconnectTimer = setTimeout(() => {
257
+ reconnecting = false;
258
+ connect();
259
+ }, delaySec * 1000);
260
+ }
261
+
224
262
  async function connect() {
225
263
  // Refresh token if needed before connecting
226
264
  if (isTokenExpired()) {
@@ -230,19 +268,25 @@ async function handleDaemon() {
230
268
  process.stderr.write(`${DIM}${PREFIX} Token refreshed${RESET}\n`);
231
269
  } else {
232
270
  process.stderr.write(`${RED}${PREFIX} Token refresh failed — retrying in 10s${RESET}\n`);
233
- reconnectTimer = setTimeout(connect, 10_000);
271
+ scheduleReconnect(10);
234
272
  return;
235
273
  }
236
274
  }
237
275
 
238
- ws = new WebSocket(serverUrl);
276
+ if (ws) {
277
+ ws.removeAllListeners();
278
+ ws.close();
279
+ }
280
+ const sock = new WebSocket(serverUrl);
281
+ ws = sock;
239
282
 
240
- ws.on("open", () => {
241
- ws.send(JSON.stringify({
283
+ sock.on("open", () => {
284
+ sock.send(JSON.stringify({
242
285
  type: "register_daemon",
243
286
  token,
244
287
  hostname,
245
288
  cliVersion,
289
+ directories,
246
290
  }));
247
291
  process.stderr.write(`${GREEN}${PREFIX} Connected${RESET}\n`);
248
292
 
@@ -250,30 +294,31 @@ async function handleDaemon() {
250
294
  if (pingTimer) clearInterval(pingTimer);
251
295
  if (pongTimer) clearTimeout(pongTimer);
252
296
  pingTimer = setInterval(() => {
253
- if (ws && ws.readyState === WebSocket.OPEN) {
254
- ws.ping();
297
+ if (sock.readyState === WebSocket.OPEN) {
298
+ sock.ping();
255
299
  pongTimer = setTimeout(() => {
256
- if (ws) ws.terminate();
300
+ sock.terminate();
257
301
  }, PONG_TIMEOUT_MS);
258
302
  }
259
303
  }, PING_INTERVAL_MS);
260
304
  });
261
305
 
262
- ws.on("pong", () => {
306
+ sock.on("pong", () => {
263
307
  if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
264
308
  });
265
309
 
266
- ws.on("message", (raw) => {
310
+ sock.on("message", (raw) => {
267
311
  let msg;
268
312
  try { msg = JSON.parse(raw.toString()); } catch { return; }
269
313
 
270
314
  if (msg.type === "error") {
271
315
  process.stderr.write(`${RED}${PREFIX} Server: ${msg.error}${RESET}\n`);
272
316
  if (/unauthorized|auth|token/i.test(msg.error || "")) {
317
+ // Don't reconnect from here — let the close handler do it after refreshing
273
318
  refreshAuthToken().then((refreshed) => {
274
319
  if (refreshed) {
275
320
  token = refreshed;
276
- connect();
321
+ process.stderr.write(`${DIM}${PREFIX} Token refreshed — will reconnect${RESET}\n`);
277
322
  } else {
278
323
  process.stderr.write(`${RED}${PREFIX} Auth failed. Run 'orch login'.${RESET}\n`);
279
324
  process.exit(1);
@@ -284,15 +329,15 @@ async function handleDaemon() {
284
329
  }
285
330
 
286
331
  if (msg.type === "start_session") {
287
- const { requestId, command, args, cwd, label, yolo } = msg;
288
- const cmdStr = [command, ...args].join(" ");
332
+ const { requestId, command: cmd, args: cmdArgs, cwd, label: lbl, yolo } = msg;
333
+ const cmdStr = [cmd, ...cmdArgs].join(" ");
289
334
  process.stderr.write(`${GREEN}${PREFIX} Spawning: ${cmdStr}${RESET}\n`);
290
335
 
291
336
  try {
292
337
  const childArgs = [];
293
- if (label) childArgs.push("-l", label);
338
+ if (lbl) childArgs.push("-l", lbl);
294
339
  if (yolo) childArgs.push("-y");
295
- childArgs.push(command, ...args);
340
+ childArgs.push(cmd, ...cmdArgs);
296
341
 
297
342
  const child = spawn(process.execPath, [orchPath, ...childArgs], {
298
343
  stdio: "ignore",
@@ -302,8 +347,8 @@ async function handleDaemon() {
302
347
  });
303
348
  child.unref();
304
349
 
305
- if (ws && ws.readyState === WebSocket.OPEN) {
306
- ws.send(JSON.stringify({
350
+ if (sock.readyState === WebSocket.OPEN) {
351
+ sock.send(JSON.stringify({
307
352
  type: "session_started",
308
353
  requestId,
309
354
  success: true,
@@ -311,8 +356,8 @@ async function handleDaemon() {
311
356
  }
312
357
  } catch (err) {
313
358
  process.stderr.write(`${RED}${PREFIX} Spawn failed: ${err.message}${RESET}\n`);
314
- if (ws && ws.readyState === WebSocket.OPEN) {
315
- ws.send(JSON.stringify({
359
+ if (sock.readyState === WebSocket.OPEN) {
360
+ sock.send(JSON.stringify({
316
361
  type: "session_started",
317
362
  requestId,
318
363
  success: false,
@@ -323,15 +368,15 @@ async function handleDaemon() {
323
368
  }
324
369
  });
325
370
 
326
- ws.on("close", () => {
327
- ws = null;
371
+ sock.on("close", () => {
372
+ if (ws === sock) ws = null;
328
373
  if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
329
374
  if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
330
375
  process.stderr.write(`${DIM}${PREFIX} Disconnected — reconnecting in 2s${RESET}\n`);
331
- reconnectTimer = setTimeout(connect, 2000);
376
+ scheduleReconnect(2);
332
377
  });
333
378
 
334
- ws.on("error", () => {
379
+ sock.on("error", () => {
335
380
  // Will trigger close
336
381
  });
337
382
  }
@@ -398,8 +443,12 @@ while (i < args.length) {
398
443
  console.error(" orch logout — Clear stored credentials");
399
444
  console.error(" orch daemon — Run background daemon for remote session launching");
400
445
  console.error("");
401
- console.error(" -l <label> Optional human-readable session label");
402
- console.error(" -y, --yolo Skip all permission prompts (auto-approve everything)");
446
+ console.error(" -l <label> Optional human-readable session label");
447
+ console.error(" -y, --yolo Skip all permission prompts (auto-approve everything)");
448
+ console.error("");
449
+ console.error("Daemon options:");
450
+ console.error(" --projects <path> Directory to scan for projects (default: ~/projects)");
451
+ console.error(" -d <path> Short alias for --projects");
403
452
  console.error("");
404
453
  console.error("Examples:");
405
454
  console.error(' orch claude "refactor auth"');
@@ -407,6 +456,7 @@ while (i < args.length) {
407
456
  console.error(' orch -l "deploy fix" codex');
408
457
  console.error(" orch bash");
409
458
  console.error(" orch daemon");
459
+ console.error(" orch daemon --projects ~/work");
410
460
  console.error("");
411
461
  console.error("Environment:");
412
462
  console.error(" ORC_URL WebSocket server URL (default: wss://api.orchestrat.ing/ws)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {