@actionway/cli-dev 0.0.0-dev.33070923659.68d2eb14d93c → 0.0.0-dev.33144058239.2ca8c154c9ba

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 (3) hide show
  1. package/README.md +6 -3
  2. package/dist/index.js +127 -221
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -22,7 +22,7 @@ actionway doctor
22
22
  actionway init
23
23
  ```
24
24
 
25
- `doctor` 只输出脱敏后的 Node/npm、registry、global PATH、配置目录、loopback
25
+ `doctor` 只输出脱敏后的 Node/npm、registry、global PATH、配置目录
26
26
  和 OAuth discovery 检查;它不会修改 npm、代理、CA、PATH 或 PowerShell policy。
27
27
 
28
28
  ### Development 通道
@@ -48,8 +48,11 @@ actionway --agent-host=workbuddy tools search "generate a product image"
48
48
 
49
49
  不带 `--agent-host` 表示用户直接使用 CLI。该声明不写入共享凭据,所以同一个用户和全局 CLI 可以交替被多个 Agent 使用。CLI 同时从每次进程的 Node.js 运行环境识别 Windows、macOS、WSL 或 Linux;这些字段只作为 Site 侧产品分析事件属性,不参与鉴权、计费或 Gateway 路由。
50
50
 
51
- - **auth 唯一途径:Clerk OAuth(PKCE)**。issuer / client_id 运行时从
52
- `{public origin}/api/auth/cli-config` 发现;浏览器授权走随机 `127.0.0.1` 回调端口;
51
+ - **auth 唯一途径:Clerk OAuth(PKCE)+ device 配对**。issuer / client_id 运行时从
52
+ `{public origin}/api/auth/cli-config` 发现;登录不依赖本机浏览器回调——CLI 展示
53
+ 短码与 `{public origin}/cli/activate` 验证 URL,用户在任意设备的浏览器上登录并
54
+ 批准后 CLI 轮询取回一次性授权码并在本地完成 PKCE token 交换(远端 / headless /
55
+ WSL 环境可直接使用,`--no-browser` 仅打印 URL 不尝试开浏览器);
53
56
  凭据存 `~/.actionway/credentials.json`(POSIX 上为 0600 owner-only 文件;
54
57
  Windows 上 chmod 无效,保密性依赖 `%USERPROFILE%` 的继承 ACL——不要把
55
58
  `ACTIONWAY_CONFIG_DIR` 指到共享目录)。CLI 从不打印或存储 OAuth client secret。
package/dist/index.js CHANGED
@@ -12069,7 +12069,6 @@ var {
12069
12069
  import { spawn } from "node:child_process";
12070
12070
  import { createHash, randomBytes } from "node:crypto";
12071
12071
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
12072
- import { createServer } from "node:http";
12073
12072
  import { join as join2 } from "node:path";
12074
12073
 
12075
12074
  // src/lib/update-state.ts
@@ -12415,6 +12414,9 @@ var REFRESH_SKEW_S = 30;
12415
12414
  var TOKEN_TIMEOUT_MS = 2e4;
12416
12415
  var DISCOVERY_TIMEOUT_MS = 1e4;
12417
12416
  var SESSION_TIMEOUT_MS = 2e4;
12417
+ var DEVICE_START_TIMEOUT_MS = 1e4;
12418
+ var DEVICE_POLL_TIMEOUT_MS = 1e4;
12419
+ var DEFAULT_POLL_INTERVAL_S = 3;
12418
12420
  var DEFAULT_LOGIN_TIMEOUT_MS = 12e5;
12419
12421
  function stripSlash(url) {
12420
12422
  return url.replace(/\/+$/, "");
@@ -12524,116 +12526,80 @@ function randomBase64Url() {
12524
12526
  function pkceChallenge(verifier) {
12525
12527
  return createHash("sha256").update(verifier).digest("base64url");
12526
12528
  }
12527
- var CALLBACK_COPY = {
12528
- en: {
12529
- successTitle: "Authorization complete",
12530
- errorTitle: "Authorization incomplete",
12531
- successMessage: "Your terminal is finishing sign-in. You can safely close this page.",
12532
- errorMessage: "Return to your terminal for details, then run the login command again.",
12533
- successStatus: "Returned safely to Actionway CLI",
12534
- errorStatus: "Return to your terminal and try again",
12535
- secure: "Secure connection",
12536
- eyebrow: "CLI authorization",
12537
- session: "Session secured with PKCE",
12538
- callback: "Local callback \xB7 127.0.0.1",
12539
- flow: "Encrypted authorization flow"
12540
- },
12541
- "zh-CN": {
12542
- successTitle: "\u6388\u6743\u5B8C\u6210",
12543
- errorTitle: "\u6388\u6743\u672A\u5B8C\u6210",
12544
- successMessage: "\u7EC8\u7AEF\u6B63\u5728\u5B8C\u6210\u767B\u5F55\uFF0C\u4F60\u73B0\u5728\u53EF\u4EE5\u5B89\u5168\u5730\u5173\u95ED\u8FD9\u4E2A\u9875\u9762\u3002",
12545
- errorMessage: "\u8BF7\u8FD4\u56DE\u7EC8\u7AEF\u67E5\u770B\u9519\u8BEF\u4FE1\u606F\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C\u767B\u5F55\u547D\u4EE4\u3002",
12546
- successStatus: "\u5DF2\u5B89\u5168\u8FD4\u56DE Actionway CLI",
12547
- errorStatus: "\u8BF7\u8FD4\u56DE\u7EC8\u7AEF\u91CD\u8BD5",
12548
- secure: "\u5B89\u5168\u8FDE\u63A5",
12549
- eyebrow: "CLI \u8EAB\u4EFD\u9A8C\u8BC1",
12550
- session: "\u4F1A\u8BDD\u5DF2\u901A\u8FC7 PKCE \u5B89\u5168\u4FDD\u62A4",
12551
- callback: "\u672C\u673A\u56DE\u8C03 \xB7 127.0.0.1",
12552
- flow: "\u52A0\u5BC6\u6388\u6743\u6D41\u7A0B"
12529
+ async function readDeviceJson(response) {
12530
+ let value;
12531
+ try {
12532
+ value = await response.json();
12533
+ } catch {
12534
+ throw new CliError("E_BACKEND", `Actionway sign-in returned an invalid response (${response.status})`);
12535
+ }
12536
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
12537
+ throw new CliError("E_BACKEND", `Actionway sign-in returned an invalid response (${response.status})`);
12538
+ }
12539
+ return value;
12540
+ }
12541
+ function deviceErrorDetail(value) {
12542
+ const error = value.error;
12543
+ const code = typeof error?.code === "string" ? error.code : "";
12544
+ const message = typeof error?.message === "string" ? error.message : "";
12545
+ return [code, message].filter(Boolean).join(": ");
12546
+ }
12547
+ async function startDevicePairing(gateway, codeChallenge, fetchImpl = fetch) {
12548
+ const response = await fetchImpl(`${stripSlash(gateway)}/api/auth/cli-device/start`, {
12549
+ method: "POST",
12550
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
12551
+ body: JSON.stringify({ code_challenge: codeChallenge }),
12552
+ signal: AbortSignal.timeout(DEVICE_START_TIMEOUT_MS)
12553
+ });
12554
+ const value = await readDeviceJson(response);
12555
+ if (!response.ok) {
12556
+ const detail = deviceErrorDetail(value);
12557
+ throw new CliError(
12558
+ "E_BACKEND",
12559
+ `Actionway sign-in is unavailable (${response.status}${detail ? `; ${detail}` : ""})`,
12560
+ "check auth_context.gateway_url is the intended Actionway environment and run `actionway login` again"
12561
+ );
12562
+ }
12563
+ if (typeof value.device_code !== "string" || !value.device_code || typeof value.user_code !== "string" || !value.user_code || typeof value.verification_url !== "string" || typeof value.verification_url_complete !== "string" || typeof value.redirect_uri !== "string") {
12564
+ throw new CliError("E_BACKEND", "Actionway sign-in returned an invalid pairing response");
12565
+ }
12566
+ return {
12567
+ deviceCode: value.device_code,
12568
+ userCode: value.user_code,
12569
+ verificationUrl: value.verification_url,
12570
+ verificationUrlComplete: value.verification_url_complete,
12571
+ redirectUri: value.redirect_uri,
12572
+ expiresInS: typeof value.expires_in === "number" && value.expires_in > 0 ? value.expires_in : 900,
12573
+ intervalS: typeof value.interval === "number" && value.interval >= 1 && value.interval <= 30 ? value.interval : DEFAULT_POLL_INTERVAL_S
12574
+ };
12575
+ }
12576
+ async function pollDevicePairing(gateway, deviceCode, fetchImpl = fetch) {
12577
+ const response = await fetchImpl(`${stripSlash(gateway)}/api/auth/cli-device/poll`, {
12578
+ method: "POST",
12579
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
12580
+ body: JSON.stringify({ device_code: deviceCode }),
12581
+ signal: AbortSignal.timeout(DEVICE_POLL_TIMEOUT_MS)
12582
+ });
12583
+ const value = await readDeviceJson(response);
12584
+ if (!response.ok) {
12585
+ const detail = deviceErrorDetail(value);
12586
+ throw new CliError("E_BACKEND", `Actionway sign-in polling failed (${response.status}${detail ? `; ${detail}` : ""})`);
12587
+ }
12588
+ switch (value.status) {
12589
+ case "pending":
12590
+ return { status: "pending" };
12591
+ case "approved":
12592
+ if (typeof value.code !== "string" || !value.code) {
12593
+ throw new CliError("E_BACKEND", "Actionway sign-in returned an invalid approval response");
12594
+ }
12595
+ return { status: "approved", code: value.code };
12596
+ case "denied":
12597
+ return { status: "denied" };
12598
+ case "expired":
12599
+ return { status: "expired" };
12600
+ default:
12601
+ throw new CliError("E_BACKEND", "Actionway sign-in returned an unknown pairing status");
12553
12602
  }
12554
- };
12555
- function callbackLocale(acceptLanguage) {
12556
- const header = Array.isArray(acceptLanguage) ? acceptLanguage.join(",") : acceptLanguage || "";
12557
- const languages = header.split(",").map((entry, index) => {
12558
- const [tag = "", ...parameters] = entry.trim().toLowerCase().split(";");
12559
- const quality = parameters.find((value) => value.trim().startsWith("q="));
12560
- const parsedQuality = quality ? Number.parseFloat(quality.split("=")[1] || "0") : 1;
12561
- return { tag, quality: Number.isFinite(parsedQuality) ? parsedQuality : 0, index };
12562
- }).filter(({ quality }) => quality > 0).sort((left, right) => right.quality - left.quality || left.index - right.index);
12563
- for (const { tag } of languages) {
12564
- if (tag === "zh" || tag.startsWith("zh-")) return "zh-CN";
12565
- if (tag === "en" || tag.startsWith("en-")) return "en";
12566
- }
12567
- return "en";
12568
- }
12569
- function callbackPage(status, locale = "en") {
12570
- const success = status === "success";
12571
- const copy = CALLBACK_COPY[locale];
12572
- const title = success ? copy.successTitle : copy.errorTitle;
12573
- const message = success ? copy.successMessage : copy.errorMessage;
12574
- const statusText = success ? copy.successStatus : copy.errorStatus;
12575
- const accent = success ? "#a6f46f" : "#ff8b81";
12576
- const accentRgb = success ? "166,244,111" : "255,139,129";
12577
- const icon = success ? '<path d="m8.5 12.5 2.2 2.2 4.8-5.2"/>' : '<path d="M12 8v5m0 3.5v.01"/>';
12578
- return `<!doctype html>
12579
- <html lang="${locale}">
12580
- <head>
12581
- <meta charset="utf-8">
12582
- <meta name="viewport" content="width=device-width,initial-scale=1">
12583
- <meta name="color-scheme" content="dark">
12584
- <title>Actionway \xB7 ${title}</title>
12585
- <style>
12586
- :root{color-scheme:dark}*{box-sizing:border-box}html,body{margin:0;min-height:100%}
12587
- body{min-height:100vh;display:grid;place-items:center;overflow:hidden;padding:32px;color:#f4f6f1;background:#070908;font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
12588
- body:before{content:"";position:fixed;inset:-20%;pointer-events:none;background:radial-gradient(circle at 22% 18%,rgba(${accentRgb},.13),transparent 25%),radial-gradient(circle at 82% 76%,rgba(109,130,255,.1),transparent 24%);filter:blur(8px)}
12589
- body:after{content:"";position:fixed;inset:0;pointer-events:none;opacity:.32;background-image:linear-gradient(rgba(255,255,255,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.045) 1px,transparent 1px);background-size:48px 48px;mask-image:radial-gradient(circle at center,black,transparent 78%)}
12590
- main{position:relative;width:min(100%,720px);overflow:hidden;border:1px solid rgba(255,255,255,.11);border-radius:30px;background:linear-gradient(145deg,rgba(28,31,29,.92),rgba(13,15,14,.96));box-shadow:0 44px 120px rgba(0,0,0,.62),inset 0 1px 0 rgba(255,255,255,.07);animation:enter .7s cubic-bezier(.2,.8,.2,1) both}
12591
- main:before{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(110deg,transparent 24%,rgba(255,255,255,.035) 44%,transparent 60%)}
12592
- .topbar{position:relative;display:flex;align-items:center;justify-content:space-between;padding:22px 26px;border-bottom:1px solid rgba(255,255,255,.08)}
12593
- .brand{display:flex;align-items:center;gap:11px;font-size:15px;font-weight:650;letter-spacing:-.02em}
12594
- .brand-mark{position:relative;display:grid;place-items:center;width:29px;height:29px;border:1px solid rgba(255,255,255,.15);border-radius:9px;color:#111;background:#f1f4ed;font:750 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace;box-shadow:0 8px 22px rgba(0,0,0,.24)}
12595
- .secure{display:flex;align-items:center;gap:8px;color:#8e978f;font:600 10px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.12em;text-transform:uppercase}
12596
- .secure-dot{width:6px;height:6px;border-radius:50%;background:${accent};box-shadow:0 0 16px rgba(${accentRgb},.8);animation:pulse 2.2s ease-in-out infinite}
12597
- .content{position:relative;padding:54px 58px 42px}
12598
- .orb{position:absolute;top:42px;right:48px;display:grid;place-items:center;width:96px;height:96px;border:1px solid rgba(${accentRgb},.28);border-radius:50%;color:${accent};background:radial-gradient(circle at 35% 28%,rgba(255,255,255,.12),rgba(${accentRgb},.06) 46%,rgba(0,0,0,.08));box-shadow:0 0 0 12px rgba(${accentRgb},.035),0 0 70px rgba(${accentRgb},.13);animation:float 4.8s ease-in-out infinite}
12599
- .orb:before,.orb:after{content:"";position:absolute;border:1px solid rgba(${accentRgb},.14);border-radius:50%}.orb:before{inset:-12px}.orb:after{inset:11px}
12600
- .orb svg{position:relative;z-index:1;width:42px;height:42px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
12601
- .eyebrow{margin:0 0 20px;color:${accent};font:650 11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.16em;text-transform:uppercase}
12602
- h1{max-width:500px;margin:0;color:#f4f6f1;font-size:clamp(48px,7vw,74px);font-weight:620;line-height:.98;letter-spacing:-.065em}
12603
- body[data-locale="en"] h1{font-size:clamp(42px,6vw,64px);letter-spacing:-.05em}
12604
- h1 span{color:#7d857e}
12605
- .message{max-width:450px;margin:26px 0 38px;color:#9ba39c;font-size:16px;line-height:1.75}
12606
- .terminal{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:15px;padding:17px 18px;border:1px solid rgba(255,255,255,.09);border-radius:15px;background:rgba(4,6,5,.5);box-shadow:inset 0 1px rgba(255,255,255,.025)}
12607
- .prompt{color:${accent};font:700 18px/1 ui-monospace,SFMono-Regular,Menlo,monospace}
12608
- .terminal strong,.terminal small{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.terminal strong{color:#d7dcd5;font-size:12px}.terminal small{margin-top:5px;color:#677068;font-size:10px;letter-spacing:.04em}
12609
- .terminal-check{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;color:#071006;background:${accent};font-size:13px;font-weight:900;box-shadow:0 0 22px rgba(${accentRgb},.2)}
12610
- footer{position:relative;display:flex;justify-content:space-between;gap:18px;padding:19px 26px;border-top:1px solid rgba(255,255,255,.075);color:#5f6760;font:550 9px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;text-transform:uppercase;letter-spacing:.13em}
12611
- @keyframes enter{from{opacity:0;transform:translateY(18px) scale(.985)}to{opacity:1;transform:none}}@keyframes float{50%{transform:translateY(-7px)}}@keyframes pulse{50%{opacity:.45;box-shadow:0 0 8px rgba(${accentRgb},.35)}}
12612
- @media(max-width:640px){body{padding:16px;overflow:auto}main{border-radius:23px}.topbar{padding:18px 20px}.content{padding:38px 26px 30px}.orb{position:relative;top:auto;right:auto;width:76px;height:76px;margin:0 0 36px}h1{font-size:48px}.message{font-size:15px}footer{padding:17px 20px;flex-direction:column;gap:5px}}
12613
- @media(prefers-reduced-motion:reduce){main,.orb,.secure-dot{animation:none}}
12614
- </style>
12615
- </head>
12616
- <body data-locale="${locale}">
12617
- <main>
12618
- <header class="topbar">
12619
- <div class="brand"><span class="brand-mark">AW</span><span>Actionway</span></div>
12620
- <div class="secure"><span class="secure-dot"></span><span>${copy.secure}</span></div>
12621
- </header>
12622
- <section class="content">
12623
- <div class="orb"><svg viewBox="0 0 24 24" aria-hidden="true">${icon}</svg></div>
12624
- <p class="eyebrow">${copy.eyebrow}</p>
12625
- <h1>${title}<br><span>Actionway CLI</span></h1>
12626
- <p class="message">${message}</p>
12627
- <div class="terminal">
12628
- <span class="prompt">\u203A</span>
12629
- <span><strong>${statusText}</strong><small>${copy.session}</small></span>
12630
- <span class="terminal-check">${success ? "\u2713" : "!"}</span>
12631
- </div>
12632
- </section>
12633
- <footer><span>${copy.callback}</span><span>${copy.flow}</span></footer>
12634
- </main>
12635
- </body>
12636
- </html>`;
12637
12603
  }
12638
12604
  function openBrowser(url, onNotice) {
12639
12605
  onNotice(`Opening your browser. If it does not open, use:
@@ -12644,62 +12610,6 @@ ${url}`);
12644
12610
  child.once("error", () => void 0);
12645
12611
  child.unref();
12646
12612
  }
12647
- async function waitForCode(state, timeoutMs) {
12648
- let resolveCode;
12649
- let rejectCode;
12650
- const code = new Promise((resolve3, reject) => {
12651
- resolveCode = resolve3;
12652
- rejectCode = reject;
12653
- });
12654
- const server = createServer((request, response) => {
12655
- const url = new URL(request.url || "/", "http://127.0.0.1");
12656
- if (request.method !== "GET" || url.pathname !== "/callback") {
12657
- response.writeHead(404).end();
12658
- return;
12659
- }
12660
- const receivedCode = url.searchParams.get("code");
12661
- const oauthError = url.searchParams.get("error_description") || url.searchParams.get("error");
12662
- const locale = callbackLocale(request.headers["accept-language"]);
12663
- if (oauthError || url.searchParams.get("state") !== state || !receivedCode) {
12664
- response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
12665
- response.end(callbackPage("error", locale));
12666
- rejectCode(new CliError("E_BACKEND", oauthError || "OAuth callback validation failed."));
12667
- return;
12668
- }
12669
- response.writeHead(200, {
12670
- "Cache-Control": "no-store",
12671
- "Content-Type": "text/html; charset=utf-8",
12672
- "Referrer-Policy": "no-referrer"
12673
- });
12674
- response.end(callbackPage("success", locale), () => {
12675
- resolveCode(receivedCode);
12676
- server.close();
12677
- });
12678
- });
12679
- await new Promise((resolve3, reject) => {
12680
- server.once("error", reject);
12681
- server.listen(0, "127.0.0.1", resolve3);
12682
- });
12683
- const port = server.address().port;
12684
- const timer = setTimeout(() => {
12685
- rejectCode(
12686
- new CliError(
12687
- "E_BACKEND",
12688
- `login timed out after ${Math.round(timeoutMs / 1e3)} seconds`,
12689
- "run `actionway login` again and ask the user to finish the browser sign-in promptly; pass --timeout-seconds <n> to allow more time"
12690
- )
12691
- );
12692
- server.close();
12693
- }, timeoutMs);
12694
- return {
12695
- redirectUri: `http://127.0.0.1:${port}/callback`,
12696
- code: code.finally(() => clearTimeout(timer)),
12697
- close: () => {
12698
- clearTimeout(timer);
12699
- return new Promise((resolve3) => server.listening ? server.close(() => resolve3()) : resolve3());
12700
- }
12701
- };
12702
- }
12703
12613
  async function readJsonObject(response) {
12704
12614
  const value = await response.json();
12705
12615
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -12792,9 +12702,13 @@ async function fetchCliSession(gateway, accessToken, fetchImpl = fetch) {
12792
12702
  walletId: value.walletId
12793
12703
  };
12794
12704
  }
12705
+ function defaultSleep(ms) {
12706
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
12707
+ }
12795
12708
  async function login(options = {}) {
12796
12709
  const env = options.env ?? process.env;
12797
12710
  const fetchImpl = options.fetchImpl ?? fetch;
12711
+ const sleep = options.sleepImpl ?? defaultSleep;
12798
12712
  const onNotice = options.onNotice ?? ((message) => process.stderr.write(`${message}
12799
12713
  `));
12800
12714
  const gateway = resolveGatewayUrl(options.gateway, env);
@@ -12841,31 +12755,56 @@ async function login(options = {}) {
12841
12755
  issuer ||= discovered.issuer;
12842
12756
  }
12843
12757
  const timeoutMs = options.timeoutSeconds !== void 0 && Number.isFinite(options.timeoutSeconds) && options.timeoutSeconds > 0 ? options.timeoutSeconds * 1e3 : DEFAULT_LOGIN_TIMEOUT_MS;
12844
- const verifier = randomBase64Url();
12845
- const state = randomBase64Url();
12846
- const callback = await waitForCode(state, timeoutMs);
12847
12758
  try {
12848
- const authorizeUrl = new URL(`${stripSlash(issuer)}/oauth/authorize`);
12849
- authorizeUrl.search = new URLSearchParams({
12850
- response_type: "code",
12851
- client_id: clientId,
12852
- redirect_uri: callback.redirectUri,
12853
- scope: SCOPES,
12854
- state,
12855
- code_challenge: pkceChallenge(verifier),
12856
- code_challenge_method: "S256"
12857
- }).toString();
12858
- if (options.noBrowser) onNotice(`Open this URL to sign in:
12859
- ${authorizeUrl}`);
12860
- else openBrowser(authorizeUrl.toString(), onNotice);
12759
+ const verifier = randomBase64Url();
12760
+ const pairing = await startDevicePairing(gateway, pkceChallenge(verifier), fetchImpl);
12761
+ onNotice(
12762
+ `Sign in to Actionway on any device:
12763
+ ${pairing.verificationUrl}
12764
+ One-time code: ${pairing.userCode}`
12765
+ );
12766
+ if (options.noBrowser) onNotice(`Open this URL to approve the sign-in:
12767
+ ${pairing.verificationUrlComplete}`);
12768
+ else openBrowser(pairing.verificationUrlComplete, onNotice);
12769
+ const deadline = Date.now() + Math.min(timeoutMs, pairing.expiresInS * 1e3);
12770
+ let approvalCode = null;
12771
+ for (; ; ) {
12772
+ const poll = await pollDevicePairing(gateway, pairing.deviceCode, fetchImpl);
12773
+ if (poll.status === "approved") {
12774
+ approvalCode = poll.code;
12775
+ break;
12776
+ }
12777
+ if (poll.status === "denied") {
12778
+ throw new CliError(
12779
+ "E_BACKEND",
12780
+ "the sign-in request was denied in the browser",
12781
+ "run `actionway login` again and approve the request on the activation page"
12782
+ );
12783
+ }
12784
+ if (poll.status === "expired") {
12785
+ throw new CliError(
12786
+ "E_BACKEND",
12787
+ "the sign-in code expired before it was approved",
12788
+ "run `actionway login` again and enter the fresh code promptly"
12789
+ );
12790
+ }
12791
+ if (Date.now() >= deadline) {
12792
+ throw new CliError(
12793
+ "E_BACKEND",
12794
+ `login timed out after ${Math.round(timeoutMs / 1e3)} seconds`,
12795
+ "run `actionway login` again and ask the user to approve the sign-in promptly; pass --timeout-seconds <n> to allow more time"
12796
+ );
12797
+ }
12798
+ await sleep(pairing.intervalS * 1e3);
12799
+ }
12861
12800
  const tokens = await tokenRequest(
12862
12801
  issuer,
12863
12802
  new URLSearchParams({
12864
12803
  grant_type: "authorization_code",
12865
12804
  client_id: clientId,
12866
- code: await callback.code,
12805
+ code: approvalCode,
12867
12806
  code_verifier: verifier,
12868
- redirect_uri: callback.redirectUri
12807
+ redirect_uri: pairing.redirectUri
12869
12808
  }),
12870
12809
  fetchImpl
12871
12810
  );
@@ -12894,8 +12833,6 @@ ${authorizeUrl}`);
12894
12833
  return { credentials, account };
12895
12834
  } catch (error) {
12896
12835
  return withAuthContext(error);
12897
- } finally {
12898
- await callback.close();
12899
12836
  }
12900
12837
  }
12901
12838
  function whoamiSnapshot(env = process.env) {
@@ -12946,7 +12883,7 @@ function failFromError2(err, fallbackHint) {
12946
12883
  });
12947
12884
  }
12948
12885
  function registerAuthCommands(program2) {
12949
- program2.command("login").description("Authenticate with Actionway in the browser (Clerk OAuth with PKCE).").option("--gateway <url>", "Actionway public origin").option("--client-id <id>", "Actionway OAuth client id (must be paired with --issuer; default: both discovered from the public origin)").option("--issuer <url>", "Actionway OAuth issuer (must be paired with --client-id; default: both discovered from the public origin)").option("--timeout-seconds <n>", "how long to wait for browser authentication").option("--no-browser", "print the authorize URL instead of opening a browser").action(async (opts) => {
12886
+ program2.command("login").description("Authenticate with Actionway by approving a one-time code in any browser (Clerk OAuth with PKCE).").option("--gateway <url>", "Actionway public origin").option("--client-id <id>", "Actionway OAuth client id (must be paired with --issuer; default: both discovered from the public origin)").option("--issuer <url>", "Actionway OAuth issuer (must be paired with --client-id; default: both discovered from the public origin)").option("--timeout-seconds <n>", "how long to wait for the sign-in to be approved").option("--no-browser", "print the verification URL instead of opening a browser (use on remote/headless machines)").action(async (opts) => {
12950
12887
  try {
12951
12888
  const { credentials, account } = await login({
12952
12889
  ...opts.gateway ? { gateway: opts.gateway } : {},
@@ -13035,7 +12972,6 @@ function registerAccountCommands(program2, getTransport2) {
13035
12972
  // src/commands/doctor.ts
13036
12973
  import { spawnSync as spawnSync2 } from "node:child_process";
13037
12974
  import { existsSync as existsSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
13038
- import { createServer as createServer2 } from "node:http";
13039
12975
  import { randomBytes as randomBytes2 } from "node:crypto";
13040
12976
  import { dirname as dirname3, join as join3, posix, win32 } from "node:path";
13041
12977
  function pass(id, message) {
@@ -13087,13 +13023,6 @@ function defaultWritable(path) {
13087
13023
  return false;
13088
13024
  }
13089
13025
  }
13090
- function checkLoopback() {
13091
- return new Promise((resolve3, reject) => {
13092
- const server = createServer2();
13093
- server.once("error", reject);
13094
- server.listen(0, "127.0.0.1", () => server.close((error) => error ? reject(error) : resolve3()));
13095
- });
13096
- }
13097
13026
  function defaultResolveCommand(name) {
13098
13027
  const result = process.platform === "win32" ? spawnSync2("where.exe", [name], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5e3, windowsHide: true }) : spawnSync2("/bin/sh", ["-c", `command -v -- ${name}`], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5e3 });
13099
13028
  if (result.error || result.status !== 0) return null;
@@ -13122,7 +13051,6 @@ async function diagnoseActionway(options = {}, dependencies = {}) {
13122
13051
  const pathExists = dependencies.pathExists ?? existsSync2;
13123
13052
  const pathWritable = dependencies.pathWritable ?? defaultWritable;
13124
13053
  const fetchOAuth = dependencies.fetchOAuth ?? fetchCliOAuthConfig;
13125
- const loopback = dependencies.checkLoopback ?? checkLoopback;
13126
13054
  const readPolicy = dependencies.readPowerShellPolicy ?? powerShellPolicy;
13127
13055
  const resolveCommand = dependencies.resolveCommand ?? defaultResolveCommand;
13128
13056
  const checks = [];
@@ -13132,17 +13060,7 @@ async function diagnoseActionway(options = {}, dependencies = {}) {
13132
13060
  checks.push(
13133
13061
  Number.isFinite(nodeMajor) && nodeMajor >= 20 ? pass("node", `Node.js ${nodeVersion} is supported.`) : fail2("node", `Node.js ${nodeVersion || "unknown"} is unsupported.`, "install Node.js 20 or newer and reopen the terminal")
13134
13062
  );
13135
- if (platform === "linux" && (env.WSL_DISTRO_NAME || env.WSL_INTEROP)) {
13136
- checks.push(
13137
- warn(
13138
- "execution_environment",
13139
- "WSL detected; browser loopback may not share the Windows host network namespace.",
13140
- "run Actionway login from the Windows host if the browser callback cannot return"
13141
- )
13142
- );
13143
- } else {
13144
- checks.push(pass("execution_environment", `${platform}/${architecture} host environment detected.`));
13145
- }
13063
+ checks.push(pass("execution_environment", `${platform}/${architecture} host environment detected.`));
13146
13064
  checks.push(
13147
13065
  pathWritable(configDir) ? pass("config_dir", `Configuration directory is writable: ${configDir}`) : fail2("config_dir", `Configuration directory is not writable: ${configDir}`, "set ACTIONWAY_CONFIG_DIR to a user-writable directory")
13148
13066
  );
@@ -13204,18 +13122,6 @@ async function diagnoseActionway(options = {}, dependencies = {}) {
13204
13122
  )
13205
13123
  );
13206
13124
  }
13207
- try {
13208
- await loopback();
13209
- checks.push(pass("loopback", "A random 127.0.0.1 callback port can be opened."));
13210
- } catch (error) {
13211
- checks.push(
13212
- fail2(
13213
- "loopback",
13214
- `Cannot open the local OAuth callback: ${error instanceof Error ? error.message : String(error)}`,
13215
- "run login from a normal host terminal and check firewall or sandbox policy"
13216
- )
13217
- );
13218
- }
13219
13125
  if (options.network === false) {
13220
13126
  checks.push({ id: "npm_registry", status: "skip", message: "npm registry check was skipped." });
13221
13127
  checks.push({ id: "actionway_oauth", status: "skip", message: "Actionway OAuth discovery check was skipped." });
@@ -13259,7 +13165,7 @@ async function diagnoseActionway(options = {}, dependencies = {}) {
13259
13165
  }
13260
13166
  function registerDoctorCommand(program2) {
13261
13167
  const distribution = cliDistribution();
13262
- program2.command("doctor").description("Diagnose Node, npm, PATH, registry, configuration, and local OAuth callback readiness.").option("--no-network", "skip npm registry and Actionway OAuth discovery checks").action(async (options) => {
13168
+ program2.command("doctor").description("Diagnose Node, npm, PATH, registry, configuration, and Actionway OAuth discovery readiness.").option("--no-network", "skip npm registry and Actionway OAuth discovery checks").action(async (options) => {
13263
13169
  const report = await diagnoseActionway({ network: options.network !== false });
13264
13170
  const command = process.platform === "win32" ? `${distribution.commandName}.cmd` : distribution.commandName;
13265
13171
  ok({
@@ -13521,7 +13427,7 @@ function registerInitCommand(program2, getClientContext = () => ({ runtimeOs: "o
13521
13427
  const distribution = cliDistribution();
13522
13428
  program2.command("init").description(
13523
13429
  "Complete browser authentication and stage the Actionway Skill for your agent to install. The CLI never writes into an agent's own configuration directory."
13524
- ).option("--gateway <url>", "Actionway public origin").option("--client-id <id>", "Actionway OAuth client id (must be paired with --issuer)").option("--issuer <url>", "Actionway OAuth issuer (must be paired with --client-id)").option("--timeout-seconds <n>", "how long to wait for browser authentication").option("--install-session-id <uuid>", "onboarding install session id").option("--no-browser", "print the authorize URL instead of opening a browser").addOption(new Option("--skill-only").hideHelp()).action(async (opts) => {
13430
+ ).option("--gateway <url>", "Actionway public origin").option("--client-id <id>", "Actionway OAuth client id (must be paired with --issuer)").option("--issuer <url>", "Actionway OAuth issuer (must be paired with --client-id)").option("--timeout-seconds <n>", "how long to wait for the sign-in to be approved").option("--install-session-id <uuid>", "onboarding install session id").option("--no-browser", "print the verification URL instead of opening a browser (use on remote/headless machines)").addOption(new Option("--skill-only").hideHelp()).action(async (opts) => {
13525
13431
  try {
13526
13432
  const clientContext = getClientContext();
13527
13433
  const invocation = clientContext.agentHost ? `${distribution.commandName} --agent-host=${clientContext.agentHost}` : distribution.commandName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actionway/cli-dev",
3
- "version": "0.0.0-dev.33070923659.68d2eb14d93c",
3
+ "version": "0.0.0-dev.33144058239.2ca8c154c9ba",
4
4
  "description": "Actionway development CLI for dev.actionway.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,8 +19,8 @@
19
19
  "tsx": "^4.20.0",
20
20
  "typescript": "7.0.2",
21
21
  "vitest": "^3.2.7",
22
- "@actionway/cli-core": "0.1.0",
23
- "@actionway/contracts": "0.1.0"
22
+ "@actionway/contracts": "0.1.0",
23
+ "@actionway/cli-core": "0.1.0"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=20"
@@ -36,9 +36,9 @@
36
36
  },
37
37
  "actionway": {
38
38
  "channel": "development",
39
- "source_sha": "68d2eb14d93c28f5f9fef30e84cb7de5d4cddfb0",
40
- "run_id": 33070923659,
41
- "run_url": "https://github.com/PawLogic/actionway/actions/runs/33070923659",
39
+ "source_sha": "2ca8c154c9ba3d13b5dd85d5de5b9f4b6beb605e",
40
+ "run_id": 33144058239,
41
+ "run_url": "https://github.com/PawLogic/actionway/actions/runs/33144058239",
42
42
  "gateway_url": "https://dev.actionway.ai"
43
43
  }
44
44
  }