dsh-workbuddy-connect 0.6.2 → 0.6.4
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/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/bin.js +5 -7
- package/lib/client.js +845 -38
- package/lib/{variants-B3Pa4EBr.js → host-heartbeat-D9DvyLWz.js} +459 -133
- package/lib/index.d.ts +212 -142
- package/lib/index.js +313 -8
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
|
5
5
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
6
6
|
import { createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
7
7
|
import { execFile, execFileSync } from "node:child_process";
|
|
8
|
-
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
8
|
+
import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
9
|
//#region src/app-version.ts
|
|
10
10
|
/**
|
|
11
11
|
* The international desktop app's version, used as the `/v3/config` UA.
|
|
@@ -18,9 +18,10 @@ import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
|
18
18
|
* removed — returns the 21-model App document. The UA is therefore built from
|
|
19
19
|
* the form verified in code, not from the earlier prose.
|
|
20
20
|
*
|
|
21
|
-
* The version
|
|
22
|
-
* plist, or a bad cached value
|
|
23
|
-
* a compiled-in constant, and
|
|
21
|
+
* The version feeds the international catalog UA and the chat identity's
|
|
22
|
+
* `X-IDE-Version`: a missing App, an unreadable plist, or a bad cached value
|
|
23
|
+
* degrades to the last saved value and finally to a compiled-in constant, and
|
|
24
|
+
* never blocks credential use or the provider.
|
|
24
25
|
*
|
|
25
26
|
* @module dsh-workbuddy-connect/app-version
|
|
26
27
|
*/
|
|
@@ -628,6 +629,7 @@ const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
|
|
|
628
629
|
/** Classify an upstream failure from its HTTP status and body excerpt. */
|
|
629
630
|
function classifyUpstreamError(status, body) {
|
|
630
631
|
if (status === 402) return "hard_credit";
|
|
632
|
+
if (status === 401) return "session_dead";
|
|
631
633
|
const lower = body.toLowerCase();
|
|
632
634
|
for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
|
|
633
635
|
for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
|
|
@@ -637,6 +639,26 @@ function classifyUpstreamError(status, body) {
|
|
|
637
639
|
if (status >= 400) return "client";
|
|
638
640
|
return "client";
|
|
639
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Extract a user-facing error message from an upstream JSON response body.
|
|
644
|
+
* Prefers WorkBuddy's localized `displayMsg.zh` / `displayMsg.en`, falling back to `msg`.
|
|
645
|
+
*/
|
|
646
|
+
function extractDisplayErrorMessage(body) {
|
|
647
|
+
const trimmed = body.trim();
|
|
648
|
+
if (!trimmed.startsWith("{")) return void 0;
|
|
649
|
+
try {
|
|
650
|
+
const parsed = JSON.parse(trimmed);
|
|
651
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
652
|
+
const obj = parsed;
|
|
653
|
+
const displayMsg = obj["displayMsg"];
|
|
654
|
+
if (typeof displayMsg === "object" && displayMsg !== null && !Array.isArray(displayMsg)) {
|
|
655
|
+
const localized = displayMsg;
|
|
656
|
+
if (typeof localized["zh"] === "string" && localized["zh"].trim() !== "") return localized["zh"].trim();
|
|
657
|
+
if (typeof localized["en"] === "string" && localized["en"].trim() !== "") return localized["en"].trim();
|
|
658
|
+
}
|
|
659
|
+
if (typeof obj["msg"] === "string" && obj["msg"].trim() !== "") return obj["msg"].trim();
|
|
660
|
+
} catch {}
|
|
661
|
+
}
|
|
640
662
|
/** Region for a login domain; an empty domain means CN (matching upstream tooling). */
|
|
641
663
|
function regionOf(domain) {
|
|
642
664
|
const lowered = domain.trim().toLowerCase();
|
|
@@ -665,18 +687,21 @@ function commonHeaders(credential) {
|
|
|
665
687
|
/**
|
|
666
688
|
* Chat request headers, including the X-No-* conventions the official CLI uses.
|
|
667
689
|
*
|
|
668
|
-
* `userAgent`
|
|
669
|
-
*
|
|
670
|
-
*
|
|
690
|
+
* `userAgent` and `clientVersion` carry the same resolved desktop identity for
|
|
691
|
+
* chat and probe requests. Refresh shares `commonHeaders` but never this
|
|
692
|
+
* override, so the two paths cannot drift into each other.
|
|
671
693
|
*/
|
|
672
|
-
function chatHeaders(credential, userAgent) {
|
|
694
|
+
function chatHeaders(credential, userAgent, clientVersion) {
|
|
673
695
|
return {
|
|
674
696
|
...commonHeaders(credential),
|
|
675
|
-
|
|
697
|
+
"User-Agent": userAgent,
|
|
676
698
|
"Content-Type": "application/json",
|
|
677
699
|
...credential.uid === "" ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
|
|
678
700
|
...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
|
|
679
701
|
...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
|
|
702
|
+
"X-IDE-Type": "WorkBuddy",
|
|
703
|
+
"X-IDE-Name": "WorkBuddy",
|
|
704
|
+
"X-IDE-Version": clientVersion,
|
|
680
705
|
"X-Product": "SaaS"
|
|
681
706
|
};
|
|
682
707
|
}
|
|
@@ -821,18 +846,21 @@ var WorkBuddyUpstreamClient = class {
|
|
|
821
846
|
/** POST the chat endpoint; a successful answer is the raw SSE response. */
|
|
822
847
|
async chatStream(credential, bodyJson, signal) {
|
|
823
848
|
const region = regionOf(credential.domain);
|
|
849
|
+
let identity;
|
|
824
850
|
let userAgent;
|
|
825
851
|
try {
|
|
826
|
-
|
|
852
|
+
identity = await this.resolveChatIdentity(region);
|
|
853
|
+
userAgent = chatUserAgent(identity, region);
|
|
827
854
|
} catch {
|
|
828
|
-
|
|
855
|
+
identity = fallbackChatIdentity(region);
|
|
856
|
+
userAgent = chatUserAgent(identity, region);
|
|
829
857
|
}
|
|
830
858
|
let response;
|
|
831
859
|
try {
|
|
832
860
|
response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
|
|
833
861
|
method: "POST",
|
|
834
862
|
headers: {
|
|
835
|
-
...chatHeaders(credential, userAgent),
|
|
863
|
+
...chatHeaders(credential, userAgent, identity.clientVersion),
|
|
836
864
|
"Authorization": `Bearer ${credential.accessToken}`
|
|
837
865
|
},
|
|
838
866
|
body: region === "global" ? prepareInternationalChatBody(bodyJson) : bodyJson,
|
|
@@ -1144,11 +1172,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1144
1172
|
*/
|
|
1145
1173
|
async probeEffort(credential, model, effort, signal) {
|
|
1146
1174
|
const international = regionOf(credential.domain) === "global";
|
|
1175
|
+
let identity;
|
|
1147
1176
|
let userAgent;
|
|
1148
1177
|
try {
|
|
1149
|
-
|
|
1178
|
+
identity = await this.resolveChatIdentity(international ? "global" : "cn");
|
|
1179
|
+
userAgent = chatUserAgent(identity, international ? "global" : "cn");
|
|
1150
1180
|
} catch {
|
|
1151
|
-
|
|
1181
|
+
identity = fallbackChatIdentity(international ? "global" : "cn");
|
|
1182
|
+
userAgent = chatUserAgent(identity, international ? "global" : "cn");
|
|
1152
1183
|
}
|
|
1153
1184
|
const payload = {
|
|
1154
1185
|
model,
|
|
@@ -1168,7 +1199,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1168
1199
|
response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
|
|
1169
1200
|
method: "POST",
|
|
1170
1201
|
headers: {
|
|
1171
|
-
...chatHeaders(credential, userAgent),
|
|
1202
|
+
...chatHeaders(credential, userAgent, identity.clientVersion),
|
|
1172
1203
|
"Authorization": `Bearer ${credential.accessToken}`
|
|
1173
1204
|
},
|
|
1174
1205
|
body: JSON.stringify(payload),
|
|
@@ -1443,15 +1474,143 @@ const INTERNATIONAL_SYSTEM_PROMPT = "You are a helpful assistant.";
|
|
|
1443
1474
|
*/
|
|
1444
1475
|
const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
|
|
1445
1476
|
//#endregion
|
|
1477
|
+
//#region src/status-paths.ts
|
|
1478
|
+
/** Node-free constants and types shared by the Host and browser halves. */
|
|
1479
|
+
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
1480
|
+
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
|
|
1481
|
+
/**
|
|
1482
|
+
* Plugin-owned probe control endpoint.
|
|
1483
|
+
*
|
|
1484
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
1485
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
1486
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
1487
|
+
* also requires the in-process key the browser half receives with the status
|
|
1488
|
+
* document.
|
|
1489
|
+
*/
|
|
1490
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
|
|
1491
|
+
/**
|
|
1492
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
1493
|
+
*
|
|
1494
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
1495
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
1496
|
+
* independently, and a shared expression is one build-config drift away from
|
|
1497
|
+
* the desk asking a route the host never mounted.
|
|
1498
|
+
*/
|
|
1499
|
+
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
1500
|
+
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
1501
|
+
/**
|
|
1502
|
+
* Same-origin route backing the browser's update reminder.
|
|
1503
|
+
*
|
|
1504
|
+
* Read-only like the status route, so the same loopback Host/Origin gate
|
|
1505
|
+
* applies; it answers public npm/GitHub metadata only and never token
|
|
1506
|
+
* material.
|
|
1507
|
+
*/
|
|
1508
|
+
const WORKBUDDY_UPDATE_PATH = "/plugins/dsh-workbuddy-connect/update";
|
|
1509
|
+
//#endregion
|
|
1510
|
+
//#region src/variants.ts
|
|
1511
|
+
/**
|
|
1512
|
+
* The two WorkBuddy desktop apps this one plugin serves.
|
|
1513
|
+
*
|
|
1514
|
+
* Both products are the same client framework in different regions, and both
|
|
1515
|
+
* write their sign-in into the *same* shared `CodeBuddyExtension` auth
|
|
1516
|
+
* directory — they differ by file basename, base URL, catalog endpoint, and
|
|
1517
|
+
* display identity. Everything that varies between them is collected here as
|
|
1518
|
+
* one descriptor, so no module has to carry its own `if (international)`
|
|
1519
|
+
* branch and a third variant would be a data change rather than a refactor.
|
|
1520
|
+
*
|
|
1521
|
+
* This module is host-side (it names files and env vars). The browser half
|
|
1522
|
+
* takes the same ids and routes from the Node-free `status-paths.ts`, which
|
|
1523
|
+
* stays the single source shared by both halves.
|
|
1524
|
+
*
|
|
1525
|
+
* @module dsh-workbuddy-connect/variants
|
|
1526
|
+
*/
|
|
1527
|
+
/** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
|
|
1528
|
+
const WORKBUDDY_VARIANTS = [{
|
|
1529
|
+
id: "workbuddy",
|
|
1530
|
+
displayName: "WorkBuddy",
|
|
1531
|
+
appName: "WorkBuddy",
|
|
1532
|
+
region: "cn",
|
|
1533
|
+
env: "WORKBUDDY_AUTH_FILE",
|
|
1534
|
+
electron: {
|
|
1535
|
+
productName: "WorkBuddy",
|
|
1536
|
+
envVar: "WORKBUDDY_ELECTRON_BIN",
|
|
1537
|
+
macOS: {
|
|
1538
|
+
bundleId: "com.tencent.workbuddy.mac",
|
|
1539
|
+
defaultPath: "/Applications/WorkBuddy.app/Contents/MacOS/Electron"
|
|
1540
|
+
},
|
|
1541
|
+
windows: {
|
|
1542
|
+
displayNamePattern: /^WorkBuddy(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
|
|
1543
|
+
exeBasename: "workbuddy.exe",
|
|
1544
|
+
defaultPathSegments: [
|
|
1545
|
+
"Programs",
|
|
1546
|
+
"WorkBuddy",
|
|
1547
|
+
"WorkBuddy.exe"
|
|
1548
|
+
]
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
desktopFilename: "workbuddy-desktop.info",
|
|
1552
|
+
ownFilename: ".workbuddy-auth.json",
|
|
1553
|
+
probeFilename: ".workbuddy-probe.json",
|
|
1554
|
+
catalogFilename: ".workbuddy-catalog.json",
|
|
1555
|
+
visibilityFilename: ".workbuddy-model-visibility.json",
|
|
1556
|
+
statusPath: WORKBUDDY_STATUS_PATH,
|
|
1557
|
+
probePath: WORKBUDDY_PROBE_PATH
|
|
1558
|
+
}, {
|
|
1559
|
+
id: "workbuddy-ai",
|
|
1560
|
+
displayName: "WorkBuddy AI",
|
|
1561
|
+
appName: "WorkBuddy AI",
|
|
1562
|
+
region: "global",
|
|
1563
|
+
env: "WORKBUDDY_AI_AUTH_FILE",
|
|
1564
|
+
electron: {
|
|
1565
|
+
productName: "WorkBuddy AI",
|
|
1566
|
+
envVar: "WORKBUDDY_AI_ELECTRON_BIN",
|
|
1567
|
+
macOS: {
|
|
1568
|
+
bundleId: "com.workbuddy.workbuddy-ai",
|
|
1569
|
+
defaultPath: "/Applications/WorkBuddy AI.app/Contents/MacOS/Electron"
|
|
1570
|
+
},
|
|
1571
|
+
windows: {
|
|
1572
|
+
displayNamePattern: /^WorkBuddy AI(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
|
|
1573
|
+
exeBasename: "workbuddyai.exe"
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
1576
|
+
desktopFilename: "workbuddy-desktop-ai.info",
|
|
1577
|
+
ownFilename: ".workbuddy-ai-auth.json",
|
|
1578
|
+
probeFilename: ".workbuddy-ai-probe.json",
|
|
1579
|
+
catalogFilename: ".workbuddy-ai-catalog.json",
|
|
1580
|
+
visibilityFilename: ".workbuddy-ai-model-visibility.json",
|
|
1581
|
+
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
1582
|
+
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
1583
|
+
}];
|
|
1584
|
+
/** The CN variant; the plugin's long-standing default and compatibility anchor. */
|
|
1585
|
+
const CN_VARIANT = WORKBUDDY_VARIANTS[0];
|
|
1586
|
+
/** The international variant. */
|
|
1587
|
+
const AI_VARIANT = WORKBUDDY_VARIANTS[1];
|
|
1588
|
+
/** Look up a variant by provider id. */
|
|
1589
|
+
function variantFor(id) {
|
|
1590
|
+
return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* The Electron profile a variant resolves with.
|
|
1594
|
+
*
|
|
1595
|
+
* Callers inside the plugin pass their known-complete variants; descriptors
|
|
1596
|
+
* assembled outside (the type is public and predates the profile) fall back
|
|
1597
|
+
* by variant id, so an id-less or unknown custom variant stays on the CN
|
|
1598
|
+
* product — the same default the store's other legacy fields assume.
|
|
1599
|
+
*/
|
|
1600
|
+
function electronProfileFor(variant) {
|
|
1601
|
+
return variant?.electron ?? (variant?.id === AI_VARIANT.id ? AI_VARIANT : CN_VARIANT).electron;
|
|
1602
|
+
}
|
|
1603
|
+
//#endregion
|
|
1446
1604
|
//#region src/desktop-credential-protection.ts
|
|
1447
1605
|
/**
|
|
1448
1606
|
* WorkBuddy 5.6.x at-rest credential protection: classification, key
|
|
1449
|
-
* resolution, and field decryption for the desktop
|
|
1607
|
+
* resolution, and field decryption for the desktop apps' encrypted auth files.
|
|
1450
1608
|
*
|
|
1451
|
-
* Since WorkBuddy 5.6
|
|
1452
|
-
* `auth.refreshToken` at rest
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1609
|
+
* Since WorkBuddy 5.6 both desktop apps (CN and international, which followed
|
|
1610
|
+
* in 5.6.2) encrypt `auth.accessToken` and `auth.refreshToken` at rest
|
|
1611
|
+
* (`buildPolicy: "fields"`, on by default), so the plugin reads
|
|
1612
|
+
* `{$wbEncrypted:1, envelope}` wrappers instead of token strings (issues
|
|
1613
|
+
* #39/#40, #59/#60). Everything needed to open them lives on the same machine:
|
|
1455
1614
|
*
|
|
1456
1615
|
* - the sealed payload (`{version:1, atRestSecretKey}`) comes from the
|
|
1457
1616
|
* WorkBuddy-modified Electron's private `workbuddyStorage` binding, reached
|
|
@@ -1469,18 +1628,17 @@ const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
|
|
|
1469
1628
|
*
|
|
1470
1629
|
* @module dsh-workbuddy-connect/desktop-credential-protection
|
|
1471
1630
|
*/
|
|
1472
|
-
/** Env variable that overrides the WorkBuddy Electron binary used as the key helper. */
|
|
1473
|
-
const WORKBUDDY_ELECTRON_BIN_ENV = "WORKBUDDY_ELECTRON_BIN";
|
|
1474
|
-
/** Platform-default Electron binary, confirmed only on macOS (5.6.2). */
|
|
1475
|
-
const MACOS_ELECTRON_PATH = "/Applications/WorkBuddy.app/Contents/MacOS/Electron";
|
|
1476
1631
|
/**
|
|
1477
|
-
* The Electron binary
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1632
|
+
* The platform-default Electron binary for one product, or `undefined` where
|
|
1633
|
+
* none is verified. macOS defaults come from each product's measured layout;
|
|
1634
|
+
* the Windows default needs `LOCALAPPDATA`, and the international app has no
|
|
1635
|
+
* verified default location at all — registry discovery only, never a guess.
|
|
1481
1636
|
*/
|
|
1482
|
-
function defaultWorkBuddyElectronPath() {
|
|
1483
|
-
|
|
1637
|
+
function defaultWorkBuddyElectronPath(product, platform = process.platform) {
|
|
1638
|
+
if (platform === "darwin") return product.macOS.defaultPath;
|
|
1639
|
+
if (platform !== "win32" || product.windows.defaultPathSegments === void 0) return void 0;
|
|
1640
|
+
const localAppData = process.env.LOCALAPPDATA?.trim();
|
|
1641
|
+
return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData, ...product.windows.defaultPathSegments);
|
|
1484
1642
|
}
|
|
1485
1643
|
/** Distinct key ids across the wrapped fields, in field order. */
|
|
1486
1644
|
function keyIdsOf(fields) {
|
|
@@ -1661,11 +1819,37 @@ function parseAtRestPayload(text) {
|
|
|
1661
1819
|
function deriveProtectorKey(secret) {
|
|
1662
1820
|
return createHash("sha256").update(secret, "utf8").digest();
|
|
1663
1821
|
}
|
|
1664
|
-
/**
|
|
1665
|
-
|
|
1822
|
+
/**
|
|
1823
|
+
* Select discovery by platform. Both products have verified layouts on macOS
|
|
1824
|
+
* and Windows (the international app is registry-only there), so discovery no
|
|
1825
|
+
* longer gates on region; Linux and others stay `none`.
|
|
1826
|
+
*/
|
|
1827
|
+
function electronDiscoveryFor(platform = process.platform) {
|
|
1828
|
+
if (platform === "darwin") return "macos-workbuddy";
|
|
1829
|
+
if (platform === "win32") return "windows-workbuddy";
|
|
1830
|
+
return "none";
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* The at-rest key provider one variant's store should use. Shared by the
|
|
1834
|
+
* plugin host and the CLI entry so the browser card and `doctor`/`status` can
|
|
1835
|
+
* never disagree about which binary a variant resolves.
|
|
1836
|
+
*/
|
|
1837
|
+
function atRestKeyProviderFor(variant) {
|
|
1838
|
+
return new WorkBuddyAtRestKeyProvider({
|
|
1839
|
+
product: electronProfileFor(variant),
|
|
1840
|
+
discovery: electronDiscoveryFor()
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1666
1843
|
/** Absolute tool paths: never resolved through PATH, which a user can change. */
|
|
1667
1844
|
const MDFIND_BIN = "/usr/bin/mdfind";
|
|
1668
1845
|
const PLUTIL_BIN = "/usr/bin/plutil";
|
|
1846
|
+
const WINDOWS_REGISTRY_ROOTS = [
|
|
1847
|
+
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
1848
|
+
"HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
1849
|
+
"HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
|
|
1850
|
+
];
|
|
1851
|
+
const WINDOWS_REGISTRY_OUTPUT_MAX_BYTES = 1048576;
|
|
1852
|
+
const WINDOWS_ELECTRON_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
|
|
1669
1853
|
/** One discovery subprocess's own limits; see {@link WorkBuddyAtRestKeyProviderOptions}. */
|
|
1670
1854
|
const WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
|
|
1671
1855
|
const MDFIND_MAX_OUTPUT_BYTES = 1048576;
|
|
@@ -1682,8 +1866,12 @@ var DiscoveryIncompleteError = class extends Error {};
|
|
|
1682
1866
|
* identity. Every failure that means "we could not tell" — a missing tool, a
|
|
1683
1867
|
* timeout, an oversized answer — is raised as {@link DiscoveryIncompleteError}
|
|
1684
1868
|
* so it can never be silently read as "no such app".
|
|
1869
|
+
*
|
|
1870
|
+
* `bundleId` is the product being searched for; the Spotlight query and the
|
|
1871
|
+
* caller's identity comparison both use it, so the two can never disagree
|
|
1872
|
+
* about which app they are looking for.
|
|
1685
1873
|
*/
|
|
1686
|
-
function workBuddyDiscoveryTools() {
|
|
1874
|
+
function workBuddyDiscoveryTools(bundleId) {
|
|
1687
1875
|
const runTool = (bin, args, maxBytes, signal) => new Promise((resolve, reject) => {
|
|
1688
1876
|
if (signal.aborted) {
|
|
1689
1877
|
reject(new DiscoveryIncompleteError(`${bin} was not started: the discovery budget was already spent`));
|
|
@@ -1715,7 +1903,7 @@ function workBuddyDiscoveryTools() {
|
|
|
1715
1903
|
});
|
|
1716
1904
|
return {
|
|
1717
1905
|
findApps: async (signal) => {
|
|
1718
|
-
return (await runTool(MDFIND_BIN, [`kMDItemCFBundleIdentifier == '${
|
|
1906
|
+
return (await runTool(MDFIND_BIN, [`kMDItemCFBundleIdentifier == '${bundleId}'`], MDFIND_MAX_OUTPUT_BYTES, signal)).split("\n").map((line) => line.trim()).filter((line) => line.endsWith(".app"));
|
|
1719
1907
|
},
|
|
1720
1908
|
bundleIdentifier: async (bundlePath, signal) => {
|
|
1721
1909
|
try {
|
|
@@ -1749,6 +1937,58 @@ function workBuddyDiscoveryTools() {
|
|
|
1749
1937
|
};
|
|
1750
1938
|
}
|
|
1751
1939
|
/**
|
|
1940
|
+
* The Windows registry discovery tool. It is deliberately separate from the
|
|
1941
|
+
* macOS Spotlight/plutil seam: the two platforms have different identity and
|
|
1942
|
+
* candidate rules, and neither tool should accidentally become cross-platform.
|
|
1943
|
+
*/
|
|
1944
|
+
function workBuddyWindowsDiscoveryTools() {
|
|
1945
|
+
const systemRoot = process.env.SystemRoot?.trim();
|
|
1946
|
+
const regPath = systemRoot === void 0 || systemRoot === "" ? void 0 : join(systemRoot, "System32", "reg.exe");
|
|
1947
|
+
const runTool = (root, signal) => new Promise((resolve, reject) => {
|
|
1948
|
+
if (regPath === void 0) {
|
|
1949
|
+
reject(new DiscoveryIncompleteError("SystemRoot is not configured"));
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (signal.aborted) {
|
|
1953
|
+
reject(new DiscoveryIncompleteError("reg.exe was not started: the discovery budget was already spent"));
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
let settled = false;
|
|
1957
|
+
const child = execFile(regPath, [
|
|
1958
|
+
"query",
|
|
1959
|
+
root,
|
|
1960
|
+
"/s"
|
|
1961
|
+
], {
|
|
1962
|
+
maxBuffer: WINDOWS_REGISTRY_OUTPUT_MAX_BYTES,
|
|
1963
|
+
timeout: WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS,
|
|
1964
|
+
windowsHide: true
|
|
1965
|
+
}, (error, stdout, stderr) => {
|
|
1966
|
+
if (settled) return;
|
|
1967
|
+
settled = true;
|
|
1968
|
+
if (error !== null && error !== void 0) {
|
|
1969
|
+
if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissing(stderr)) {
|
|
1970
|
+
resolve("");
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
reject(new DiscoveryIncompleteError(`reg.exe could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
resolve(stdout);
|
|
1977
|
+
});
|
|
1978
|
+
const abort = () => {
|
|
1979
|
+
if (settled) return;
|
|
1980
|
+
settled = true;
|
|
1981
|
+
child.kill();
|
|
1982
|
+
reject(new DiscoveryIncompleteError("reg.exe was abandoned: the discovery budget was spent"));
|
|
1983
|
+
};
|
|
1984
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1985
|
+
child.on("close", () => {
|
|
1986
|
+
signal.removeEventListener("abort", abort);
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
return { queryUninstallRoot: runTool };
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1752
1992
|
* In-memory protector-key resolver: one spawn per key id, single-flight, never
|
|
1753
1993
|
* persisted. The cache is keyed by the id envelopes ask for, so an envelope
|
|
1754
1994
|
* sealed under a rotated key triggers exactly one fresh resolution.
|
|
@@ -1760,9 +2000,12 @@ function workBuddyDiscoveryTools() {
|
|
|
1760
2000
|
* a different app.
|
|
1761
2001
|
*/
|
|
1762
2002
|
explicitPath;
|
|
2003
|
+
product;
|
|
1763
2004
|
defaultPath;
|
|
1764
2005
|
discovery;
|
|
1765
2006
|
tools;
|
|
2007
|
+
windowsTools;
|
|
2008
|
+
platform;
|
|
1766
2009
|
discoveryBudgetMs;
|
|
1767
2010
|
timeoutMs;
|
|
1768
2011
|
source;
|
|
@@ -1775,13 +2018,16 @@ function workBuddyDiscoveryTools() {
|
|
|
1775
2018
|
discoveredPath;
|
|
1776
2019
|
cache;
|
|
1777
2020
|
inflight;
|
|
1778
|
-
constructor(options
|
|
1779
|
-
|
|
2021
|
+
constructor(options) {
|
|
2022
|
+
this.product = options.product;
|
|
2023
|
+
const fromEnv = process.env[options.product.envVar]?.trim();
|
|
1780
2024
|
const envPath = fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
|
|
1781
2025
|
this.explicitPath = options.electronPath ?? envPath;
|
|
1782
2026
|
this.discovery = options.discovery ?? "none";
|
|
1783
|
-
this.
|
|
1784
|
-
this.
|
|
2027
|
+
this.platform = options.platform ?? process.platform;
|
|
2028
|
+
this.defaultPath = this.discovery === "none" ? void 0 : options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath(options.product, this.platform) : options.defaultElectronPath ?? void 0;
|
|
2029
|
+
this.tools = options.tools ?? workBuddyDiscoveryTools(options.product.macOS.bundleId);
|
|
2030
|
+
this.windowsTools = options.windowsTools ?? workBuddyWindowsDiscoveryTools();
|
|
1785
2031
|
this.discoveryBudgetMs = options.discoveryBudgetMs ?? 1e4;
|
|
1786
2032
|
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
1787
2033
|
this.spawnHelper = options.spawnHelper ?? ((path) => this.spawnAt(path));
|
|
@@ -1839,29 +2085,29 @@ function workBuddyDiscoveryTools() {
|
|
|
1839
2085
|
*/
|
|
1840
2086
|
async resolveElectronPath() {
|
|
1841
2087
|
if (this.explicitPath !== void 0) {
|
|
1842
|
-
if (!isExecutable(this.explicitPath)) throw new WorkBuddyElectronPathError("electron-path-invalid", `the configured
|
|
2088
|
+
if (!isExecutable(this.explicitPath)) throw new WorkBuddyElectronPathError("electron-path-invalid", `the configured ${this.product.productName} Electron binary is not available at ${this.explicitPath}; check ${this.product.envVar} or unset it to let the plugin look for the app itself`);
|
|
1843
2089
|
return this.explicitPath;
|
|
1844
2090
|
}
|
|
1845
|
-
if (this.discovery === "none") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no
|
|
2091
|
+
if (this.discovery === "none") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
|
|
1846
2092
|
if (this.defaultPath !== void 0 && isExecutable(this.defaultPath)) return this.defaultPath;
|
|
1847
2093
|
if (this.discoveredPath !== void 0) {
|
|
1848
2094
|
if (isExecutable(this.discoveredPath)) return this.discoveredPath;
|
|
1849
2095
|
this.discoveredPath = void 0;
|
|
1850
2096
|
}
|
|
1851
|
-
const found = await this.discoverMacosApp();
|
|
2097
|
+
const found = this.discovery === "macos-workbuddy" ? await this.discoverMacosApp() : await this.discoverWindowsApp();
|
|
1852
2098
|
this.discoveredPath = found;
|
|
1853
2099
|
return found;
|
|
1854
2100
|
}
|
|
1855
2101
|
/**
|
|
1856
|
-
* Resolve
|
|
1857
|
-
* before it can be executed.
|
|
2102
|
+
* Resolve this product's app through Spotlight, then prove each candidate's
|
|
2103
|
+
* identity before it can be executed.
|
|
1858
2104
|
*
|
|
1859
2105
|
* The whole flow shares one budget: a hang in one candidate must not extend
|
|
1860
2106
|
* the wait for the others, and running out of budget is reported as an
|
|
1861
2107
|
* unfinished check rather than an absent app.
|
|
1862
2108
|
*/
|
|
1863
2109
|
async discoverMacosApp() {
|
|
1864
|
-
if (
|
|
2110
|
+
if (this.platform !== "darwin") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
|
|
1865
2111
|
const controller = new AbortController();
|
|
1866
2112
|
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
1867
2113
|
try {
|
|
@@ -1869,7 +2115,7 @@ function workBuddyDiscoveryTools() {
|
|
|
1869
2115
|
try {
|
|
1870
2116
|
candidates = await this.tools.findApps(controller.signal);
|
|
1871
2117
|
} catch {
|
|
1872
|
-
throw discoveryIncomplete("the app search did not complete");
|
|
2118
|
+
throw discoveryIncomplete(this.product.productName, "the app search did not complete");
|
|
1873
2119
|
}
|
|
1874
2120
|
const seen = /* @__PURE__ */ new Map();
|
|
1875
2121
|
let unresolved = false;
|
|
@@ -1892,7 +2138,7 @@ function workBuddyDiscoveryTools() {
|
|
|
1892
2138
|
unresolved = true;
|
|
1893
2139
|
continue;
|
|
1894
2140
|
}
|
|
1895
|
-
if (bundleIdentifier !==
|
|
2141
|
+
if (bundleIdentifier !== this.product.macOS.bundleId) continue;
|
|
1896
2142
|
const electronPath = join(candidate, "Contents", "MacOS", "Electron");
|
|
1897
2143
|
if (!isExecutable(electronPath)) continue;
|
|
1898
2144
|
let identity;
|
|
@@ -1914,15 +2160,65 @@ function workBuddyDiscoveryTools() {
|
|
|
1914
2160
|
...version === void 0 ? {} : { version }
|
|
1915
2161
|
});
|
|
1916
2162
|
}
|
|
1917
|
-
if (seen.size > 1)
|
|
1918
|
-
|
|
1919
|
-
|
|
2163
|
+
if (seen.size > 1) {
|
|
2164
|
+
const listed = [...seen.values()].map((app) => ` - ${app.bundlePath}${app.version === void 0 ? "" : ` (${app.version})`}`).join("\n");
|
|
2165
|
+
throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one ${this.product.productName} application was found, so none was chosen:\n${listed}\n set ${this.product.envVar} to the one to use`);
|
|
2166
|
+
}
|
|
2167
|
+
if (unresolved) throw discoveryIncomplete(this.product.productName, "some candidates could not be checked");
|
|
2168
|
+
if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", `no ${this.product.productName} application was found in the default location or the system index; if it is installed elsewhere, it may not be indexed yet; set ${this.product.envVar} to the app's Electron binary`);
|
|
1920
2169
|
return [...seen.values()][0].electronPath;
|
|
1921
2170
|
} finally {
|
|
1922
2171
|
clearTimeout(timer);
|
|
1923
2172
|
controller.abort();
|
|
1924
2173
|
}
|
|
1925
2174
|
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Resolve this product's app through Windows uninstall records. Registry
|
|
2177
|
+
* entries provide hints, not trust: every DisplayIcon candidate must still
|
|
2178
|
+
* be the product's Electron binary with the known Electron layout before
|
|
2179
|
+
* execution.
|
|
2180
|
+
*/
|
|
2181
|
+
async discoverWindowsApp() {
|
|
2182
|
+
if (this.platform !== "win32") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
|
|
2183
|
+
const controller = new AbortController();
|
|
2184
|
+
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
2185
|
+
try {
|
|
2186
|
+
const candidates = [];
|
|
2187
|
+
let unresolved = false;
|
|
2188
|
+
for (const root of WINDOWS_REGISTRY_ROOTS) {
|
|
2189
|
+
let output;
|
|
2190
|
+
try {
|
|
2191
|
+
output = await this.windowsTools.queryUninstallRoot(root, controller.signal);
|
|
2192
|
+
} catch {
|
|
2193
|
+
unresolved = true;
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
const parsed = parseWindowsRegistryOutput(output, this.product.windows.displayNamePattern);
|
|
2197
|
+
candidates.push(...parsed.candidates);
|
|
2198
|
+
unresolved ||= parsed.incomplete;
|
|
2199
|
+
}
|
|
2200
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2201
|
+
for (const candidate of new Set(candidates)) {
|
|
2202
|
+
const inspection = inspectWindowsElectronCandidate(candidate, this.platform, this.product.windows.exeBasename);
|
|
2203
|
+
if (inspection === "unresolved") {
|
|
2204
|
+
unresolved = true;
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
if (inspection === void 0) continue;
|
|
2208
|
+
seen.set(inspection.identity, inspection.electronPath);
|
|
2209
|
+
}
|
|
2210
|
+
if (unresolved) throw discoveryIncomplete(this.product.productName, "some registry entries or candidates could not be checked");
|
|
2211
|
+
if (seen.size > 1) {
|
|
2212
|
+
const listed = [...seen.values()].map((path) => ` - ${path}`).join("\n");
|
|
2213
|
+
throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one ${this.product.productName} application was found, so none was chosen:\n${listed}\n set ${this.product.envVar} to the one to use`);
|
|
2214
|
+
}
|
|
2215
|
+
if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", `no usable ${this.product.productName} Electron binary was found in the default location or Windows uninstall records; set ${this.product.envVar} to the app's Electron binary`);
|
|
2216
|
+
return [...seen.values()][0];
|
|
2217
|
+
} finally {
|
|
2218
|
+
clearTimeout(timer);
|
|
2219
|
+
controller.abort();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
1926
2222
|
async spawnPayload() {
|
|
1927
2223
|
return await this.spawnHelper(await this.resolveElectronPath());
|
|
1928
2224
|
}
|
|
@@ -1951,6 +2247,115 @@ function workBuddyDiscoveryTools() {
|
|
|
1951
2247
|
});
|
|
1952
2248
|
}
|
|
1953
2249
|
};
|
|
2250
|
+
/** Only known missing-key diagnostics can safely make a failed query empty. */
|
|
2251
|
+
function windowsRegistryKeyMissing(stderr) {
|
|
2252
|
+
const detail = stderr.trim();
|
|
2253
|
+
return /^ERROR:\s*The system was unable to find the specified registry key or value\.?$/iu.test(detail) || /^错误[::]\s*系统找不到指定的注册表项或值[。.]?$/u.test(detail);
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Parse the value columns emitted by `reg query ... /s`.
|
|
2257
|
+
*
|
|
2258
|
+
* Entries are filtered by the product's DisplayName pattern *before* the
|
|
2259
|
+
* DisplayIcon is judged, so another product's records — however broken — are
|
|
2260
|
+
* excluded as decisively not ours and can never mark this product's search
|
|
2261
|
+
* incomplete.
|
|
2262
|
+
*/
|
|
2263
|
+
function parseWindowsRegistryOutput(output, displayNamePattern) {
|
|
2264
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2265
|
+
let currentKey;
|
|
2266
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
2267
|
+
const keyMatch = /^\s*(HKEY_[^\r\n]+?)\s*$/iu.exec(line);
|
|
2268
|
+
if (keyMatch !== null) {
|
|
2269
|
+
currentKey = keyMatch[1];
|
|
2270
|
+
entries.set(currentKey, {});
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
if (currentKey === void 0) continue;
|
|
2274
|
+
const valueMatch = /^\s+(DisplayName|DisplayIcon)\s+REG_[A-Z0-9_]+\s*(.*?)\s*$/iu.exec(line);
|
|
2275
|
+
if (valueMatch === null) continue;
|
|
2276
|
+
const entry = entries.get(currentKey);
|
|
2277
|
+
if (entry === void 0) continue;
|
|
2278
|
+
const value = valueMatch[2] ?? "";
|
|
2279
|
+
if (valueMatch[1].toLowerCase() === "displayname") entry.displayName = value;
|
|
2280
|
+
else entry.displayIcon = value;
|
|
2281
|
+
}
|
|
2282
|
+
const candidates = [];
|
|
2283
|
+
let incomplete = false;
|
|
2284
|
+
for (const entry of entries.values()) {
|
|
2285
|
+
if (entry.displayName === void 0) continue;
|
|
2286
|
+
if (!displayNamePattern.test(entry.displayName.trim())) continue;
|
|
2287
|
+
const displayIcon = entry.displayIcon === void 0 ? void 0 : parseWindowsDisplayIcon(entry.displayIcon);
|
|
2288
|
+
if (displayIcon === void 0) incomplete = true;
|
|
2289
|
+
else candidates.push(displayIcon);
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
candidates,
|
|
2293
|
+
incomplete
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
/** Read a quoted DisplayIcon path and remove the Windows icon-index suffix. */
|
|
2297
|
+
function parseWindowsDisplayIcon(value) {
|
|
2298
|
+
const raw = value.trim();
|
|
2299
|
+
let path;
|
|
2300
|
+
if (raw.startsWith("\"")) {
|
|
2301
|
+
const closingQuote = raw.indexOf("\"", 1);
|
|
2302
|
+
if (closingQuote < 0) return void 0;
|
|
2303
|
+
const suffix = raw.slice(closingQuote + 1).trim();
|
|
2304
|
+
if (suffix !== "" && !/^,\d+$/u.test(suffix)) return void 0;
|
|
2305
|
+
path = raw.slice(1, closingQuote).replace(/,\d+$/u, "");
|
|
2306
|
+
} else {
|
|
2307
|
+
const match = /^(.+?\.exe)(?:,\d+)?$/iu.exec(raw);
|
|
2308
|
+
if (match === null) return void 0;
|
|
2309
|
+
path = match[1];
|
|
2310
|
+
}
|
|
2311
|
+
path = path.trim();
|
|
2312
|
+
return /\.exe$/iu.test(path) ? path : void 0;
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Validate the known Windows layout for the product's exe. `undefined` is a
|
|
2316
|
+
* decidable exclusion; `unresolved` is reserved for errors that prevent
|
|
2317
|
+
* checking.
|
|
2318
|
+
*/
|
|
2319
|
+
function inspectWindowsElectronCandidate(electronPath, platform, exeBasename) {
|
|
2320
|
+
if (platform !== "win32" || basename(electronPath).toLowerCase() !== exeBasename) return void 0;
|
|
2321
|
+
let binaryStat;
|
|
2322
|
+
try {
|
|
2323
|
+
binaryStat = statSync(electronPath);
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2326
|
+
}
|
|
2327
|
+
if (!binaryStat.isFile()) return void 0;
|
|
2328
|
+
try {
|
|
2329
|
+
accessSync(electronPath, constants.X_OK);
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2332
|
+
}
|
|
2333
|
+
const installRoot = dirname(electronPath);
|
|
2334
|
+
let version;
|
|
2335
|
+
try {
|
|
2336
|
+
version = readFileSync(join(installRoot, "version"), "utf8").trim();
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2339
|
+
}
|
|
2340
|
+
if (!WINDOWS_ELECTRON_VERSION_PATTERN.test(version)) return void 0;
|
|
2341
|
+
let resourcesStat;
|
|
2342
|
+
try {
|
|
2343
|
+
resourcesStat = statSync(join(installRoot, "resources", "app.asar"));
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2346
|
+
}
|
|
2347
|
+
if (!resourcesStat.isFile()) return void 0;
|
|
2348
|
+
let identity;
|
|
2349
|
+
try {
|
|
2350
|
+
identity = realpathSync(electronPath);
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2353
|
+
}
|
|
2354
|
+
return {
|
|
2355
|
+
electronPath,
|
|
2356
|
+
identity: platform === "win32" ? identity.toLowerCase() : identity
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
1954
2359
|
/** Whether a path exists and is executable; never throws. */
|
|
1955
2360
|
function isExecutable(path) {
|
|
1956
2361
|
try {
|
|
@@ -1981,8 +2386,8 @@ function reasonCodeOf(error) {
|
|
|
1981
2386
|
function isENOENT$1(error) {
|
|
1982
2387
|
return error?.code === "ENOENT";
|
|
1983
2388
|
}
|
|
1984
|
-
function discoveryIncomplete(detail) {
|
|
1985
|
-
return new WorkBuddyElectronPathError("electron-discovery-incomplete", `the
|
|
2389
|
+
function discoveryIncomplete(productName, detail) {
|
|
2390
|
+
return new WorkBuddyElectronPathError("electron-discovery-incomplete", `the ${productName} application search did not finish (${detail}); this is not proof that the app is missing`);
|
|
1986
2391
|
}
|
|
1987
2392
|
/**
|
|
1988
2393
|
* The helper: run inside WorkBuddy's Electron as plain Node, where the
|
|
@@ -2199,7 +2604,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
2199
2604
|
this.refresh = options.refresh;
|
|
2200
2605
|
this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
|
|
2201
2606
|
this.ownPath = options.ownPath ?? (options.variant ? join(resolveDshHome(), options.variant.ownFilename) : workbuddyOwnAuthPath());
|
|
2202
|
-
this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider();
|
|
2607
|
+
this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider({ product: electronProfileFor(options.variant) });
|
|
2203
2608
|
this.desktopPathOverride = options.desktopPath;
|
|
2204
2609
|
}
|
|
2205
2610
|
/**
|
|
@@ -3203,33 +3608,8 @@ var WorkBuddyCatalog = class {
|
|
|
3203
3608
|
}
|
|
3204
3609
|
};
|
|
3205
3610
|
//#endregion
|
|
3206
|
-
//#region src/status-paths.ts
|
|
3207
|
-
/** Node-free constants and types shared by the Host and browser halves. */
|
|
3208
|
-
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
3209
|
-
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
|
|
3210
|
-
/**
|
|
3211
|
-
* Plugin-owned probe control endpoint.
|
|
3212
|
-
*
|
|
3213
|
-
* Separate from the status route because it accepts writes: the status route's
|
|
3214
|
-
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
3215
|
-
* not the same as authorizing a state-changing action. This route therefore
|
|
3216
|
-
* also requires the in-process key the browser half receives with the status
|
|
3217
|
-
* document.
|
|
3218
|
-
*/
|
|
3219
|
-
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
|
|
3220
|
-
/**
|
|
3221
|
-
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
3222
|
-
*
|
|
3223
|
-
* Kept as separate constants rather than a computed suffix so both halves
|
|
3224
|
-
* reference literal strings: the browser bundle and the host bundle are built
|
|
3225
|
-
* independently, and a shared expression is one build-config drift away from
|
|
3226
|
-
* the desk asking a route the host never mounted.
|
|
3227
|
-
*/
|
|
3228
|
-
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
3229
|
-
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
3230
|
-
//#endregion
|
|
3231
3611
|
//#region src/version.ts
|
|
3232
|
-
const WORKBUDDY_CONNECT_VERSION = "0.6.
|
|
3612
|
+
const WORKBUDDY_CONNECT_VERSION = "0.6.4";
|
|
3233
3613
|
//#endregion
|
|
3234
3614
|
//#region src/host-heartbeat.ts
|
|
3235
3615
|
/**
|
|
@@ -3400,58 +3780,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
3400
3780
|
return startAtMs <= heartbeat.registeredAt;
|
|
3401
3781
|
}
|
|
3402
3782
|
//#endregion
|
|
3403
|
-
|
|
3404
|
-
/**
|
|
3405
|
-
* The two WorkBuddy desktop apps this one plugin serves.
|
|
3406
|
-
*
|
|
3407
|
-
* Both products are the same client framework in different regions, and both
|
|
3408
|
-
* write their sign-in into the *same* shared `CodeBuddyExtension` auth
|
|
3409
|
-
* directory — they differ by file basename, base URL, catalog endpoint, and
|
|
3410
|
-
* display identity. Everything that varies between them is collected here as
|
|
3411
|
-
* one descriptor, so no module has to carry its own `if (international)`
|
|
3412
|
-
* branch and a third variant would be a data change rather than a refactor.
|
|
3413
|
-
*
|
|
3414
|
-
* This module is host-side (it names files and env vars). The browser half
|
|
3415
|
-
* takes the same ids and routes from the Node-free `status-paths.ts`, which
|
|
3416
|
-
* stays the single source shared by both halves.
|
|
3417
|
-
*
|
|
3418
|
-
* @module dsh-workbuddy-connect/variants
|
|
3419
|
-
*/
|
|
3420
|
-
/** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
|
|
3421
|
-
const WORKBUDDY_VARIANTS = [{
|
|
3422
|
-
id: "workbuddy",
|
|
3423
|
-
displayName: "WorkBuddy",
|
|
3424
|
-
appName: "WorkBuddy",
|
|
3425
|
-
region: "cn",
|
|
3426
|
-
env: "WORKBUDDY_AUTH_FILE",
|
|
3427
|
-
desktopFilename: "workbuddy-desktop.info",
|
|
3428
|
-
ownFilename: ".workbuddy-auth.json",
|
|
3429
|
-
probeFilename: ".workbuddy-probe.json",
|
|
3430
|
-
catalogFilename: ".workbuddy-catalog.json",
|
|
3431
|
-
visibilityFilename: ".workbuddy-model-visibility.json",
|
|
3432
|
-
statusPath: WORKBUDDY_STATUS_PATH,
|
|
3433
|
-
probePath: WORKBUDDY_PROBE_PATH
|
|
3434
|
-
}, {
|
|
3435
|
-
id: "workbuddy-ai",
|
|
3436
|
-
displayName: "WorkBuddy AI",
|
|
3437
|
-
appName: "WorkBuddy AI",
|
|
3438
|
-
region: "global",
|
|
3439
|
-
env: "WORKBUDDY_AI_AUTH_FILE",
|
|
3440
|
-
desktopFilename: "workbuddy-desktop-ai.info",
|
|
3441
|
-
ownFilename: ".workbuddy-ai-auth.json",
|
|
3442
|
-
probeFilename: ".workbuddy-ai-probe.json",
|
|
3443
|
-
catalogFilename: ".workbuddy-ai-catalog.json",
|
|
3444
|
-
visibilityFilename: ".workbuddy-ai-model-visibility.json",
|
|
3445
|
-
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
3446
|
-
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
3447
|
-
}];
|
|
3448
|
-
/** The CN variant; the plugin's long-standing default and compatibility anchor. */
|
|
3449
|
-
const CN_VARIANT = WORKBUDDY_VARIANTS[0];
|
|
3450
|
-
/** The international variant. */
|
|
3451
|
-
const AI_VARIANT = WORKBUDDY_VARIANTS[1];
|
|
3452
|
-
/** Look up a variant by provider id. */
|
|
3453
|
-
function variantFor(id) {
|
|
3454
|
-
return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
|
|
3455
|
-
}
|
|
3456
|
-
//#endregion
|
|
3457
|
-
export { normalizeCredits as A, chatUserAgent as B, desktopAuthCandidatesFor as C, WorkBuddyUpstreamClient as D, WorkBuddyAtRestKeyProvider as E, PROBE_EFFORT_CANDIDATES as F, WORKBUDDY_APP_VERSION_FILENAME as G, readCliVersion as H, probeModel as I, readBundleVersion as J, appUserAgent as K, randomSentinel as L, prepareChatBody as M, prepareInternationalChatBody as N, classifyUpstreamError as O, regionOf as P, CN_APP_VERSION_FILENAME as R, defaultDesktopAuthPath as S, workbuddyOwnAuthPath as T, resolveChatIdentity as U, fallbackChatIdentity as V, validCliVersion as W, validAppVersion as X, resolveAppVersion as Y, WorkBuddyCatalog as _, WORKBUDDY_HOST_HEARTBEAT_FILENAME as a, WorkBuddyCredentialStore as b, processStartTimeMs as c, writeHostHeartbeat as d, WORKBUDDY_CONNECT_VERSION as f, FALLBACK_WORKBUDDY_MODELS as g, FALLBACK_WORKBUDDY_AI_MODELS as h, variantFor as i, parseModelCatalog as j, modelWithCurrentPromotion as k, readHostHeartbeat as l, WORKBUDDY_STATUS_PATH as m, CN_VARIANT as n, clearHostHeartbeat as o, WORKBUDDY_PROBE_PATH as p, installedAppVersion as q, WORKBUDDY_VARIANTS as r, isHeartbeatProcessAlive as s, AI_VARIANT as t, workbuddyHostHeartbeatPath as u, WORKBUDDY_AUTH_FILENAME as v, parseWorkBuddyAuth as w, defaultDesktopAuthCandidates as x, WORKBUDDY_AUTH_FILE_ENV as y, FALLBACK_CN_APP_VERSION as z };
|
|
3783
|
+
export { extractDisplayErrorMessage as A, CN_APP_VERSION_FILENAME as B, WORKBUDDY_VARIANTS as C, WORKBUDDY_UPDATE_PATH as D, WORKBUDDY_STATUS_PATH as E, prepareInternationalChatBody as F, resolveChatIdentity as G, chatUserAgent as H, regionOf as I, appUserAgent as J, validCliVersion as K, PROBE_EFFORT_CANDIDATES as L, normalizeCredits as M, parseModelCatalog as N, WorkBuddyUpstreamClient as O, prepareChatBody as P, validAppVersion as Q, probeModel as R, CN_VARIANT as S, WORKBUDDY_PROBE_PATH as T, fallbackChatIdentity as U, FALLBACK_CN_APP_VERSION as V, readCliVersion as W, readBundleVersion as X, installedAppVersion as Y, resolveAppVersion as Z, desktopAuthCandidatesFor as _, readHostHeartbeat as a, atRestKeyProviderFor as b, WORKBUDDY_CONNECT_VERSION as c, WorkBuddyCatalog as d, WORKBUDDY_AUTH_FILENAME as f, defaultDesktopAuthPath as g, defaultDesktopAuthCandidates as h, processStartTimeMs as i, modelWithCurrentPromotion as j, classifyUpstreamError as k, FALLBACK_WORKBUDDY_AI_MODELS as l, WorkBuddyCredentialStore as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, WORKBUDDY_AUTH_FILE_ENV as p, WORKBUDDY_APP_VERSION_FILENAME as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, parseWorkBuddyAuth as v, variantFor as w, AI_VARIANT as x, workbuddyOwnAuthPath as y, randomSentinel as z };
|