pleumrouter 0.5.51 → 0.5.53

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": "pleumrouter",
3
- "version": "0.5.51",
3
+ "version": "0.5.53",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,7 @@ _pleum() {
46
46
  completion) compadd zsh bash;;
47
47
  esac
48
48
  }
49
+ compdef _pleum pleum 2>/dev/null || true
49
50
  _pleum "$@"`;
50
51
  }
51
52
 
package/src/login.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
- import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials, setActiveContext, refreshAccessToken, accessTokenIsStale } from "./config.mjs";
4
+ import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials, setActiveContext, refreshAccessToken, accessTokenIsStale, readJwtExp } from "./config.mjs";
5
5
  import { t } from "./i18n.mjs";
6
6
  import { sanitizeTerminalText } from "./ansi.mjs";
7
7
 
@@ -30,13 +30,16 @@ function openBrowser(url) {
30
30
  }
31
31
  }
32
32
 
33
- function saveTokens({ access_token, refresh_token, expires_in }) {
33
+ export function saveTokens({ access_token, refresh_token, expires_in }) {
34
34
  const existing = readConfig() || {};
35
35
  const { key: _legacy, ...rest } = existing;
36
+ const exp = readJwtExp(access_token);
36
37
  const token_expires_at =
37
- typeof expires_in === "number"
38
- ? Date.now() + expires_in * 1000
39
- : undefined;
38
+ exp != null
39
+ ? exp * 1000
40
+ : typeof expires_in === "number"
41
+ ? Date.now() + expires_in * 1000
42
+ : undefined;
40
43
  writeConfig(
41
44
  mergeLoginConfig(rest, {
42
45
  access_token,
@@ -151,10 +154,31 @@ export async function login() {
151
154
  console.log(`✓ ${t("Logged in. Saved the session to ~/.pleum/config.json.")}`);
152
155
  }
153
156
 
157
+ export function retryAfterSeconds(response) {
158
+ const value = response.headers.get("Retry-After");
159
+ const seconds = value === null ? NaN : Number(value);
160
+ return Number.isFinite(seconds) && seconds > 0 ? Math.ceil(seconds) : 60;
161
+ }
162
+
154
163
  // device-code 로그인 — 브라우저 없는 환경(SSH·CI).
155
164
  // 1) code 발급 → 2) 사용자가 /device에서 확인 → 3) CLI가 폴링으로 JWT 수신.
156
165
  export async function loginDevice() {
157
- const res = await fetch(`${V1}/auth/device/code`, { method: "POST" });
166
+ const codeDeadline = Date.now() + 120_000;
167
+ let res;
168
+ while (true) {
169
+ try {
170
+ res = await fetch(`${V1}/auth/device/code`, { method: "POST", signal: AbortSignal.timeout(Math.max(1, codeDeadline - Date.now())) });
171
+ } catch (error) {
172
+ if (error.name !== "TimeoutError") throw error;
173
+ console.error(t("Login timed out (2 minutes)"));
174
+ process.exit(1);
175
+ }
176
+ if (res.status !== 429) break;
177
+ const wait = retryAfterSeconds(res) * 1000;
178
+ if (Date.now() + wait >= codeDeadline) break;
179
+ console.error(t("Failed to issue a device code (server responded {status})", { status: res.status }));
180
+ await new Promise((r) => setTimeout(r, wait));
181
+ }
158
182
  if (!res.ok) {
159
183
  console.error(t("Failed to issue a device code (server responded {status})", { status: res.status }));
160
184
  process.exit(1);
@@ -170,19 +194,26 @@ export async function loginDevice() {
170
194
  const expiresIn = Number(expires_in) || 600;
171
195
  const pollInterval = Math.max(1, Number(interval) || 5);
172
196
  const deadline = Date.now() + expiresIn * 1000;
173
- while (Date.now() < deadline) {
174
- await new Promise((r) => setTimeout(r, pollInterval * 1000));
197
+ let nextWait = pollInterval * 1000;
198
+ while (Date.now() + nextWait < deadline) {
199
+ await new Promise((r) => setTimeout(r, nextWait));
200
+ nextWait = pollInterval * 1000;
175
201
  let poll;
176
202
  try {
177
203
  poll = await fetch(`${V1}/auth/device/token`, {
178
204
  method: "POST",
179
205
  headers: { "Content-Type": "application/json" },
180
206
  body: JSON.stringify({ device_code }),
207
+ signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
181
208
  });
182
209
  } catch {
183
210
  continue; // Transient network error; retry on next poll tick
184
211
  }
185
212
  if (poll.status === 202) continue; // authorization_pending
213
+ if (poll.status === 429) {
214
+ nextWait = Math.max(pollInterval, retryAfterSeconds(poll)) * 1000;
215
+ continue;
216
+ }
186
217
  if (!poll.ok) {
187
218
  const e = await poll.json().catch(() => ({}));
188
219
  if (e.detail === "expired_token") { console.error(t("The code has expired.")); process.exit(1); }