harness-wingman 0.9.7 → 0.9.8

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.
@@ -283,6 +283,14 @@ var init_cli = __esm({
283
283
  "cli.licenseJson": "The license file is not valid JSON",
284
284
  "cli.licenseFields": "The license file is missing token/signature fields",
285
285
  "cli.licenseSignature": "License signature verification failed (modified content or issued for another product)",
286
+ "cli.licenseOtherDevice": "This license is bound to another device",
287
+ "cli.licenseDeviceUnavailable": "This Mac\u2019s device identifier is unavailable. Activation is unavailable; existing unbound licenses keep their current validity.",
288
+ "cli.licenseDeviceLimit": "This license is already activated on 3 devices",
289
+ "cli.licenseActivationExpired": "This license has expired. Renew it before activating this device.",
290
+ "cli.licenseUnbindRequiresActiveDevice": "Unbind devices from an activated device with an unexpired license.",
291
+ "cli.licenseDeviceRevoked": "This device\u2019s activation was revoked. Click \u201CActivate this device\u201D to activate it again.",
292
+ "cli.licenseFixturesOffline": "Fixture mode does not contact the online license server",
293
+ "cli.licenseDevicesInvalid": "The license server returned an invalid device list",
286
294
  "cli.licenseClaimsFields": "License payload fields are incomplete",
287
295
  "cli.licenseClaimsDecode": "The license payload could not be decoded",
288
296
  "cli.licenseValidUntil": "{plan} valid until {exp}",
@@ -443,6 +451,10 @@ var init_panel = __esm({
443
451
  "panel.mobile.disclosure": "After pairing, your Mac uploads encrypted project and session status for the phone app to read. The relay only sees ciphertext.",
444
452
  "panel.mobile.scan": "Scan with the Wingman app on your phone",
445
453
  "panel.mobile.qrLabel": "Phone pairing QR code",
454
+ "panel.mobile.qrExpired": "QR code expired",
455
+ "panel.mobile.expired": "Expired",
456
+ "panel.mobile.regenerate": "Regenerate",
457
+ "panel.mobile.expiredTip": "The pairing window has ended. Generate a new QR code to scan.",
446
458
  "panel.mobile.autoClose": "Closes automatically after you scan on your phone",
447
459
  "panel.mobile.cancel": "Cancel pairing",
448
460
  "panel.mobile.unnamed": "Phone",
@@ -559,6 +571,23 @@ var init_panel = __esm({
559
571
  "panel.license.licenseImportedNewStatusUnknownRefresh": "License imported (new status unknown; refresh this page)",
560
572
  "panel.license.dropOneLicenseJsonFileAt": "Drop one license.json file at a time (received {p0})",
561
573
  "panel.license.onlyJsonFilesAreAcceptedReceived": "Only .json files are accepted (received {p0})",
574
+ "panel.license.deviceActive": "This device is activated \xB7 {count}/{max} devices",
575
+ "panel.license.deviceLimit": "This license is activated on {count} devices",
576
+ "panel.license.deviceNotRegistered": "This device is not activated \xB7 {count}/{max} devices",
577
+ "panel.license.deviceNotRegisteredTip": "Import the license again to activate this device when a slot is available",
578
+ "panel.license.deviceRevoked": "Activation for this device was revoked",
579
+ "panel.license.deviceRevokedTip": "Your existing license is kept until it expires. Automatic and manual refresh cannot reactivate this device. Select Activate this device when a device slot is available.",
580
+ "panel.license.activateDevice": "Activate this device",
581
+ "panel.license.deviceUnavailable": "Cannot read this device\u2019s identifier; activation is unavailable",
582
+ "panel.license.devicePending": "This device is not activated yet",
583
+ "panel.license.deviceTip": "Each subscription supports up to {max} devices. Licenses renew automatically every 24 hours. View devices.",
584
+ "panel.license.deviceLimitTip": "Unbind an old device from an activated device, then import the license again here. Any existing local license is kept until it expires.",
585
+ "panel.license.deviceManageElsewhere": "Unbind from an activated device with an unexpired license",
586
+ "panel.license.devices": "Activated devices",
587
+ "panel.license.unnamedDevice": "Unnamed device",
588
+ "panel.license.deviceLastSeen": "Last renewed: {at}",
589
+ "panel.license.unbind": "Unbind",
590
+ "panel.license.unbindTip": "Unbinding revokes renewals for that device. Its existing license is kept until it expires. Only importing a license again or selecting Activate this device on that machine can reactivate it.",
562
591
  "panel.live.sessionStarted": "Session started",
563
592
  "panel.live.sessionEnded": "Session ended",
564
593
  "panel.live.promptSubmitted": "Prompt submitted",
@@ -1639,6 +1668,14 @@ var init_cli2 = __esm({
1639
1668
  "cli.licenseJson": "license \u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON",
1640
1669
  "cli.licenseFields": "license \u6587\u4EF6\u7F3A token/signature \u5B57\u6BB5",
1641
1670
  "cli.licenseSignature": "license \u9A8C\u7B7E\u5931\u8D25(\u5185\u5BB9\u88AB\u6539\u52A8\u6216\u975E\u672C\u4EA7\u54C1\u7B7E\u53D1)",
1671
+ "cli.licenseOtherDevice": "\u6B64 license \u7ED1\u5B9A\u4E86\u5176\u4ED6\u8BBE\u5907",
1672
+ "cli.licenseDeviceUnavailable": "\u65E0\u6CD5\u83B7\u53D6\u672C\u673A\u8BBE\u5907\u6807\u8BC6\uFF0C\u6682\u65F6\u65E0\u6CD5\u6FC0\u6D3B\uFF1B\u5DF2\u6709\u672A\u7ED1\u5B9A license \u4ECD\u6309\u539F\u6709\u6548\u671F\u4F7F\u7528\u3002",
1673
+ "cli.licenseDeviceLimit": "\u6B64 license \u5DF2\u5728 3 \u53F0\u8BBE\u5907\u6FC0\u6D3B",
1674
+ "cli.licenseActivationExpired": "\u6B64 license \u5DF2\u5230\u671F\uFF0C\u8BF7\u5148\u7EED\u671F\u540E\u518D\u6FC0\u6D3B\u672C\u673A\u3002",
1675
+ "cli.licenseUnbindRequiresActiveDevice": "\u8BF7\u4ECE\u5DF2\u6FC0\u6D3B\u4E14\u6388\u6743\u672A\u5230\u671F\u7684\u8BBE\u5907\u89E3\u7ED1\u3002",
1676
+ "cli.licenseDeviceRevoked": "\u672C\u673A\u6FC0\u6D3B\u5DF2\u88AB\u64A4\u9500\uFF1B\u5982\u9700\u7EE7\u7EED\u4F7F\u7528\uFF0C\u8BF7\u70B9\u51FB\u300C\u6FC0\u6D3B\u672C\u673A\u300D\u3002",
1677
+ "cli.licenseFixturesOffline": "\u6837\u4F8B\u6A21\u5F0F\u4E0D\u4F1A\u8054\u7CFB\u5728\u7EBF\u7B7E\u53D1\u7AEF",
1678
+ "cli.licenseDevicesInvalid": "\u7B7E\u53D1\u7AEF\u8FD4\u56DE\u7684\u8BBE\u5907\u5217\u8868\u65E0\u6548",
1642
1679
  "cli.licenseClaimsFields": "license \u8F7D\u8377\u5B57\u6BB5\u4E0D\u5B8C\u6574",
1643
1680
  "cli.licenseClaimsDecode": "license \u8F7D\u8377\u65E0\u6CD5\u89E3\u7801",
1644
1681
  "cli.licenseValidUntil": "{plan}\u6709\u6548\u81F3 {exp}",
@@ -1799,6 +1836,10 @@ var init_panel2 = __esm({
1799
1836
  "panel.mobile.disclosure": "\u914D\u5BF9\u540E Mac \u4F1A\u628A\u9879\u76EE\u4E0E\u4F1A\u8BDD\u72B6\u6001\u52A0\u5BC6\u4E0A\u4F20,\u624B\u673A App \u624D\u80FD\u770B\u5230;\u4E2D\u8F6C\u53EA\u89C1\u5BC6\u6587",
1800
1837
  "panel.mobile.scan": "\u6253\u5F00\u624B\u673A\u4E0A\u7684 Wingman App \u626B\u7801\u914D\u5BF9",
1801
1838
  "panel.mobile.qrLabel": "\u624B\u673A\u914D\u5BF9\u4E8C\u7EF4\u7801",
1839
+ "panel.mobile.qrExpired": "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F",
1840
+ "panel.mobile.expired": "\u5DF2\u8FC7\u671F",
1841
+ "panel.mobile.regenerate": "\u91CD\u65B0\u751F\u6210",
1842
+ "panel.mobile.expiredTip": "\u914D\u5BF9\u7B49\u5F85\u7A97\u53E3\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801\u540E\u626B\u7801\u3002",
1802
1843
  "panel.mobile.autoClose": "\u5DF2\u5728\u624B\u673A\u4E0A\u626B\u7801\u540E\u81EA\u52A8\u5173\u95ED",
1803
1844
  "panel.mobile.cancel": "\u53D6\u6D88\u914D\u5BF9",
1804
1845
  "panel.mobile.unnamed": "\u624B\u673A",
@@ -1915,6 +1956,23 @@ var init_panel2 = __esm({
1915
1956
  "panel.license.licenseImportedNewStatusUnknownRefresh": "license \u5DF2\u5BFC\u5165(\u65B0\u72B6\u6001\u672A\u77E5,\u5237\u65B0\u672C\u9875\u67E5\u770B)",
1916
1957
  "panel.license.dropOneLicenseJsonFileAt": "\u4E00\u6B21\u62D6\u5165 1 \u4E2A license.json \u6587\u4EF6(\u6536\u5230 {p0} \u4E2A)",
1917
1958
  "panel.license.onlyJsonFilesAreAcceptedReceived": "\u53EA\u8BA4 .json \u6587\u4EF6(\u6536\u5230 {p0})",
1959
+ "panel.license.deviceActive": "\u672C\u673A\u5DF2\u6FC0\u6D3B \xB7 {count}/{max} \u53F0",
1960
+ "panel.license.deviceLimit": "\u6B64 license \u5DF2\u5728 {count} \u53F0\u8BBE\u5907\u6FC0\u6D3B",
1961
+ "panel.license.deviceNotRegistered": "\u672C\u673A\u5C1A\u672A\u6FC0\u6D3B \xB7 {count}/{max} \u53F0",
1962
+ "panel.license.deviceNotRegisteredTip": "\u6709\u8BBE\u5907\u540D\u989D\u65F6\uFF0C\u8BF7\u91CD\u65B0\u5BFC\u5165 license \u4EE5\u6FC0\u6D3B\u672C\u673A",
1963
+ "panel.license.deviceRevoked": "\u672C\u673A\u6FC0\u6D3B\u5DF2\u88AB\u64A4\u9500",
1964
+ "panel.license.deviceRevokedTip": "\u73B0\u6709\u6388\u6743\u4FDD\u7559\u81F3\u5230\u671F\uFF0C\u81EA\u52A8\u6216\u624B\u52A8\u5237\u65B0\u4E0D\u4F1A\u91CD\u65B0\u6FC0\u6D3B\u3002\u82E5\u8981\u7EE7\u7EED\u5728\u672C\u673A\u4F7F\u7528\uFF0C\u8BF7\u5728\u6709\u8BBE\u5907\u540D\u989D\u65F6\u70B9\u300C\u6FC0\u6D3B\u672C\u673A\u300D\u3002",
1965
+ "panel.license.activateDevice": "\u6FC0\u6D3B\u672C\u673A",
1966
+ "panel.license.deviceUnavailable": "\u65E0\u6CD5\u8BFB\u53D6\u672C\u673A\u8BBE\u5907\u6807\u8BC6\uFF0C\u6682\u65F6\u65E0\u6CD5\u6FC0\u6D3B",
1967
+ "panel.license.devicePending": "\u672C\u673A\u5C1A\u672A\u6FC0\u6D3B",
1968
+ "panel.license.deviceTip": "\u6BCF\u6761\u8BA2\u9605\u6700\u591A {max} \u53F0\u8BBE\u5907\uFF0C\u6388\u6743\u6BCF 24 \u5C0F\u65F6\u81EA\u52A8\u7EED\u671F\u3002\u70B9\u6B64\u67E5\u770B\u8BBE\u5907\u3002",
1969
+ "panel.license.deviceLimitTip": "\u8BF7\u5148\u5728\u5DF2\u6FC0\u6D3B\u8BBE\u5907\u4E0A\u89E3\u7ED1\u4E00\u53F0\u65E7\u8BBE\u5907\uFF0C\u518D\u5728\u672C\u673A\u91CD\u65B0\u5BFC\u5165 license\uFF1B\u672C\u673A\u5DF2\u6709\u6388\u6743\u4FDD\u7559\u81F3\u5176\u5230\u671F\u3002",
1970
+ "panel.license.deviceManageElsewhere": "\u8BF7\u5728\u5DF2\u6FC0\u6D3B\u4E14\u6388\u6743\u672A\u5230\u671F\u7684\u8BBE\u5907\u4E0A\u89E3\u7ED1",
1971
+ "panel.license.devices": "\u5DF2\u6FC0\u6D3B\u8BBE\u5907",
1972
+ "panel.license.unnamedDevice": "\u672A\u547D\u540D\u8BBE\u5907",
1973
+ "panel.license.deviceLastSeen": "\u6700\u8FD1\u7EED\u671F\uFF1A{at}",
1974
+ "panel.license.unbind": "\u89E3\u7ED1",
1975
+ "panel.license.unbindTip": "\u89E3\u7ED1\u4F1A\u64A4\u9500\u8BE5\u8BBE\u5907\u7684\u7EED\u671F\u8D44\u683C\uFF0C\u73B0\u6709\u6388\u6743\u4FDD\u7559\u81F3\u5230\u671F\uFF1B\u53EA\u6709\u5728\u90A3\u53F0\u673A\u5668\u91CD\u65B0\u5BFC\u5165 license \u6216\u70B9\u300C\u6FC0\u6D3B\u672C\u673A\u300D\u624D\u4F1A\u91CD\u65B0\u6FC0\u6D3B\u3002",
1918
1976
  "panel.live.sessionStarted": "\u4F1A\u8BDD\u5F00\u59CB",
1919
1977
  "panel.live.sessionEnded": "\u4F1A\u8BDD\u7ED3\u675F",
1920
1978
  "panel.live.promptSubmitted": "\u63D0\u4EA4\u63D0\u793A",
@@ -12582,7 +12640,7 @@ var init_json_edit = __esm({
12582
12640
  });
12583
12641
 
12584
12642
  // packages/adapter-cc/dist/toggle.js
12585
- import { createHash as createHash4 } from "node:crypto";
12643
+ import { createHash as createHash5 } from "node:crypto";
12586
12644
  import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "node:fs";
12587
12645
  import { homedir as homedir6 } from "node:os";
12588
12646
  import { basename as basename4, join as join12, resolve as resolve3 } from "node:path";
@@ -12692,7 +12750,7 @@ function isCcFeatureId(id2) {
12692
12750
  return FEATURE_KEYS.some((key) => id2 === `cc:gates:user:${key}`);
12693
12751
  }
12694
12752
  function featureBytesHash(raw) {
12695
- return raw === void 0 ? null : createHash4("sha256").update(raw).digest("hex");
12753
+ return raw === void 0 ? null : createHash5("sha256").update(raw).digest("hex");
12696
12754
  }
12697
12755
  function matchesTarget(data, key, target) {
12698
12756
  if (featureValue(data, key) !== target)
@@ -17489,6 +17547,38 @@ import { verify } from "node:crypto";
17489
17547
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
17490
17548
  import { homedir as homedir3 } from "node:os";
17491
17549
  import { join as join7 } from "node:path";
17550
+
17551
+ // apps/cli/dist/license-device.js
17552
+ import { execFileSync as execFileSync2 } from "node:child_process";
17553
+ import { createHash as createHash3 } from "node:crypto";
17554
+ import { hostname } from "node:os";
17555
+ var cachedDevice;
17556
+ function getLicenseDevice() {
17557
+ const device = cachedDevice ?? readLicenseDevice();
17558
+ if (device)
17559
+ cachedDevice = device;
17560
+ return device;
17561
+ }
17562
+ function readLicenseDevice(options = {}) {
17563
+ if ((options.platform ?? process.platform) !== "darwin")
17564
+ return null;
17565
+ try {
17566
+ const output = (options.read ?? (() => execFileSync2("/usr/sbin/ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
17567
+ encoding: "utf8",
17568
+ timeout: 2e3
17569
+ })))();
17570
+ const uuid = /"IOPlatformUUID"\s*=\s*"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/i.exec(output)?.[1];
17571
+ if (!uuid || /^0{8}-0{4}-0{4}-0{4}-0{12}$/.test(uuid))
17572
+ return null;
17573
+ const id2 = createHash3("sha256").update(uuid.toUpperCase()).digest("base64url").slice(0, 22);
17574
+ const name = (options.hostname ?? hostname()).split(".")[0];
17575
+ return { id: id2, ...name ? { name } : {} };
17576
+ } catch {
17577
+ return null;
17578
+ }
17579
+ }
17580
+
17581
+ // apps/cli/dist/license.js
17492
17582
  var OFFLINE_GRACE_DAYS = 14;
17493
17583
  var PROD_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAyU4yO0SJcflt/CV77wqVxIwCkM52CYGC6qrPGP8dMME=\n-----END PUBLIC KEY-----\n";
17494
17584
  function checkLicense(options = {}) {
@@ -17510,7 +17600,15 @@ function licenseStatus(options = {}) {
17510
17600
  if (publicKeyPem === "") {
17511
17601
  return { state: "active", detail: t("cli.licenseDevelopment") };
17512
17602
  }
17513
- return verifyEntitlement(raw, options.now ?? /* @__PURE__ */ new Date(), publicKeyPem);
17603
+ const status3 = verifyEntitlement(raw, options.now ?? /* @__PURE__ */ new Date(), publicKeyPem);
17604
+ if (status3.claims?.dev !== void 0) {
17605
+ const device = options.device === void 0 ? getLicenseDevice() : options.device;
17606
+ if (!device)
17607
+ return { state: "invalid", detail: t("cli.licenseDeviceUnavailable") };
17608
+ if (status3.claims.dev !== device.id)
17609
+ return { state: "invalid", detail: t("cli.licenseOtherDevice") };
17610
+ }
17611
+ return status3;
17514
17612
  }
17515
17613
  function verifyEntitlement(raw, now, publicKeyPem) {
17516
17614
  let parsed;
@@ -17533,10 +17631,16 @@ function verifyEntitlement(raw, now, publicKeyPem) {
17533
17631
  let claims;
17534
17632
  try {
17535
17633
  const decoded = JSON.parse(Buffer.from(parsed.token, "base64url").toString("utf8"));
17536
- if (typeof decoded.sub !== "string" || decoded.plan !== "trial" && decoded.plan !== "subscription" || typeof decoded.iat !== "string" || typeof decoded.exp !== "string" || Number.isNaN(Date.parse(decoded.exp))) {
17634
+ if (typeof decoded.sub !== "string" || decoded.plan !== "trial" && decoded.plan !== "subscription" || typeof decoded.iat !== "string" || typeof decoded.exp !== "string" || Number.isNaN(Date.parse(decoded.exp)) || decoded.dev !== void 0 && (typeof decoded.dev !== "string" || !decoded.dev)) {
17537
17635
  return { state: "invalid", detail: t("cli.licenseClaimsFields") };
17538
17636
  }
17539
- claims = { sub: decoded.sub, plan: decoded.plan, iat: decoded.iat, exp: decoded.exp };
17637
+ claims = {
17638
+ sub: decoded.sub,
17639
+ plan: decoded.plan,
17640
+ iat: decoded.iat,
17641
+ exp: decoded.exp,
17642
+ ...typeof decoded.dev === "string" ? { dev: decoded.dev } : {}
17643
+ };
17540
17644
  } catch {
17541
17645
  return { state: "invalid", detail: t("cli.licenseClaimsDecode") };
17542
17646
  }
@@ -17616,7 +17720,34 @@ import { copyFileSync, readFileSync as readFileSync4, renameSync as renameSync2,
17616
17720
  import { homedir as homedir4 } from "node:os";
17617
17721
  import { dirname as dirname6, join as join8 } from "node:path";
17618
17722
  var DEFAULT_LICENSE_SERVER = "https://api.getwingman.dev";
17723
+ function parseDevices(value) {
17724
+ if (!Array.isArray(value) || value.length > 3)
17725
+ return void 0;
17726
+ if (!value.every((item) => typeof item === "object" && item !== null && "id" in item && typeof item.id === "string" && "registeredAt" in item && typeof item.registeredAt === "string" && !Number.isNaN(Date.parse(item.registeredAt)) && "renewedAt" in item && typeof item.renewedAt === "string" && !Number.isNaN(Date.parse(item.renewedAt)) && (!("name" in item) || typeof item.name === "string")))
17727
+ return void 0;
17728
+ return value.map((item) => ({
17729
+ id: item.id,
17730
+ registeredAt: item.registeredAt,
17731
+ renewedAt: item.renewedAt,
17732
+ ...item.name ? { name: item.name } : {}
17733
+ }));
17734
+ }
17735
+ function licenseNetworkAllowed(options, baseUrl) {
17736
+ if (!options.offline || options.fetchFn)
17737
+ return true;
17738
+ try {
17739
+ return ["127.0.0.1", "localhost", "[::1]"].includes(new URL(baseUrl).hostname);
17740
+ } catch {
17741
+ return false;
17742
+ }
17743
+ }
17619
17744
  async function refreshLicense(options = {}) {
17745
+ return renewLicense(options, "refresh");
17746
+ }
17747
+ async function activateLicense(options) {
17748
+ return renewLicense(options, "activate");
17749
+ }
17750
+ async function renewLicense(options, action) {
17620
17751
  const home = options.homeDir ?? homedir4();
17621
17752
  const baseUrl = options.baseUrl ?? process.env.WINGMAN_LICENSE_SERVER ?? DEFAULT_LICENSE_SERVER;
17622
17753
  const licensePath = join8(home, ".wingman", "license.json");
@@ -17630,12 +17761,33 @@ async function refreshLicense(options = {}) {
17630
17761
  reason: "missing"
17631
17762
  };
17632
17763
  }
17764
+ if (!licenseNetworkAllowed(options, baseUrl)) {
17765
+ return { ok: false, message: t("cli.licenseFixturesOffline"), reason: "offline" };
17766
+ }
17767
+ const publicKeyPem = options.publicKeyPem ?? PROD_PUBLIC_KEY_PEM;
17768
+ if (publicKeyPem !== "") {
17769
+ const status3 = verifyEntitlement(raw, options.now ?? /* @__PURE__ */ new Date(), publicKeyPem);
17770
+ if (status3.state === "invalid")
17771
+ return { ok: false, message: t("cli.licenseSignature"), reason: "invalid-license" };
17772
+ if (action === "activate" && status3.state !== "active")
17773
+ return { ok: false, message: t("cli.licenseActivationExpired"), reason: "invalid-license" };
17774
+ }
17775
+ const device = options.device === void 0 ? getLicenseDevice() : options.device;
17776
+ if (!device)
17777
+ return { ok: false, message: t("cli.licenseDeviceUnavailable"), reason: "device-unavailable" };
17778
+ let requestBody;
17779
+ try {
17780
+ requestBody = JSON.stringify(action === "activate" ? { license: JSON.parse(raw), action, ...device } : { ...JSON.parse(raw), device: { id: device.id } });
17781
+ } catch {
17782
+ return { ok: false, message: t("cli.licenseJson"), reason: "invalid-license" };
17783
+ }
17633
17784
  let response;
17634
17785
  try {
17635
- response = await fetch(`${baseUrl}/refresh`, {
17786
+ response = await (options.fetchFn ?? fetch)(`${baseUrl}/${action === "activate" ? "devices" : "refresh"}`, {
17636
17787
  method: "POST",
17637
17788
  headers: { "content-type": "application/json" },
17638
- body: raw,
17789
+ body: requestBody,
17790
+ redirect: "error",
17639
17791
  signal: AbortSignal.timeout(15e3)
17640
17792
  });
17641
17793
  } catch {
@@ -17650,6 +17802,25 @@ async function refreshLicense(options = {}) {
17650
17802
  let inactive = false;
17651
17803
  try {
17652
17804
  const body = await response.json();
17805
+ if (response.status === 409 && body.code === "device_limit") {
17806
+ const devices2 = parseDevices(body.devices);
17807
+ if (devices2)
17808
+ return {
17809
+ ok: false,
17810
+ message: t("cli.licenseDeviceLimit"),
17811
+ reason: "device_limit",
17812
+ devices: devices2
17813
+ };
17814
+ }
17815
+ if (response.status === 403 && body.code === "device_revoked") {
17816
+ const devices2 = parseDevices(body.devices);
17817
+ return {
17818
+ ok: false,
17819
+ message: t("cli.licenseDeviceRevoked"),
17820
+ reason: "device_revoked",
17821
+ ...devices2 ? { devices: devices2 } : {}
17822
+ };
17823
+ }
17653
17824
  if (typeof body.error === "string")
17654
17825
  detail = body.error;
17655
17826
  inactive = body.reason === "inactive";
@@ -17680,13 +17851,14 @@ async function refreshLicense(options = {}) {
17680
17851
  }
17681
17852
  const next = `${JSON.stringify({ token: entitlement.token, signature: entitlement.signature })}
17682
17853
  `;
17683
- const publicKeyPem = options.publicKeyPem ?? PROD_PUBLIC_KEY_PEM;
17684
17854
  if (publicKeyPem !== "") {
17685
17855
  const status3 = verifyEntitlement(next, options.now ?? /* @__PURE__ */ new Date(), publicKeyPem);
17686
- if (status3.state !== "active" && status3.state !== "grace") {
17856
+ if (status3.state !== "active" && status3.state !== "grace" || status3.claims?.dev !== device.id) {
17687
17857
  return {
17688
17858
  ok: false,
17689
- message: t("cli.refreshInvalid", { detail: status3.detail }),
17859
+ message: t("cli.refreshInvalid", {
17860
+ detail: status3.claims?.dev !== device.id ? t("cli.licenseOtherDevice") : status3.detail
17861
+ }),
17690
17862
  reason: "invalid-response"
17691
17863
  };
17692
17864
  }
@@ -17719,7 +17891,70 @@ async function refreshLicense(options = {}) {
17719
17891
  reason: "write"
17720
17892
  };
17721
17893
  }
17722
- return { ok: true, message: t("cli.licenseRenewed", { suffix: expSuffix(entitlement.token) }) };
17894
+ const devices = parseDevices(entitlement.devices);
17895
+ return {
17896
+ ok: true,
17897
+ message: t("cli.licenseRenewed", { suffix: expSuffix(entitlement.token) }),
17898
+ ...devices ? { devices } : {}
17899
+ };
17900
+ }
17901
+ async function manageLicenseDevices(options, action, id2) {
17902
+ const baseUrl = options.baseUrl ?? process.env.WINGMAN_LICENSE_SERVER ?? DEFAULT_LICENSE_SERVER;
17903
+ if (!licenseNetworkAllowed(options, baseUrl))
17904
+ return { ok: false, message: t("cli.licenseFixturesOffline"), reason: "offline" };
17905
+ let raw;
17906
+ try {
17907
+ raw = readFileSync4(join8(options.homeDir ?? homedir4(), ".wingman", "license.json"), "utf8");
17908
+ } catch {
17909
+ return { ok: false, message: t("cli.licenseMissing"), reason: "missing" };
17910
+ }
17911
+ const key = options.publicKeyPem ?? PROD_PUBLIC_KEY_PEM;
17912
+ const status3 = key === "" ? void 0 : verifyEntitlement(raw, options.now ?? /* @__PURE__ */ new Date(), key);
17913
+ if (status3?.state === "invalid")
17914
+ return { ok: false, message: t("cli.licenseSignature"), reason: "invalid-license" };
17915
+ let callerDeviceId;
17916
+ if (action === "unbind") {
17917
+ const device = options.device === void 0 ? getLicenseDevice() : options.device;
17918
+ if (!device)
17919
+ return {
17920
+ ok: false,
17921
+ message: t("cli.licenseDeviceUnavailable"),
17922
+ reason: "device-unavailable"
17923
+ };
17924
+ if (status3?.state !== "active" || status3.claims?.dev !== device.id)
17925
+ return {
17926
+ ok: false,
17927
+ message: t("cli.licenseUnbindRequiresActiveDevice"),
17928
+ reason: "invalid-license"
17929
+ };
17930
+ callerDeviceId = device.id;
17931
+ }
17932
+ try {
17933
+ const response = await (options.fetchFn ?? fetch)(`${baseUrl}/devices`, {
17934
+ method: "POST",
17935
+ headers: { "content-type": "application/json" },
17936
+ body: JSON.stringify({
17937
+ license: JSON.parse(raw),
17938
+ action,
17939
+ ...id2 ? { id: id2 } : {},
17940
+ ...callerDeviceId === void 0 ? {} : { device: { id: callerDeviceId } }
17941
+ }),
17942
+ signal: AbortSignal.timeout(15e3),
17943
+ redirect: "error"
17944
+ });
17945
+ if (!response.ok)
17946
+ return {
17947
+ ok: false,
17948
+ message: response.status === 403 && action === "unbind" ? t("cli.licenseUnbindRequiresActiveDevice") : t("cli.refreshHttp", { status: response.status }),
17949
+ reason: response.status === 403 ? "invalid-license" : "server"
17950
+ };
17951
+ const devices = parseDevices((await response.json()).devices);
17952
+ if (!devices)
17953
+ return { ok: false, message: t("cli.licenseDevicesInvalid"), reason: "invalid-response" };
17954
+ return { ok: true, message: "", devices };
17955
+ } catch {
17956
+ return { ok: false, message: t("cli.refreshNetwork"), reason: "network" };
17957
+ }
17723
17958
  }
17724
17959
  function expSuffix(token) {
17725
17960
  try {
@@ -17949,9 +18184,9 @@ function runSecurity(args) {
17949
18184
  // apps/cli/dist/mobile/snapshot.js
17950
18185
  init_dist2();
17951
18186
  init_dist();
17952
- import { createHash as createHash3 } from "node:crypto";
18187
+ import { createHash as createHash4 } from "node:crypto";
17953
18188
  function mobileId(value) {
17954
- return createHash3("sha256").update(value).digest("hex");
18189
+ return createHash4("sha256").update(value).digest("hex");
17955
18190
  }
17956
18191
  function buildMobileSnapshot(narrative, at, wired) {
17957
18192
  const sources = narrative.getProjects().projects;
@@ -18194,6 +18429,7 @@ function writeMobileState(homeDir, state) {
18194
18429
  }
18195
18430
 
18196
18431
  // apps/cli/dist/mobile/service.js
18432
+ var PAIRING_WINDOW_MS = 5 * 6e4;
18197
18433
  function createMobileService(options) {
18198
18434
  const keychain = options.keychain ?? createMacKeychain();
18199
18435
  const fetchRelay = options.fetch ?? globalThis.fetch;
@@ -18678,7 +18914,7 @@ function createMobileService(options) {
18678
18914
  throw new Error("mobile.relayUnavailable");
18679
18915
  created = true;
18680
18916
  persist((current) => [...current, pair]);
18681
- pairs.set(pairId, { ...runtime(pair), pairingUntil: now().getTime() + 1e4 });
18917
+ pairs.set(pairId, { ...runtime(pair), pairingUntil: now().getTime() + PAIRING_WINDOW_MS });
18682
18918
  stateError = void 0;
18683
18919
  return { pairId, url: pairingUrl({ relay, pairId, deviceKey }) };
18684
18920
  } catch {
@@ -18702,16 +18938,15 @@ function createMobileService(options) {
18702
18938
  throw new Error(created ? "mobile.localWriteFailed" : "mobile.relayUnavailable");
18703
18939
  }
18704
18940
  },
18705
- async list(pairing) {
18706
- const entry = pairing ? pairs.get(pairing) : void 0;
18707
- if (entry?.status === "pending")
18708
- entry.pairingUntil = now().getTime() + 1e4;
18941
+ async list() {
18709
18942
  await refresh();
18710
- const error = stateError ?? [...pairs.values()].find((entry2) => entry2.error)?.error;
18943
+ const at = now().getTime();
18944
+ const error = stateError ?? [...pairs.values()].find((entry) => entry.error)?.error;
18711
18945
  return {
18712
- pairs: [...pairs.values()].map((entry2) => ({
18713
- ...entry2.pair,
18714
- status: entry2.status,
18946
+ pairs: [...pairs.values()].map((entry) => ({
18947
+ ...entry.pair,
18948
+ status: entry.status,
18949
+ ...entry.status === "pending" && at >= (entry.pairingUntil ?? 0) ? { expired: true } : {},
18715
18950
  ...!options.licensed() ? { paused: true } : {}
18716
18951
  })),
18717
18952
  ...error ? { error } : {}
@@ -20977,7 +21212,7 @@ function resolveWingmanVersion({ defined, moduleUrl = import.meta.url, readPacka
20977
21212
  return void 0;
20978
21213
  }
20979
21214
  function wingmanVersion() {
20980
- const defined = false ? void 0 : "0.9.7";
21215
+ const defined = false ? void 0 : "0.9.8";
20981
21216
  return resolveWingmanVersion({
20982
21217
  defined,
20983
21218
  readPackage: (url) => JSON.parse(readFileSync18(url, "utf8"))
@@ -22766,12 +23001,12 @@ function humanFeedbackState(harness2, session2) {
22766
23001
  return state;
22767
23002
  return state.state === "needs_user" ? { ...state, since: at } : { ...state, requestSince: at };
22768
23003
  }
22769
- function humanWaiting(harness2, lines2, wait, at = wait.since) {
22770
- if (harness2 !== "codex" || wait.kind !== "permission")
22771
- return wait;
23004
+ function humanWaiting(harness2, lines2, wait2, at = wait2.since) {
23005
+ if (harness2 !== "codex" || wait2.kind !== "permission")
23006
+ return wait2;
22772
23007
  if (codexAutomaticReviewAt(lines2, at))
22773
23008
  return null;
22774
- return codexAutomaticReviewAt(lines2, wait.since) ? { ...wait, since: at } : wait;
23009
+ return codexAutomaticReviewAt(lines2, wait2.since) ? { ...wait2, since: at } : wait2;
22775
23010
  }
22776
23011
  function observedMcpServers(lines2, configuredServers) {
22777
23012
  const names = /* @__PURE__ */ new Set();
@@ -22837,7 +23072,7 @@ function observedSkills(lines2) {
22837
23072
  function derive(harness2, session2, nowIso, configuredPlugins, configuredMcpServers) {
22838
23073
  const codexLines = sortedCodexByTimestamp(session2.codexLines);
22839
23074
  const waiting2 = session2.waiting === null ? null : humanWaiting(harness2, codexLines, session2.waiting, session2.feedbackRequestAt);
22840
- const waitEpisodes = session2.waitEpisodes.map((wait) => humanWaiting(harness2, codexLines, wait, wait.requestAt)).filter((wait) => wait !== null);
23075
+ const waitEpisodes = session2.waitEpisodes.map((wait2) => humanWaiting(harness2, codexLines, wait2, wait2.requestAt)).filter((wait2) => wait2 !== null);
22841
23076
  const lines2 = harness2 === "codex" ? adaptCodexNarrativeLines(codexLines) : sortedByTimestamp(session2.lines);
22842
23077
  const timeline = deriveTodoTimeline(extractTodoSnapshots(lines2));
22843
23078
  const allSteps = extractSteps(lines2);
@@ -24082,9 +24317,132 @@ import { execFile as execFile3 } from "node:child_process";
24082
24317
  import { createHmac, timingSafeEqual } from "node:crypto";
24083
24318
  import { readFile as readFile4 } from "node:fs/promises";
24084
24319
  import { createServer } from "node:http";
24085
- import { hostname } from "node:os";
24320
+ import { hostname as hostname2 } from "node:os";
24086
24321
  import { extname, isAbsolute as isAbsolute7, relative as relative5, resolve as resolve14, sep as sep2 } from "node:path";
24087
24322
 
24323
+ // apps/cli/dist/license-activation.js
24324
+ import { setTimeout as wait } from "node:timers/promises";
24325
+ function createLicenseActivation(options) {
24326
+ let activation = { state: "pending", devices: [], maxDevices: 3 };
24327
+ let queue = Promise.resolve();
24328
+ const serial = (work) => {
24329
+ const next = queue.then(work, work);
24330
+ queue = next.catch(() => {
24331
+ });
24332
+ return next;
24333
+ };
24334
+ const statusOptions = () => options;
24335
+ const status3 = () => {
24336
+ const current = licenseStatus(statusOptions());
24337
+ return {
24338
+ state: current.state,
24339
+ detail: current.detail,
24340
+ ...current.claims ? { plan: current.claims.plan, exp: current.claims.exp } : {},
24341
+ tier: checkLicense(statusOptions()) ? "full" : "free",
24342
+ ...current.state === "none" || activation.state === "pending" && current.claims?.dev ? {} : { activation }
24343
+ };
24344
+ };
24345
+ const refresh = async (explicitActivation = false) => {
24346
+ const result5 = await (explicitActivation ? activateLicense : refreshLicense)(options);
24347
+ if (result5.reason === "device_limit")
24348
+ activation = {
24349
+ state: activation.state === "device_revoked" ? "device_revoked" : "device_limit",
24350
+ devices: result5.devices ?? [],
24351
+ maxDevices: 3,
24352
+ detail: result5.message
24353
+ };
24354
+ else if (result5.reason === "device_revoked")
24355
+ activation = {
24356
+ state: "device_revoked",
24357
+ devices: result5.devices ?? activation.devices,
24358
+ maxDevices: 3,
24359
+ detail: result5.message
24360
+ };
24361
+ else if (result5.reason === "device-unavailable")
24362
+ activation = { state: "unavailable", devices: [], maxDevices: 3, detail: result5.message };
24363
+ else if (result5.ok) {
24364
+ const current = licenseStatus(statusOptions());
24365
+ activation = {
24366
+ state: current.claims?.dev && result5.devices ? "active" : "pending",
24367
+ devices: result5.devices ?? [],
24368
+ maxDevices: 3
24369
+ };
24370
+ }
24371
+ return result5;
24372
+ };
24373
+ return {
24374
+ status: status3,
24375
+ refresh: () => serial(() => refresh()),
24376
+ autoRefresh: () => serial(async () => {
24377
+ if ((options.publicKeyPem ?? PROD_PUBLIC_KEY_PEM) === "")
24378
+ return;
24379
+ const result5 = await refresh();
24380
+ return result5.ok || result5.reason === "missing" ? void 0 : result5.reason;
24381
+ }),
24382
+ install: (raw) => serial(async () => {
24383
+ const result5 = installLicense(raw, statusOptions());
24384
+ if (!result5.ok)
24385
+ return { ok: false, message: result5.error };
24386
+ activation = { state: "pending", devices: [], maxDevices: 3 };
24387
+ if ((options.publicKeyPem ?? PROD_PUBLIC_KEY_PEM) !== "")
24388
+ await refresh(true);
24389
+ return { ok: true, message: "" };
24390
+ }),
24391
+ devices: (action, id2) => serial(async () => {
24392
+ if (action === "activate")
24393
+ return refresh(true);
24394
+ const result5 = await manageLicenseDevices(options, action, id2);
24395
+ if (!result5.ok)
24396
+ return result5;
24397
+ activation = { ...activation, devices: result5.devices ?? [] };
24398
+ if (action === "unbind") {
24399
+ await (options.wait ?? wait)(1e3);
24400
+ return refresh();
24401
+ }
24402
+ const current = licenseStatus(statusOptions());
24403
+ if (activation.state !== "device_revoked" && current.claims?.dev && (current.state === "active" || current.state === "grace")) {
24404
+ const devices = result5.devices ?? [];
24405
+ activation = {
24406
+ state: devices.some((device) => device.id === current.claims?.dev) ? "active" : "device_limit",
24407
+ devices,
24408
+ maxDevices: 3
24409
+ };
24410
+ }
24411
+ return result5;
24412
+ })
24413
+ };
24414
+ }
24415
+
24416
+ // apps/cli/dist/license-auto-refresh.js
24417
+ function startLicenseAutoRefresh(options) {
24418
+ const clock = options.clock ?? { setTimeout, clearTimeout, setInterval, clearInterval };
24419
+ let running = false;
24420
+ const run = async () => {
24421
+ if (running)
24422
+ return;
24423
+ running = true;
24424
+ try {
24425
+ await options.refresh();
24426
+ } catch {
24427
+ options.warn?.("license automatic refresh failed");
24428
+ } finally {
24429
+ running = false;
24430
+ }
24431
+ };
24432
+ let interval;
24433
+ const timeout = clock.setTimeout(() => {
24434
+ void run();
24435
+ interval = clock.setInterval(() => void run(), 24 * 60 * 60 * 1e3);
24436
+ interval.unref?.();
24437
+ }, 5e3);
24438
+ timeout.unref?.();
24439
+ return () => {
24440
+ clock.clearTimeout(timeout);
24441
+ if (interval !== void 0)
24442
+ clock.clearInterval(interval);
24443
+ };
24444
+ }
24445
+
24088
24446
  // apps/cli/dist/suggest.js
24089
24447
  init_dist2();
24090
24448
  init_dist();
@@ -24271,10 +24629,21 @@ var PANEL_ACTION_HEADER = "x-wingman-panel-action";
24271
24629
  var LOCAL_HOST = /^(?:127\.0\.0\.1|localhost)(?::\d+)?$|^\[::1\](?::\d+)?$/i;
24272
24630
  function createWingmanServer(options) {
24273
24631
  const warn = options.warn ?? ((message) => console.error(message));
24274
- const runtime = { anatomy: null };
24632
+ const licenseOptions = {
24633
+ ...options.homeDir === void 0 ? {} : { homeDir: options.homeDir },
24634
+ ...options.licenseOptions,
24635
+ ...options.licenseRefreshOptions,
24636
+ ...options.readonlyActions && options.licenseOptions?.device === void 0 && options.licenseRefreshOptions?.device === void 0 ? { device: null } : {},
24637
+ offline: options.readonlyActions === true || options.licenseRefreshOptions?.offline === true
24638
+ };
24639
+ options = { ...options, licenseOptions };
24640
+ const runtime = {
24641
+ anatomy: null,
24642
+ license: createLicenseActivation(licenseOptions)
24643
+ };
24275
24644
  options.narrative?.setCodexPluginsProvider(() => runtime.anatomy?.configs.find((config) => config.harness === "codex")?.slots.skills.filter((item) => item.nativeName.startsWith("plugins.")).map((item) => item.nativeName.slice("plugins.".length)) ?? []);
24276
24645
  options.narrative?.setCcMcpServersProvider(() => runtime.anatomy?.configs.find((config) => config.harness === "cc")?.slots.tools.filter((item) => item.nativeName.startsWith("mcpServers.")).map((item) => item.nativeName.slice("mcpServers.".length)) ?? []);
24277
- return createServer({ requireHostHeader: false }, (req, res) => {
24646
+ const server = createServer({ requireHostHeader: false }, (req, res) => {
24278
24647
  handle(req, res, options, warn, runtime).catch((error) => {
24279
24648
  warn(t("cli.server.requestFailed", { error: errorMessage3(error) }));
24280
24649
  if (!res.headersSent)
@@ -24283,6 +24652,21 @@ function createWingmanServer(options) {
24283
24652
  res.end();
24284
24653
  });
24285
24654
  });
24655
+ if (options.licenseAutoRefresh) {
24656
+ server.once("listening", () => {
24657
+ const stop = startLicenseAutoRefresh({
24658
+ ...options.licenseAutoRefresh,
24659
+ refresh: async () => {
24660
+ const reason = await runtime.license.autoRefresh();
24661
+ if (reason)
24662
+ warn(`license automatic refresh: ${reason}`);
24663
+ },
24664
+ warn
24665
+ });
24666
+ server.once("close", stop);
24667
+ });
24668
+ }
24669
+ return server;
24286
24670
  }
24287
24671
  function listen(server, port) {
24288
24672
  return new Promise((resolvePort, reject) => {
@@ -24382,7 +24766,7 @@ async function handle(req, res, options, warn, runtime) {
24382
24766
  return;
24383
24767
  }
24384
24768
  if (url.pathname === "/api/license/install" && req.method === "POST") {
24385
- await handleLicenseInstall(req, res, options);
24769
+ await handleLicenseInstall(req, res, options, runtime);
24386
24770
  return;
24387
24771
  }
24388
24772
  if (url.pathname === "/api/license/refresh") {
@@ -24394,7 +24778,36 @@ async function handle(req, res, options, warn, runtime) {
24394
24778
  sendJson2(res, 403, { error: t("cli.server.refreshFromPanel") });
24395
24779
  return;
24396
24780
  }
24397
- await handleLicenseRefresh(res, options);
24781
+ await handleLicenseRefresh(res, options, runtime);
24782
+ return;
24783
+ }
24784
+ if (url.pathname === "/api/license/devices") {
24785
+ if (req.method !== "POST") {
24786
+ sendText2(res, 405, "method not allowed");
24787
+ return;
24788
+ }
24789
+ if (req.headers[PANEL_ACTION_HEADER] !== "license-devices") {
24790
+ sendJson2(res, 403, { error: t("cli.server.refreshFromPanel") });
24791
+ return;
24792
+ }
24793
+ const raw = await readTextBody(req, LICENSE_BODY_LIMIT);
24794
+ let body;
24795
+ try {
24796
+ body = JSON.parse(raw ?? "");
24797
+ } catch {
24798
+ sendJson2(res, 400, { error: "invalid device action" });
24799
+ return;
24800
+ }
24801
+ if (!body || body.action !== "list" && body.action !== "unbind" && body.action !== "activate" || body.action === "unbind" && (typeof body.id !== "string" || !body.id)) {
24802
+ sendJson2(res, 400, { error: "invalid device action" });
24803
+ return;
24804
+ }
24805
+ const result5 = await runtime.license.devices(body.action, typeof body.id === "string" ? body.id : void 0);
24806
+ sendJson2(res, result5.ok ? 200 : result5.reason === "device_limit" ? 409 : result5.reason === "device_revoked" || result5.reason === "invalid-license" ? 403 : 400, {
24807
+ ...licenseApiStatus(options, runtime),
24808
+ ...result5.ok ? {} : { error: result5.message },
24809
+ ...result5.reason === "device_revoked" ? { code: result5.reason } : {}
24810
+ });
24398
24811
  return;
24399
24812
  }
24400
24813
  if (req.method !== "GET") {
@@ -24402,19 +24815,7 @@ async function handle(req, res, options, warn, runtime) {
24402
24815
  return;
24403
24816
  }
24404
24817
  if (url.pathname === "/api/license") {
24405
- const body = publicLicenseStatus(licenseStatus(options.licenseOptions));
24406
- body.tier = checkLicense(options.licenseOptions) ? "full" : "free";
24407
- if (body.tier === "free") {
24408
- const cached = runtime.anatomy?.configs;
24409
- const detected = new Set((cached ?? []).map((config) => config.harness));
24410
- const homeDir = options.homeDir ?? options.licenseOptions?.homeDir;
24411
- const settings = homeDir === void 0 ? void 0 : readReturnTargetSettings(homeDir);
24412
- const selected = settings && !("unavailable" in settings) ? settings.freeHarness : void 0;
24413
- body.freeHarness = selected !== void 0 && (cached === void 0 || detected.has(selected)) ? selected : HARNESS_IDS.find((harness2) => detected.has(harness2)) ?? HARNESS_IDS[0];
24414
- if (settings && !("unavailable" in settings) && settings.freeHarnessChosenAt !== void 0) {
24415
- body.freeHarnessSwitchableAt = new Date(Date.parse(settings.freeHarnessChosenAt) + FREE_HARNESS_SWITCH_COOLDOWN_MS).toISOString();
24416
- }
24417
- }
24818
+ const body = licenseApiStatus(options, runtime);
24418
24819
  sendJson2(res, 200, body);
24419
24820
  return;
24420
24821
  }
@@ -24709,7 +25110,7 @@ function handleProtocolUserAction(body, res, options, runtime) {
24709
25110
  ...install && before.version !== void 0 && after.version !== void 0 && before.version < after.version ? { fromVersion: before.version, toVersion: after.version } : {},
24710
25111
  blockBytes: Buffer.byteLength(block ?? ""),
24711
25112
  backupDir: t("cli.server.backupPreview"),
24712
- hostname: hostname()
25113
+ hostname: hostname2()
24713
25114
  });
24714
25115
  } else {
24715
25116
  runtime.anatomy = null;
@@ -24889,56 +25290,45 @@ async function handleTaskOverlay(req, res, options) {
24889
25290
  sendJson2(res, 200, { key, entry: result5.entry });
24890
25291
  }
24891
25292
  var LICENSE_BODY_LIMIT = 64 * 1024;
24892
- async function handleLicenseInstall(req, res, options) {
25293
+ function licenseApiStatus(options, runtime) {
25294
+ const body = runtime.license.status();
25295
+ if (body.tier === "free") {
25296
+ const cached = runtime.anatomy?.configs;
25297
+ const detected = new Set((cached ?? []).map((config) => config.harness));
25298
+ const homeDir = options.homeDir ?? options.licenseOptions?.homeDir;
25299
+ const settings = homeDir === void 0 ? void 0 : readReturnTargetSettings(homeDir);
25300
+ const selected = settings && !("unavailable" in settings) ? settings.freeHarness : void 0;
25301
+ body.freeHarness = selected !== void 0 && (cached === void 0 || detected.has(selected)) ? selected : HARNESS_IDS.find((harness2) => detected.has(harness2)) ?? HARNESS_IDS[0];
25302
+ if (settings && !("unavailable" in settings) && settings.freeHarnessChosenAt !== void 0) {
25303
+ body.freeHarnessSwitchableAt = new Date(Date.parse(settings.freeHarnessChosenAt) + FREE_HARNESS_SWITCH_COOLDOWN_MS).toISOString();
25304
+ }
25305
+ }
25306
+ return body;
25307
+ }
25308
+ async function handleLicenseInstall(req, res, options, runtime) {
24893
25309
  const raw = await readTextBody(req, LICENSE_BODY_LIMIT);
24894
25310
  if (raw === void 0) {
24895
25311
  sendJson2(res, 400, { error: t("cli.server.licenseRead") });
24896
25312
  return;
24897
25313
  }
24898
- const result5 = installLicense(raw, options.licenseOptions);
25314
+ const result5 = await runtime.license.install(raw);
24899
25315
  if (!result5.ok) {
24900
- sendJson2(res, 400, { error: result5.error });
25316
+ sendJson2(res, 400, { error: result5.message });
24901
25317
  return;
24902
25318
  }
24903
- sendJson2(res, 200, { ...publicLicenseStatus(result5.status), tier: "full" });
25319
+ sendJson2(res, 200, licenseApiStatus(options, runtime));
24904
25320
  }
24905
- async function handleLicenseRefresh(res, options) {
24906
- const homeDir = options.licenseRefreshOptions?.homeDir ?? options.licenseOptions?.homeDir;
24907
- const publicKeyPem = options.licenseRefreshOptions?.publicKeyPem ?? options.licenseOptions?.publicKeyPem;
24908
- const now = options.licenseRefreshOptions?.now ?? options.licenseOptions?.now;
24909
- const refreshOptions = {
24910
- ...options.licenseRefreshOptions,
24911
- ...homeDir === void 0 ? {} : { homeDir },
24912
- ...publicKeyPem === void 0 ? {} : { publicKeyPem },
24913
- ...now === void 0 ? {} : { now }
24914
- };
24915
- const result5 = await refreshLicense(refreshOptions);
25321
+ async function handleLicenseRefresh(res, options, runtime) {
25322
+ const result5 = await runtime.license.refresh();
24916
25323
  if (!result5.ok) {
24917
- sendJson2(res, 400, {
25324
+ sendJson2(res, result5.reason === "device_limit" ? 409 : result5.reason === "device_revoked" ? 403 : 400, {
24918
25325
  error: result5.message,
24919
- canSubscribe: result5.reason === "inactive"
25326
+ canSubscribe: result5.reason === "inactive",
25327
+ ...result5.reason === "device_revoked" ? { code: result5.reason } : {}
24920
25328
  });
24921
25329
  return;
24922
25330
  }
24923
- const statusOptions = {
24924
- ...options.licenseOptions,
24925
- ...homeDir === void 0 ? {} : { homeDir },
24926
- ...publicKeyPem === void 0 ? {} : { publicKeyPem },
24927
- ...now === void 0 ? {} : { now }
24928
- };
24929
- sendJson2(res, 200, {
24930
- ...publicLicenseStatus(licenseStatus(statusOptions)),
24931
- tier: "full",
24932
- message: result5.message
24933
- });
24934
- }
24935
- function publicLicenseStatus(status3) {
24936
- const body = { state: status3.state, detail: status3.detail };
24937
- if (status3.claims) {
24938
- body.plan = status3.claims.plan;
24939
- body.exp = status3.claims.exp;
24940
- }
24941
- return body;
25331
+ sendJson2(res, 200, { ...licenseApiStatus(options, runtime), message: result5.message });
24942
25332
  }
24943
25333
  function readTextBody(req, maxBytes) {
24944
25334
  return new Promise((resolveBody) => {
@@ -25161,7 +25551,7 @@ async function main(argv) {
25161
25551
  return 0;
25162
25552
  }
25163
25553
  if (rest[0] === "refresh") {
25164
- const result5 = await refreshLicense({ homeDir });
25554
+ const result5 = await refreshLicense({ homeDir, offline: argv.includes("--fixtures") });
25165
25555
  process.stdout.write(`${result5.message}
25166
25556
  `);
25167
25557
  if (!result5.ok)
@@ -25271,7 +25661,10 @@ async function runServe(args) {
25271
25661
  receiver: createReceiver({ onEvent: narrative.handleEvent }),
25272
25662
  readonlyActions: fixtures,
25273
25663
  homeDir,
25664
+ // fixtures 只替换扫描根;授权仍使用进程 HOME,测试由子进程环境注入临时目录。
25665
+ licenseOptions: { homeDir: homedir17() },
25274
25666
  narrative,
25667
+ licenseAutoRefresh: {},
25275
25668
  ...mobile === void 0 ? {} : { mobile },
25276
25669
  hookToken,
25277
25670
  ...locale === void 0 ? {} : { localeArg: locale },