orchestrating 0.1.31 → 0.1.33

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 +120 -46
  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
 
@@ -249,6 +313,7 @@ async function handleDaemon(projectsDir) {
249
313
 
250
314
  let reconnecting = false;
251
315
  const recentRequests = new Set();
316
+ const recentSpawns = new Map(); // "command:cwd" -> timestamp
252
317
 
253
318
  function scheduleReconnect(delaySec) {
254
319
  if (reconnecting) return;
@@ -336,6 +401,15 @@ async function handleDaemon(projectsDir) {
336
401
  recentRequests.add(requestId);
337
402
  setTimeout(() => recentRequests.delete(requestId), 30_000);
338
403
 
404
+ // Dedup: prevent spawning same command in same directory within 10s
405
+ const spawnKey = `${cmd}:${(cmdArgs || []).join(",")}:${cwd || ""}`;
406
+ const lastSpawn = recentSpawns.get(spawnKey);
407
+ if (lastSpawn && Date.now() - lastSpawn < 10_000) {
408
+ process.stderr.write(`${DIM}${PREFIX} Ignoring duplicate spawn: ${spawnKey}${RESET}\n`);
409
+ return;
410
+ }
411
+ recentSpawns.set(spawnKey, Date.now());
412
+
339
413
  const cmdStr = [cmd, ...cmdArgs].join(" ");
340
414
  process.stderr.write(`${GREEN}${PREFIX} Spawning: ${cmdStr}${RESET}\n`);
341
415
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrating",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Stream terminal sessions to the orchestrat.ing dashboard",
5
5
  "type": "module",
6
6
  "bin": {