orchestrating 0.1.32 → 0.1.34

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 +176 -48
  2. package/package.json +1 -1
package/bin/orch CHANGED
@@ -106,63 +106,127 @@ function isTokenExpired() {
106
106
  return Date.now() / 1000 > auth.expires_at - 300;
107
107
  }
108
108
 
109
+ // --- Login: save auth from a base64-encoded code ---
110
+ function saveAuthFromCode(code) {
111
+ try {
112
+ const decoded = JSON.parse(Buffer.from(code.trim(), "base64").toString());
113
+ if (decoded.access_token) {
114
+ saveAuth({
115
+ access_token: decoded.access_token,
116
+ refresh_token: decoded.refresh_token || "",
117
+ expires_at: decoded.expires_at || 0,
118
+ });
119
+ return true;
120
+ }
121
+ } catch {}
122
+ return false;
123
+ }
124
+
125
+ // --- Login: read a line from stdin ---
126
+ function readLine(prompt) {
127
+ return new Promise((resolve) => {
128
+ process.stdout.write(prompt);
129
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
130
+ rl.once("line", (line) => { rl.close(); resolve(line); });
131
+ });
132
+ }
133
+
109
134
  // --- Login command ---
110
135
  async function handleLogin() {
111
136
  const loginUrl = process.env.ORC_LOGIN_URL || "https://orchestrat.ing/cli-auth";
112
137
 
138
+ // Try to start a local callback server (works when browser is on same machine)
139
+ let resolved = false;
140
+
113
141
  return new Promise((resolve) => {
114
- const server = http.createServer((req, res) => {
115
- const url = new URL(req.url, `http://localhost`);
116
- if (url.pathname === "/callback") {
117
- const accessToken = url.searchParams.get("access_token");
118
- const refreshToken = url.searchParams.get("refresh_token");
119
- const expiresAt = url.searchParams.get("expires_at");
120
-
121
- if (accessToken) {
122
- saveAuth({
123
- access_token: accessToken,
124
- refresh_token: refreshToken || "",
125
- expires_at: expiresAt ? Number(expiresAt) : 0,
126
- });
127
- res.writeHead(200, { "Content-Type": "text/html" });
128
- res.end('<html><head><meta http-equiv="refresh" content="0;url=https://app.orchestrat.ing"></head><body><p>Redirecting to dashboard...</p></body></html>');
129
- console.log("\x1b[32mLogged in successfully.\x1b[0m");
142
+ let server;
143
+ let codePromise;
144
+
145
+ function done() {
146
+ if (resolved) return;
147
+ resolved = true;
148
+ if (server) try { server.close(); } catch {}
149
+ resolve();
150
+ }
151
+
152
+ try {
153
+ server = http.createServer((req, res) => {
154
+ const url = new URL(req.url, `http://localhost`);
155
+ if (url.pathname === "/callback") {
156
+ const accessToken = url.searchParams.get("access_token");
157
+ const refreshToken = url.searchParams.get("refresh_token");
158
+ const expiresAt = url.searchParams.get("expires_at");
159
+
160
+ if (accessToken) {
161
+ saveAuth({
162
+ access_token: accessToken,
163
+ refresh_token: refreshToken || "",
164
+ expires_at: expiresAt ? Number(expiresAt) : 0,
165
+ });
166
+ res.writeHead(200, { "Content-Type": "text/html" });
167
+ res.end('<html><head><meta http-equiv="refresh" content="0;url=https://app.orchestrat.ing"></head><body><p>Redirecting to dashboard...</p></body></html>');
168
+ console.log("\x1b[32mLogged in successfully.\x1b[0m");
169
+ } else {
170
+ res.writeHead(400, { "Content-Type": "text/html" });
171
+ res.end("<html><body><h2>Authentication failed. Please try again.</h2></body></html>");
172
+ console.error("Authentication failed — no token received.");
173
+ }
174
+ done();
130
175
  } else {
131
- res.writeHead(400, { "Content-Type": "text/html" });
132
- res.end("<html><body><h2>Authentication failed. Please try again.</h2></body></html>");
133
- console.error("Authentication failed — no token received.");
176
+ res.writeHead(404);
177
+ res.end();
134
178
  }
179
+ });
135
180
 
136
- setTimeout(() => { server.close(); resolve(); }, 500);
137
- } else {
138
- res.writeHead(404);
139
- res.end();
140
- }
141
- });
181
+ server.listen(0, "127.0.0.1", () => {
182
+ const port = server.address().port;
183
+ const authUrl = `${loginUrl}?port=${port}`;
142
184
 
143
- server.listen(0, "127.0.0.1", () => {
144
- const port = server.address().port;
145
- const authUrl = `${loginUrl}?port=${port}`;
146
- console.log(`Opening browser for authentication...`);
147
- console.log(`If it doesn't open, visit: ${authUrl}`);
185
+ // Try to open browser
186
+ const openCmd = process.platform === "darwin" ? "open"
187
+ : process.platform === "win32" ? "start"
188
+ : "xdg-open";
189
+ try {
190
+ execSync(`${openCmd} "${authUrl}"`, { stdio: "ignore" });
191
+ console.log(`Opened browser for authentication.`);
192
+ } catch {
193
+ // Browser open failed
194
+ }
148
195
 
149
- // Open browser
150
- const openCmd = process.platform === "darwin" ? "open"
151
- : process.platform === "win32" ? "start"
152
- : "xdg-open";
153
- try {
154
- execSync(`${openCmd} "${authUrl}"`, { stdio: "ignore" });
155
- } catch {
156
- // Browser open failed — user can visit URL manually
157
- }
196
+ console.log(`\nVisit this URL to log in:\n\n ${authUrl}\n`);
197
+ console.log(`Or for headless/VM, visit:\n\n ${loginUrl}\n`);
198
+
199
+ // Also accept pasted code
200
+ codePromise = readLine("Paste auth code here (or wait for browser): ").then((code) => {
201
+ if (resolved) return;
202
+ if (code && saveAuthFromCode(code)) {
203
+ console.log("\x1b[32mLogged in successfully.\x1b[0m");
204
+ done();
205
+ } else if (code) {
206
+ console.error("Invalid code. Try again.");
207
+ }
208
+ });
158
209
 
159
- // Timeout after 2 minutes
160
- setTimeout(() => {
161
- console.error("Login timed out.");
162
- server.close();
163
- resolve();
164
- }, 120_000);
165
- });
210
+ // Timeout after 5 minutes
211
+ setTimeout(() => {
212
+ if (!resolved) {
213
+ console.error("Login timed out.");
214
+ done();
215
+ }
216
+ }, 300_000);
217
+ });
218
+ } catch {
219
+ // Can't start server — code-paste only
220
+ console.log(`Visit this URL to log in:\n\n ${loginUrl}\n`);
221
+ readLine("Paste auth code here: ").then((code) => {
222
+ if (code && saveAuthFromCode(code)) {
223
+ console.log("\x1b[32mLogged in successfully.\x1b[0m");
224
+ } else {
225
+ console.error("Invalid code.");
226
+ }
227
+ done();
228
+ });
229
+ }
166
230
  });
167
231
  }
168
232
 
@@ -179,14 +243,78 @@ if (firstArg === "logout") {
179
243
  }
180
244
 
181
245
  if (firstArg === "daemon") {
182
- // Parse daemon flags: orch daemon [--projects <path>]
183
246
  const daemonArgs = process.argv.slice(3);
247
+ const pidFile = path.join(os.homedir(), ".orch-daemon.pid");
248
+
249
+ // orch daemon --stop
250
+ if (daemonArgs.includes("--stop")) {
251
+ if (existsSync(pidFile)) {
252
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
253
+ try {
254
+ process.kill(pid, "SIGTERM");
255
+ unlinkSync(pidFile);
256
+ console.log(`Daemon stopped (pid ${pid}).`);
257
+ } catch {
258
+ unlinkSync(pidFile);
259
+ console.log("Daemon was not running. Cleaned up pid file.");
260
+ }
261
+ } else {
262
+ console.log("No daemon running.");
263
+ }
264
+ process.exit(0);
265
+ }
266
+
267
+ // orch daemon --status
268
+ if (daemonArgs.includes("--status")) {
269
+ if (existsSync(pidFile)) {
270
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
271
+ try {
272
+ process.kill(pid, 0); // Check if process exists
273
+ console.log(`Daemon running (pid ${pid}).`);
274
+ } catch {
275
+ console.log("Daemon not running (stale pid file).");
276
+ }
277
+ } else {
278
+ console.log("Daemon not running.");
279
+ }
280
+ process.exit(0);
281
+ }
282
+
283
+ // Parse flags
184
284
  let projectsDir = null;
285
+ const background = daemonArgs.includes("--background") || daemonArgs.includes("-b");
185
286
  for (let di = 0; di < daemonArgs.length; di++) {
186
287
  if ((daemonArgs[di] === "--projects" || daemonArgs[di] === "-d") && daemonArgs[di + 1]) {
187
288
  projectsDir = daemonArgs[++di];
188
289
  }
189
290
  }
291
+
292
+ // orch daemon --background — fork and exit
293
+ if (background) {
294
+ const orchPath = fileURLToPath(import.meta.url);
295
+ const fwdArgs = ["daemon"];
296
+ if (projectsDir) fwdArgs.push("--projects", projectsDir);
297
+ const logFile = path.join(os.homedir(), ".orch-daemon.log");
298
+ const out = (await import("fs")).openSync(logFile, "a");
299
+ const child = spawn(process.execPath, [orchPath, ...fwdArgs], {
300
+ stdio: ["ignore", out, out],
301
+ detached: true,
302
+ env: { ...process.env },
303
+ });
304
+ child.unref();
305
+ writeFileSync(pidFile, String(child.pid));
306
+ console.log(`Daemon started in background (pid ${child.pid}).`);
307
+ console.log(`Logs: ${logFile}`);
308
+ console.log(`Stop: orch daemon --stop`);
309
+ process.exit(0);
310
+ }
311
+
312
+ // Foreground — write pid file for --stop/--status
313
+ writeFileSync(pidFile, String(process.pid));
314
+ process.on("exit", () => { try { unlinkSync(pidFile); } catch {} });
315
+ process.on("SIGTERM", () => process.exit(0));
316
+ process.on("SIGINT", () => process.exit(0));
317
+
190
318
  await handleDaemon(projectsDir);
191
319
  // handleDaemon runs forever (or exits on fatal error)
192
320
  }
@@ -245,7 +373,7 @@ async function handleDaemon(projectsDir) {
245
373
 
246
374
  process.stderr.write(`${GREEN}${PREFIX} Listening for remote sessions as "${hostname}"${RESET}\n`);
247
375
  process.stderr.write(`${DIM}${PREFIX} Projects: ${scanRoot} (${directories.length} dirs)${RESET}\n`);
248
- process.stderr.write(`${DIM}${PREFIX} Tip: Use 'nohup orch daemon &' to run in background${RESET}\n`);
376
+ process.stderr.write(`${DIM}${PREFIX} Tip: Use 'orch daemon -b' to run in background${RESET}\n`);
249
377
 
250
378
  let reconnecting = false;
251
379
  const recentRequests = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {