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.
- package/bin/orch +76 -26
- 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
|
-
|
|
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
|
-
|
|
271
|
+
scheduleReconnect(10);
|
|
234
272
|
return;
|
|
235
273
|
}
|
|
236
274
|
}
|
|
237
275
|
|
|
238
|
-
|
|
276
|
+
if (ws) {
|
|
277
|
+
ws.removeAllListeners();
|
|
278
|
+
ws.close();
|
|
279
|
+
}
|
|
280
|
+
const sock = new WebSocket(serverUrl);
|
|
281
|
+
ws = sock;
|
|
239
282
|
|
|
240
|
-
|
|
241
|
-
|
|
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 (
|
|
254
|
-
|
|
297
|
+
if (sock.readyState === WebSocket.OPEN) {
|
|
298
|
+
sock.ping();
|
|
255
299
|
pongTimer = setTimeout(() => {
|
|
256
|
-
|
|
300
|
+
sock.terminate();
|
|
257
301
|
}, PONG_TIMEOUT_MS);
|
|
258
302
|
}
|
|
259
303
|
}, PING_INTERVAL_MS);
|
|
260
304
|
});
|
|
261
305
|
|
|
262
|
-
|
|
306
|
+
sock.on("pong", () => {
|
|
263
307
|
if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
|
|
264
308
|
});
|
|
265
309
|
|
|
266
|
-
|
|
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
|
-
|
|
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 = [
|
|
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 (
|
|
338
|
+
if (lbl) childArgs.push("-l", lbl);
|
|
294
339
|
if (yolo) childArgs.push("-y");
|
|
295
|
-
childArgs.push(
|
|
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 (
|
|
306
|
-
|
|
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 (
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
+
scheduleReconnect(2);
|
|
332
377
|
});
|
|
333
378
|
|
|
334
|
-
|
|
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>
|
|
402
|
-
console.error(" -y, --yolo
|
|
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)");
|