@cats-inc/cats-platform 0.2.8 → 0.3.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.md +17 -0
- package/build/desktop/bootstrapPage.d.ts.map +1 -1
- package/build/desktop/bootstrapPage.js +26 -129
- package/build/desktop/bootstrapPage.js.map +1 -1
- package/build/desktop/cliInventoryProbe.d.ts +6 -0
- package/build/desktop/cliInventoryProbe.d.ts.map +1 -1
- package/build/desktop/cliInventoryProbe.js +6 -3
- package/build/desktop/cliInventoryProbe.js.map +1 -1
- package/build/desktop/contracts.d.ts +2 -0
- package/build/desktop/contracts.d.ts.map +1 -1
- package/build/desktop/contracts.js.map +1 -1
- package/build/desktop/main.js +95 -41
- package/build/desktop/main.js.map +1 -1
- package/build/desktop/preload.cjs +8 -6
- package/build/desktop/preload.cjs.map +1 -1
- package/build/desktop/providerManager.d.ts +46 -0
- package/build/desktop/providerManager.d.ts.map +1 -0
- package/build/desktop/providerManager.js +220 -0
- package/build/desktop/providerManager.js.map +1 -0
- package/build/desktop/providerSelection.d.ts +1 -0
- package/build/desktop/providerSelection.d.ts.map +1 -1
- package/build/desktop/providerSelection.js +7 -1
- package/build/desktop/providerSelection.js.map +1 -1
- package/build/desktop/setupAssets.d.ts.map +1 -1
- package/build/desktop/setupAssets.js +12 -0
- package/build/desktop/setupAssets.js.map +1 -1
- package/build/desktop/setupAudit.d.ts +3 -1
- package/build/desktop/setupAudit.d.ts.map +1 -1
- package/build/desktop/setupAudit.js +12 -8
- package/build/desktop/setupAudit.js.map +1 -1
- package/build/desktop/setupBridge.js +1 -1
- package/build/desktop/setupBridge.js.map +1 -1
- package/build/desktop/setupPrerequisites.d.ts +11 -0
- package/build/desktop/setupPrerequisites.d.ts.map +1 -0
- package/build/desktop/setupPrerequisites.js +47 -0
- package/build/desktop/setupPrerequisites.js.map +1 -0
- package/build/renderer/assets/ActiveSessionPermissionChip-B49WcZBJ.js +1 -0
- package/build/renderer/assets/{App-BZA23O2J.js → App-BpxZB5ym.js} +1 -1
- package/build/renderer/assets/{App-rGw9J8PZ.js → App-DYTUfAl6.js} +1 -1
- package/build/renderer/assets/{App-YqprEL2n.js → App-boB4WrmU.js} +1 -1
- package/build/renderer/assets/{WorkspaceAppRoutes-BCHCikmJ.js → WorkspaceAppRoutes-CPqz85iw.js} +1 -1
- package/build/renderer/assets/app-C52wwCGz.js +85 -0
- package/build/renderer/assets/app-CqnsY5ed.css +1 -0
- package/build/renderer/assets/{client-BdGHAUoK.js → client-DREGSZqQ.js} +1 -1
- package/build/renderer/assets/conversationSidebarRecentEntries-DK0iWS73.js +6 -0
- package/build/renderer/assets/{screenshotOverlay-DfBcolX-.js → screenshotOverlay-EF07cXvw.js} +1 -1
- package/build/renderer/assets/{useDraftSessionChips-D9BX6X92.js → useDraftSessionChips-BdZ_B1gy.js} +1 -1
- package/build/renderer/desktop/overlay/index.html +2 -2
- package/build/renderer/index.html +3 -3
- package/build/server/app/renderer/auth/api.d.ts.map +1 -1
- package/build/server/app/renderer/auth/api.js +4 -1
- package/build/server/app/renderer/auth/api.js.map +1 -1
- package/build/server/app/renderer/providerCatalogClient.d.ts.map +1 -1
- package/build/server/app/renderer/providerCatalogClient.js +19 -6
- package/build/server/app/renderer/providerCatalogClient.js.map +1 -1
- package/build/server/app/renderer/providerClientInvalidation.d.ts +6 -0
- package/build/server/app/renderer/providerClientInvalidation.d.ts.map +1 -0
- package/build/server/app/renderer/providerClientInvalidation.js +12 -0
- package/build/server/app/renderer/providerClientInvalidation.js.map +1 -0
- package/build/server/app/renderer/providerReadLoop.d.ts +5 -0
- package/build/server/app/renderer/providerReadLoop.d.ts.map +1 -0
- package/build/server/app/renderer/providerReadLoop.js +52 -0
- package/build/server/app/renderer/providerReadLoop.js.map +1 -0
- package/build/server/app/renderer/providerRegistryClient.d.ts +2 -1
- package/build/server/app/renderer/providerRegistryClient.d.ts.map +1 -1
- package/build/server/app/renderer/providerRegistryClient.js +33 -17
- package/build/server/app/renderer/providerRegistryClient.js.map +1 -1
- package/build/server/app/server/authGatePolicy.js +7 -0
- package/build/server/app/server/authGatePolicy.js.map +1 -1
- package/build/server/app/server/subscribeRoutes.d.ts.map +1 -1
- package/build/server/app/server/subscribeRoutes.js +5 -3
- package/build/server/app/server/subscribeRoutes.js.map +1 -1
- package/build/server/design/components/providerModelFieldsSupport.d.ts +2 -21
- package/build/server/design/components/providerModelFieldsSupport.d.ts.map +1 -1
- package/build/server/design/components/providerModelFieldsSupport.js +32 -36
- package/build/server/design/components/providerModelFieldsSupport.js.map +1 -1
- package/build/server/design/components/useProviderCatalogState.d.ts +1 -2
- package/build/server/design/components/useProviderCatalogState.d.ts.map +1 -1
- package/build/server/design/components/useProviderCatalogState.js +71 -152
- package/build/server/design/components/useProviderCatalogState.js.map +1 -1
- package/build/server/design/components/useProviderModelFieldActions.js +2 -2
- package/build/server/design/components/useProviderModelFieldActions.js.map +1 -1
- package/build/server/design/components/useProviderRegistryState.d.ts +1 -7
- package/build/server/design/components/useProviderRegistryState.d.ts.map +1 -1
- package/build/server/design/components/useProviderRegistryState.js +41 -91
- package/build/server/design/components/useProviderRegistryState.js.map +1 -1
- package/build/server/design/components/useProviderTargetReconciliation.d.ts +1 -0
- package/build/server/design/components/useProviderTargetReconciliation.d.ts.map +1 -1
- package/build/server/design/components/useProviderTargetReconciliation.js +1 -1
- package/build/server/design/components/useProviderTargetReconciliation.js.map +1 -1
- package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts +2 -1
- package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts.map +1 -1
- package/build/server/platform/orchestration/entitySubscriptions/channel.js +3 -18
- package/build/server/platform/orchestration/entitySubscriptions/channel.js.map +1 -1
- package/build/server/platform/orchestration/entitySubscriptions/index.d.ts +1 -0
- package/build/server/platform/orchestration/entitySubscriptions/index.d.ts.map +1 -1
- package/build/server/platform/orchestration/entitySubscriptions/index.js.map +1 -1
- package/build/server/products/chat/api/routeSupport.d.ts +9 -1
- package/build/server/products/chat/api/routeSupport.d.ts.map +1 -1
- package/build/server/products/chat/api/routeSupport.js +22 -0
- package/build/server/products/chat/api/routeSupport.js.map +1 -1
- package/build/server/products/chat/state/model/readModels.d.ts +1 -0
- package/build/server/products/chat/state/model/readModels.d.ts.map +1 -1
- package/build/server/products/chat/state/model/readModels.js +2 -1
- package/build/server/products/chat/state/model/readModels.js.map +1 -1
- package/build/server/products/shared/renderer/api/appShell.d.ts +1 -0
- package/build/server/products/shared/renderer/api/appShell.d.ts.map +1 -1
- package/build/server/products/shared/renderer/api/appShell.js +9 -0
- package/build/server/products/shared/renderer/api/appShell.js.map +1 -1
- package/build/server/runtime/client.d.ts.map +1 -1
- package/build/server/runtime/client.js +5 -1
- package/build/server/runtime/client.js.map +1 -1
- package/build/server/runtime/setup.d.ts +4 -0
- package/build/server/runtime/setup.d.ts.map +1 -1
- package/build/server/runtime/setup.js +3 -1
- package/build/server/runtime/setup.js.map +1 -1
- package/build/server/server/routes/providers.d.ts.map +1 -1
- package/build/server/server/routes/providers.js +21 -50
- package/build/server/server/routes/providers.js.map +1 -1
- package/build/server/shared/desktopRecoveryBridge.d.ts +2 -1
- package/build/server/shared/desktopRecoveryBridge.d.ts.map +1 -1
- package/build/server/shared/desktopRecoveryBridge.js.map +1 -1
- package/build/server/shared/providerCatalogData.d.ts +128 -57
- package/build/server/shared/providerCatalogData.d.ts.map +1 -1
- package/build/server/shared/providerCatalogData.js +51 -26
- package/build/server/shared/providerCatalogData.js.map +1 -1
- package/build/server/shared/providerRegistryWarnings.d.ts +4 -0
- package/build/server/shared/providerRegistryWarnings.d.ts.map +1 -1
- package/build/server/shared/providerRegistryWarnings.js +7 -0
- package/build/server/shared/providerRegistryWarnings.js.map +1 -1
- package/package.json +12 -5
- package/packages/provider-setup/manager.d.ts +79 -0
- package/packages/provider-setup/manager.js +441 -0
- package/scripts/README.md +23 -0
- package/scripts/build-desktop-installer.mjs +124 -16
- package/scripts/linux/install-github-cli.sh +5 -0
- package/scripts/linux/install-node.sh +9 -3
- package/scripts/linux/node-cli-common.sh +87 -48
- package/scripts/linux/provider-cli-common.sh +80 -105
- package/scripts/linux/provider-version-common.sh +247 -0
- package/scripts/linux/test-linux-package-smoke.sh +2 -0
- package/scripts/linux/verified-npm-update.sh +20 -0
- package/scripts/macos/install-github-cli.sh +5 -0
- package/scripts/macos/install-node.sh +9 -3
- package/scripts/macos/node-cli-common.sh +87 -48
- package/scripts/macos/provider-cli-common.sh +80 -105
- package/scripts/macos/provider-version-common.sh +247 -0
- package/scripts/macos/test-macos-package-smoke.sh +2 -0
- package/scripts/macos/verified-npm-update.sh +20 -0
- package/scripts/testing/conversation-navigation-smoke-electron.cjs +134 -0
- package/scripts/testing/conversation-navigation-smoke-renderer.tsx +102 -0
- package/scripts/testing/conversation-navigation-smoke.mjs +67 -0
- package/scripts/validate-release-assets.mjs +4 -1
- package/scripts/windows/Install-Antigravity.ps1 +26 -3
- package/scripts/windows/Install-ClaudeCode.ps1 +25 -7
- package/scripts/windows/Install-CursorAgent.ps1 +45 -30
- package/scripts/windows/Install-Devin.ps1 +16 -47
- package/scripts/windows/Install-GitHubCli.ps1 +4 -0
- package/scripts/windows/Install-Goose.ps1 +18 -1
- package/scripts/windows/Install-Grok.ps1 +14 -1
- package/scripts/windows/Install-Junie.ps1 +32 -11
- package/scripts/windows/Install-KiroCli.ps1 +37 -13
- package/scripts/windows/Install-MetaCli.ps1 +8 -1
- package/scripts/windows/Install-Node.ps1 +6 -1
- package/scripts/windows/Setup-NodeGlobalPrefix.ps1 +10 -0
- package/scripts/windows/Test-WindowsInstallerSmoke.ps1 +2 -0
- package/scripts/windows/_HiddenProcess.ps1 +14 -3
- package/scripts/windows/_NativeInstallerSupport.ps1 +308 -0
- package/scripts/windows/_NpmCliInstaller.ps1 +35 -14
- package/scripts/windows/_PackagedUninstall.ps1 +9 -1
- package/scripts/windows/_VerifiedNpmUpdate.ps1 +25 -0
- package/build/renderer/assets/ActiveSessionPermissionChip-CAgN-tXv.js +0 -1
- package/build/renderer/assets/app-BV3UAQkR.css +0 -1
- package/build/renderer/assets/app-CVPqqfZo.js +0 -45
- package/build/renderer/assets/conversationSidebarRecentEntries-iqaLqA1n.js +0 -6
- package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts +0 -48
- package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts.map +0 -1
- package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js +0 -535
- package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js.map +0 -1
- package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts +0 -13
- package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts.map +0 -1
- package/build/server/design/components/useProviderRegistryAutoRecheck.js +0 -70
- package/build/server/design/components/useProviderRegistryAutoRecheck.js.map +0 -1
- package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts +0 -19
- package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts.map +0 -1
- package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js +0 -53
- package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js.map +0 -1
- package/build/server/products/shared/renderer/api/providers.d.ts +0 -17
- package/build/server/products/shared/renderer/api/providers.d.ts.map +0 -1
- package/build/server/products/shared/renderer/api/providers.js +0 -50
- package/build/server/products/shared/renderer/api/providers.js.map +0 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Source this file for passive native version comparison and bounded vendor queries.
|
|
3
|
+
# Usage: source provider-version-common.sh; cats_native_latest_version <provider>
|
|
4
|
+
# Adapted from environment-bootstrap shared/posix/upstream-version.sh at 752dc13.
|
|
5
|
+
# Queries return an empty value on failure; check-only mode never invokes them.
|
|
6
|
+
cats_version_token() {
|
|
7
|
+
printf '%s' "$1" | tr -d '\r' | grep -oE '[0-9]+(\.[0-9]+)+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*|\.[A-Za-z][0-9A-Za-z]*(\.[0-9]+)*)?' | head -n 1 || true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# cats_version_compare <a> <b>
|
|
11
|
+
# 只比數字段、補滿四段:印 -1(a 較舊)、0、1(a 較新);任一邊解析不出版本印空字串。
|
|
12
|
+
# 空字串要當成「無法比較、交回原本流程」,不能當成落後,否則查不到版本時每次都會重裝。
|
|
13
|
+
cats_version_compare() {
|
|
14
|
+
local a b i x y
|
|
15
|
+
a="$(printf '%s' "$1" | tr -d '\r' | grep -oE '[0-9]+(\.[0-9]+)*' | head -n 1 || true)"
|
|
16
|
+
b="$(printf '%s' "$2" | tr -d '\r' | grep -oE '[0-9]+(\.[0-9]+)*' | head -n 1 || true)"
|
|
17
|
+
if [ -z "$a" ] || [ -z "$b" ]; then
|
|
18
|
+
printf ''
|
|
19
|
+
return 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
i=1
|
|
23
|
+
while [ "$i" -le 4 ]; do
|
|
24
|
+
x="$(printf '%s' "$a" | awk -F. -v n="$i" '{print $n}')"
|
|
25
|
+
y="$(printf '%s' "$b" | awk -F. -v n="$i" '{print $n}')"
|
|
26
|
+
[ -n "$x" ] || x=0
|
|
27
|
+
[ -n "$y" ] || y=0
|
|
28
|
+
if [ "$((10#$x))" -lt "$((10#$y))" ]; then printf -- '-1'; return 0; fi
|
|
29
|
+
if [ "$((10#$x))" -gt "$((10#$y))" ]; then printf '1'; return 0; fi
|
|
30
|
+
i=$((i + 1))
|
|
31
|
+
done
|
|
32
|
+
printf '0'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# _cats_json_string_field <field> :從 stdin 的 JSON 文字撈第一個 "field":"value" 的 value
|
|
36
|
+
_cats_json_string_field() {
|
|
37
|
+
grep -oE "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n 1 | sed -E 's/.*"([^"]*)"$/\1/'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# cats_github_latest_release <owner/repo>
|
|
41
|
+
# GitHub Releases 的最新穩定版 tag(去掉開頭的 v);失敗印空。
|
|
42
|
+
cats_github_latest_release() {
|
|
43
|
+
local json
|
|
44
|
+
json="$(curl -fsSL --connect-timeout 5 --max-time 20 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$1/releases/latest" 2>/dev/null)" || return 0
|
|
45
|
+
printf '%s' "$json" | _cats_json_string_field tag_name | sed 's/^v//' || true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# cats_antigravity_latest_version [已下載的官方 install.sh]
|
|
49
|
+
# 官方腳本讀的是 $DOWNLOAD_BASE_URL/manifests/<platform>.json 的 version。
|
|
50
|
+
# 傳入抓下來的腳本可以順便從裡面撈 base URL,官方搬家時就不必改這裡;撈不到用寫死的。
|
|
51
|
+
# platform 的算法照抄官方(darwin_arm64 / linux_amd64 / linux_amd64_musl …)。
|
|
52
|
+
cats_antigravity_latest_version() {
|
|
53
|
+
local base='https://antigravity-cli-auto-updater-974169037036.us-central1.run.app'
|
|
54
|
+
local found os arch platform
|
|
55
|
+
|
|
56
|
+
if [ -n "${1:-}" ] && [ -r "$1" ]; then
|
|
57
|
+
found="$(grep -oE 'DOWNLOAD_BASE_URL="https://[^"]+"' "$1" | head -n 1 | sed -E 's/^DOWNLOAD_BASE_URL="(.*)"$/\1/')"
|
|
58
|
+
[ -n "$found" ] && base="$found"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
case "$(uname -s)" in
|
|
62
|
+
Darwin) os=darwin ;;
|
|
63
|
+
Linux) os=linux ;;
|
|
64
|
+
*) return 0 ;;
|
|
65
|
+
esac
|
|
66
|
+
case "$(uname -m)" in
|
|
67
|
+
x86_64|amd64) arch=amd64 ;;
|
|
68
|
+
arm64|aarch64) arch=arm64 ;;
|
|
69
|
+
*) return 0 ;;
|
|
70
|
+
esac
|
|
71
|
+
if [ "$os" = linux ] && { [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; }; then
|
|
72
|
+
platform="linux_${arch}_musl"
|
|
73
|
+
else
|
|
74
|
+
platform="${os}_${arch}"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
curl -fsSL --connect-timeout 5 --max-time 20 "$base/manifests/$platform.json" 2>/dev/null | _cats_json_string_field version || true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# cats_cursor_agent_latest_version <已下載的官方 install 腳本>
|
|
81
|
+
# 官方 https://cursor.com/install 是支寫死版本的腳本,目錄名就是版本
|
|
82
|
+
# (…/versions/2026.09.10-fd3934a)。版本是「日期-commit」,cats_version_compare 只比得到
|
|
83
|
+
# 日期,同日不同 commit 由呼叫端用字串相等另判。
|
|
84
|
+
cats_cursor_agent_latest_version() {
|
|
85
|
+
[ -n "${1:-}" ] && [ -r "$1" ] || return 0
|
|
86
|
+
grep -oE 'versions/[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[0-9A-Za-z]+' "$1" | head -n 1 | sed 's#^versions/##' || true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# cats_junie_latest_build
|
|
90
|
+
# 官方 install.sh 讀 GitHub raw 的 update-info.jsonl:一行一個 build,version 是 build 號
|
|
91
|
+
# (3110.7,也是 versions/ 底下的目錄名)、marketing 才是 26.9.7、platform 是
|
|
92
|
+
# linux-amd64 / macos-aarch64 這種。印本平台最大的 build 號;失敗印空。
|
|
93
|
+
cats_junie_latest_build() {
|
|
94
|
+
local os arch platform jsonl line v best=''
|
|
95
|
+
|
|
96
|
+
case "$(uname -s)" in
|
|
97
|
+
Darwin) os=macos ;;
|
|
98
|
+
Linux) os=linux ;;
|
|
99
|
+
*) return 0 ;;
|
|
100
|
+
esac
|
|
101
|
+
case "$(uname -m)" in
|
|
102
|
+
x86_64|amd64) arch=amd64 ;;
|
|
103
|
+
arm64|aarch64) arch=aarch64 ;;
|
|
104
|
+
*) return 0 ;;
|
|
105
|
+
esac
|
|
106
|
+
platform="${os}-${arch}"
|
|
107
|
+
|
|
108
|
+
jsonl="$(curl -fsSL --connect-timeout 5 --max-time 20 'https://raw.githubusercontent.com/jetbrains-junie/junie/main/update-info.jsonl' 2>/dev/null)" || return 0
|
|
109
|
+
|
|
110
|
+
while IFS= read -r line; do
|
|
111
|
+
case "$line" in
|
|
112
|
+
*"\"platform\":\"$platform\""*) ;;
|
|
113
|
+
*) continue ;;
|
|
114
|
+
esac
|
|
115
|
+
v="$(printf '%s' "$line" | _cats_json_string_field version || true)"
|
|
116
|
+
[ -n "$v" ] || continue
|
|
117
|
+
if [ -z "$best" ] || [ "$(cats_version_compare "$v" "$best")" = "1" ]; then
|
|
118
|
+
best="$v"
|
|
119
|
+
fi
|
|
120
|
+
done <<< "$jsonl"
|
|
121
|
+
|
|
122
|
+
printf '%s' "$best"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# cats_kiro_latest_version
|
|
126
|
+
# Kiro 的官方 installer 讀 stable/latest/manifest.json(Windows 版 Install-KiroCLI.ps1
|
|
127
|
+
# 用的是同一份),欄位 version。
|
|
128
|
+
cats_kiro_latest_version() {
|
|
129
|
+
curl -fsSL --connect-timeout 5 --max-time 20 'https://prod.download.cli.kiro.dev/stable/latest/manifest.json' 2>/dev/null | _cats_json_string_field version || true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# cats_devin_latest_version
|
|
133
|
+
# Devin 的官方 install.sh 與 Windows 版 setup.ps1 讀的都是 cli/current/manifest.json,欄位
|
|
134
|
+
# version(3000.10.27)。官方腳本同版雖不重抓,POSIX 版仍會重建兩個符號連結、Windows 版更會
|
|
135
|
+
# 無條件重寫 180 MB 的 devin.exe;先問這裡,同版就完全不動。
|
|
136
|
+
cats_devin_latest_version() {
|
|
137
|
+
curl -fsSL --connect-timeout 5 --max-time 20 'https://static.devin.ai/cli/current/manifest.json' 2>/dev/null | _cats_json_string_field version || true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# cats_grok_latest_version [channel]
|
|
141
|
+
# Grok 的官方 install.sh(與 Windows 的 install.ps1)決定要下載哪一版,靠的只是一個純文字端點:
|
|
142
|
+
# https://x.ai/cli/<channel>(Cloudflare 前端)不通就退到 GCS 上的同名檔案,內容就是版本號
|
|
143
|
+
# (1.0.30)。這裡照抄同一套順序;channel 預設跟官方一樣吃 GROK_CHANNEL(stable)。
|
|
144
|
+
# 三平台都問這裡,刻意不用 `grok update --check --json`:那要執行 grok 本體去連 x.ai 做 TLS
|
|
145
|
+
# 交握,而 grok 的 TLS 實作用到 SHA-512 硬體指令(sha512su0),沒有這個擴充的 aarch64
|
|
146
|
+
# (Raspberry Pi 4/5 的 Cortex-A72/A76)會當場 SIGILL、輸出全空 —— 見 CLAUDE.md Known Gotchas。
|
|
147
|
+
# 這個端點不必執行任何工具,問到的也正是安裝器接下來會抓的版本。失敗印空。
|
|
148
|
+
cats_grok_latest_version() {
|
|
149
|
+
local channel="${1:-${GROK_CHANNEL:-stable}}" url v
|
|
150
|
+
for url in "https://x.ai/cli/${channel}" "https://storage.googleapis.com/grok-build-public-artifacts/cli/${channel}"; do
|
|
151
|
+
v="$(curl -fsSL --connect-timeout 5 --max-time 20 "$url" 2>/dev/null | tr -d '\r' | head -n 1 | tr -d '[:space:]' || true)"
|
|
152
|
+
if printf '%s' "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._]+)?$'; then
|
|
153
|
+
printf '%s' "$v"
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
done
|
|
157
|
+
printf ''
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
cats_native_latest_version() {
|
|
162
|
+
local script
|
|
163
|
+
case "$1" in
|
|
164
|
+
antigravity) cats_antigravity_latest_version ;;
|
|
165
|
+
cursor)
|
|
166
|
+
script="$(curl -fsSL --connect-timeout 5 --max-time 20 https://cursor.com/install 2>/dev/null)" || return 0
|
|
167
|
+
printf '%s' "$script" | grep -oE 'versions/[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[0-9A-Za-z]+' | head -n 1 | sed 's#^versions/##' || true
|
|
168
|
+
;;
|
|
169
|
+
goose) cats_github_latest_release aaif-goose/goose ;;
|
|
170
|
+
junie) cats_junie_latest_build ;;
|
|
171
|
+
kiro) cats_kiro_latest_version ;;
|
|
172
|
+
devin) cats_devin_latest_version ;;
|
|
173
|
+
grok) cats_grok_latest_version ;;
|
|
174
|
+
*) return 0 ;;
|
|
175
|
+
esac
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
cats_native_version_token() {
|
|
179
|
+
if [ "$1" = junie ]; then
|
|
180
|
+
printf '%s' "$2" | sed -nE 's/.*\(([0-9]+(\.[0-9]+)+)\).*/\1/p' | head -n 1 || true
|
|
181
|
+
else
|
|
182
|
+
cats_version_token "$2"
|
|
183
|
+
fi
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Print current/older/unknown; Cursor same-day hashes are distinct releases.
|
|
187
|
+
cats_native_version_state() {
|
|
188
|
+
local current latest comparison
|
|
189
|
+
current="$(cats_native_version_token "$1" "$2")"
|
|
190
|
+
latest="$3"
|
|
191
|
+
[ -n "$current" ] && [ -n "$latest" ] || { printf unknown; return 0; }
|
|
192
|
+
comparison="$(cats_version_compare "$current" "$latest")"
|
|
193
|
+
if [ "$comparison" = 1 ] || { [ "$comparison" = 0 ] && { [ "$1" != cursor ] || [ "$current" = "$latest" ]; }; }; then
|
|
194
|
+
printf current
|
|
195
|
+
elif [ -n "$comparison" ]; then printf older
|
|
196
|
+
else printf unknown; fi
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Delete only older numeric version directories in a known installer-owned tree.
|
|
200
|
+
# Current/pending/newer builds, symlinks and custom locations outside HOME remain.
|
|
201
|
+
cats_cleanup_native_versions() {
|
|
202
|
+
local provider="$1" version="$2" root='' data='' keep='' actual='' directory='' name='' ancestor=''
|
|
203
|
+
keep="$(cats_native_version_token "$provider" "$version")"
|
|
204
|
+
[ -n "$keep" ] || return 0
|
|
205
|
+
case "$provider" in
|
|
206
|
+
cursor)
|
|
207
|
+
root="$HOME/.local/share/cursor-agent/versions"
|
|
208
|
+
[ -L "$HOME/.local/bin/cursor-agent" ] || return 0
|
|
209
|
+
actual="$(readlink "$HOME/.local/bin/cursor-agent")" || return 0
|
|
210
|
+
[ "$(basename "$(dirname "$actual")")" = "$keep" ] || return 0
|
|
211
|
+
;;
|
|
212
|
+
devin)
|
|
213
|
+
root="${XDG_DATA_HOME:-$HOME/.local/share}/devin/cli/_versions"
|
|
214
|
+
[ -L "$root/current" ] || return 0
|
|
215
|
+
actual="$(readlink "$root/current")" || return 0
|
|
216
|
+
[ "$(basename "$actual")" = "$keep" ] || return 0
|
|
217
|
+
;;
|
|
218
|
+
junie)
|
|
219
|
+
data="${JUNIE_DATA_DIR:-$HOME/.local/share/junie}"
|
|
220
|
+
root="$data/versions"
|
|
221
|
+
[ ! -e "$data/updates/pending-update.json" ] && [ -L "$data/current" ] || return 0
|
|
222
|
+
actual="$(readlink "$data/current")" || return 0
|
|
223
|
+
[ "$(basename "$actual")" = "$keep" ] || return 0
|
|
224
|
+
;;
|
|
225
|
+
*) return 0 ;;
|
|
226
|
+
esac
|
|
227
|
+
case "$root" in "$HOME"/*) ;; *) return 0 ;; esac
|
|
228
|
+
[ -d "$root" ] || return 0
|
|
229
|
+
ancestor="$root"
|
|
230
|
+
while [ "$ancestor" != / ] && [ "$ancestor" != . ]; do
|
|
231
|
+
[ ! -L "$ancestor" ] || return 0
|
|
232
|
+
ancestor="$(dirname "$ancestor")"
|
|
233
|
+
done
|
|
234
|
+
local canonical_home
|
|
235
|
+
canonical_home="$(cd -P -- "$HOME" && pwd -P)" || return 0
|
|
236
|
+
root="$(cd -P -- "$root" && pwd -P)" || return 0
|
|
237
|
+
case "$root" in "$canonical_home"/*) ;; *) return 0 ;; esac
|
|
238
|
+
for directory in "$root"/*; do
|
|
239
|
+
[ -d "$directory" ] && [ ! -L "$directory" ] || continue
|
|
240
|
+
name="$(basename "$directory")"
|
|
241
|
+
printf '%s' "$name" | grep -qE '^[0-9]+(\.[0-9]+)+(-[0-9A-Za-z]+)?$' || continue
|
|
242
|
+
[ "$(cats_version_compare "$name" "$keep")" = -1 ] || continue
|
|
243
|
+
# Skip directory trees with links rather than recurse through an unknown layout.
|
|
244
|
+
[ -z "$(find "$directory" -type l -print 2>/dev/null)" ] || continue
|
|
245
|
+
rm -rf -- "$directory" || printf 'Could not remove old %s build %s.\n' "$provider" "$name" >&2
|
|
246
|
+
done
|
|
247
|
+
}
|
|
@@ -66,6 +66,8 @@ assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/install-goose.sh" 'bun
|
|
|
66
66
|
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/install-junie.sh" 'bundled macOS Junie installer helper'
|
|
67
67
|
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/install-kiro-cli.sh" 'bundled macOS Kiro installer helper'
|
|
68
68
|
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/check-installation.sh" 'bundled macOS readiness audit helper'
|
|
69
|
+
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/provider-version-common.sh" 'bundled native version library'
|
|
70
|
+
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/verified-npm-update.sh" 'bundled npm verification library'
|
|
69
71
|
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/provider-cli-common.sh" 'bundled macOS provider helper library'
|
|
70
72
|
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/macos/node-cli-common.sh" 'bundled macOS npm helper library'
|
|
71
73
|
assert_file "${RESOURCES_ROOT}/desktop/setup-assets/manifest.json" 'bundled setup-assets manifest'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: source verified-npm-update.sh; cats_upgrade_npm
|
|
3
|
+
# Explicit npm upgrade only. Adapted from environment-bootstrap 4690075.
|
|
4
|
+
# Preserve the current prefix (including nvm), enforce engines, verify command and package.
|
|
5
|
+
cats_upgrade_npm() {
|
|
6
|
+
local current latest installed root
|
|
7
|
+
current="$(npm --version)" || { printf 'Could not read npm version.\n' >&2; return 1; }
|
|
8
|
+
latest="$(npm view npm@latest version --fetch-retries=0 --fetch-timeout=10000 --loglevel=error)" || { printf 'npm version query failed.\n' >&2; return 1; }
|
|
9
|
+
[[ "$latest" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { printf 'Invalid npm version metadata.\n' >&2; return 1; }
|
|
10
|
+
[ "$current" != "$latest" ] || return 0
|
|
11
|
+
[ "$(cats_version_compare "$current" "$latest")" != 1 ] || return 0
|
|
12
|
+
npm install -g "npm@$latest" --engine-strict --fetch-retries=0 --fetch-timeout=30000 || return 1
|
|
13
|
+
hash -r
|
|
14
|
+
installed="$(npm --version)" && root="$(npm root -g)" || return 1
|
|
15
|
+
[ "$installed" = "$latest" ] && node -e '
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const pkg = require(path.join(process.argv[1], "npm", "package.json"));
|
|
18
|
+
process.exit(pkg.version === process.argv[2] ? 0 : 1);
|
|
19
|
+
' "$root" "$latest" || { printf 'npm verification failed. Check PATH and prefix.\n' >&2; return 1; }
|
|
20
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Usage: invoked by conversation-navigation-smoke.mjs; never run against real state.
|
|
2
|
+
// CATS_NAV_SMOKE_URL selects the fixture server, PROFILE isolates Electron state,
|
|
3
|
+
// and OUTPUT selects the screenshot/result directory. No Desktop supervisor loads.
|
|
4
|
+
const { app, BrowserWindow } = require('electron');
|
|
5
|
+
const { writeFile } = require('node:fs/promises');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
app.setPath('userData', process.env.CATS_NAV_SMOKE_PROFILE);
|
|
8
|
+
app.whenReady().then(async () => {
|
|
9
|
+
const window = new BrowserWindow({ width: 1440, height: 1000, show: false,
|
|
10
|
+
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false, offscreen: true } });
|
|
11
|
+
window.webContents.setFrameRate(60);
|
|
12
|
+
const errors = [];
|
|
13
|
+
window.webContents.on('console-message', (event) => { if (event.level === 'error') errors.push(event.message); });
|
|
14
|
+
try {
|
|
15
|
+
await window.loadURL(process.env.CATS_NAV_SMOKE_URL);
|
|
16
|
+
const result = await window.webContents.executeJavaScript(`(async () => {
|
|
17
|
+
const wait = async (predicate, label) => {
|
|
18
|
+
const started = performance.now();
|
|
19
|
+
while (!predicate()) {
|
|
20
|
+
if (performance.now() - started > 10000) throw new Error('Timeout: ' + label + '\\n' + document.body.innerText);
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
22
|
+
}
|
|
23
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
24
|
+
};
|
|
25
|
+
await wait(() => document.body.innerText.includes('Transcript 1'), 'initial Chat render');
|
|
26
|
+
const fixture = window.__navigationSmoke;
|
|
27
|
+
const click = (title) => {
|
|
28
|
+
const row = [...document.querySelectorAll('button,a,[role="button"],span')].find(el => el.textContent.trim() === title);
|
|
29
|
+
if (!row) throw new Error('Missing navigation control: ' + title);
|
|
30
|
+
row.click();
|
|
31
|
+
};
|
|
32
|
+
fixture.click = click;
|
|
33
|
+
fixture.wait = wait;
|
|
34
|
+
fixture.delays.shell = 60000;
|
|
35
|
+
fixture.delays.preference = 2000;
|
|
36
|
+
const timings = [];
|
|
37
|
+
const checks = [];
|
|
38
|
+
for (const index of [1, 2, 3]) {
|
|
39
|
+
const start = performance.now(); click('Conversation ' + (index + 1));
|
|
40
|
+
await wait(() => document.body.innerText.includes('Transcript ' + (index + 1)), 'cold conversation ' + index);
|
|
41
|
+
timings.push({ kind: 'cold', conversation: index + 1, ms: performance.now() - start });
|
|
42
|
+
}
|
|
43
|
+
fixture.delays.snapshot = 2000;
|
|
44
|
+
for (const index of [0, 2, 1, 3, 0]) {
|
|
45
|
+
const start = performance.now(); click('Conversation ' + (index + 1));
|
|
46
|
+
await wait(() => document.body.innerText.includes('Transcript ' + (index + 1)), 'warm conversation ' + index);
|
|
47
|
+
const ms = performance.now() - start;
|
|
48
|
+
if (ms >= 1500) throw new Error('Warm navigation waited for a delayed response: ' + ms);
|
|
49
|
+
timings.push({ kind: 'warm', conversation: index + 1, ms });
|
|
50
|
+
}
|
|
51
|
+
fixture.pushReply(fixture.ids[0], 'Background reply received');
|
|
52
|
+
await wait(() => document.body.innerText.includes('Background reply received'), 'subscription update');
|
|
53
|
+
click('Conversation 2');
|
|
54
|
+
await wait(() => document.body.innerText.includes('Transcript 2'), 'leave updated conversation');
|
|
55
|
+
click('Conversation 1');
|
|
56
|
+
await wait(() => document.body.innerText.includes('Background reply received'), 'retained updated transcript');
|
|
57
|
+
const type = (value) => {
|
|
58
|
+
const input = document.querySelector('textarea');
|
|
59
|
+
if (!input) throw new Error('Missing composer textarea');
|
|
60
|
+
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(input, value);
|
|
61
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
62
|
+
};
|
|
63
|
+
type('Keep conversation one draft');
|
|
64
|
+
await wait(() => document.querySelector('textarea')?.value === 'Keep conversation one draft', 'composer edit');
|
|
65
|
+
click('New Chat');
|
|
66
|
+
await wait(() => location.pathname.endsWith('/new'), 'new chat');
|
|
67
|
+
if (document.querySelector('textarea')?.value) throw new Error('New chat inherited conversation draft');
|
|
68
|
+
click('Conversation 1');
|
|
69
|
+
await wait(() => document.querySelector('textarea')?.value === 'Keep conversation one draft', 'restore conversation draft after New Chat');
|
|
70
|
+
checks.push('cold/warm A/B/C/D navigation', 'background update retention', 'conversation draft after New Chat');
|
|
71
|
+
fixture.delays.snapshot = 20;
|
|
72
|
+
for (const serverClose of [false, true]) {
|
|
73
|
+
const body = serverClose ? 'Recovered server read failure' : 'Recovered disconnected stream';
|
|
74
|
+
fixture.disconnect(fixture.ids[0], serverClose);
|
|
75
|
+
if (!document.body.innerText.includes('Transcript 1')) throw new Error('Disconnect cleared retained transcript');
|
|
76
|
+
fixture.pushReply(fixture.ids[0], body, false);
|
|
77
|
+
await wait(() => document.body.innerText.includes(body), body);
|
|
78
|
+
}
|
|
79
|
+
checks.push('network and server-error reconnection');
|
|
80
|
+
click('Group Chat');
|
|
81
|
+
await wait(() => location.pathname.includes('/new'), 'group draft');
|
|
82
|
+
checks.push({ mode: 'group draft', path: location.pathname, text: document.body.innerText });
|
|
83
|
+
click('Parallel Chat');
|
|
84
|
+
await wait(() => location.pathname.includes('/new'), 'parallel draft');
|
|
85
|
+
checks.push({ mode: 'parallel draft', path: location.pathname, text: document.body.innerText });
|
|
86
|
+
click('group fixture');
|
|
87
|
+
await wait(() => document.body.innerText.includes('group transcript'), 'group room');
|
|
88
|
+
checks.push({ mode: 'group room', text: document.body.innerText });
|
|
89
|
+
click('Fixture Cat Two');
|
|
90
|
+
await wait(() => document.body.innerText.includes('direct transcript'), 'direct lane');
|
|
91
|
+
checks.push({ mode: 'direct lane', text: document.body.innerText });
|
|
92
|
+
click('Conversation 1');
|
|
93
|
+
await wait(() => document.querySelector('textarea')?.value === 'Keep conversation one draft', 'draft after other chat modes');
|
|
94
|
+
return { timings, checks, calls: fixture.calls, text: document.body.innerText };
|
|
95
|
+
})()`);
|
|
96
|
+
result.errors = errors;
|
|
97
|
+
await writeFile(path.join(process.env.CATS_NAV_SMOKE_OUTPUT, 'result.json'), JSON.stringify(result, null, 2));
|
|
98
|
+
await writeFile(path.join(process.env.CATS_NAV_SMOKE_OUTPUT, 'chat.png'), (await window.webContents.capturePage()).toPNG());
|
|
99
|
+
for (const [control, marker, name] of [
|
|
100
|
+
['Group Chat', 'Add another model to collaborate', 'group-draft'],
|
|
101
|
+
['Parallel Chat', 'Follows lead', 'parallel-draft'],
|
|
102
|
+
['group fixture', 'group transcript', 'group-room'],
|
|
103
|
+
['Fixture Cat Two', 'direct transcript', 'direct-lane'],
|
|
104
|
+
]) {
|
|
105
|
+
await window.webContents.executeJavaScript(`(async () => {
|
|
106
|
+
const fixture = window.__navigationSmoke;
|
|
107
|
+
fixture.click(${JSON.stringify(control)});
|
|
108
|
+
await fixture.wait(() => document.body.innerText.includes(${JSON.stringify(marker)}), ${JSON.stringify(name)});
|
|
109
|
+
})()`);
|
|
110
|
+
await writeFile(path.join(process.env.CATS_NAV_SMOKE_OUTPUT, `${name}.png`), (await window.webContents.capturePage()).toPNG());
|
|
111
|
+
}
|
|
112
|
+
await window.webContents.executeJavaScript(`(async () => {
|
|
113
|
+
document.querySelector('.recentOverflowButton').click();
|
|
114
|
+
await window.__navigationSmoke.wait(() => document.querySelector('.recentOverflowMenu'), 'sidebar overflow');
|
|
115
|
+
const menu = document.querySelector('.recentOverflowMenu');
|
|
116
|
+
const bounds = menu.getBoundingClientRect();
|
|
117
|
+
const topElement = document.elementFromPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
|
|
118
|
+
if (!menu.contains(topElement)) throw new Error('Sidebar overflow is obscured');
|
|
119
|
+
})()`);
|
|
120
|
+
await writeFile(path.join(process.env.CATS_NAV_SMOKE_OUTPUT, 'sidebar-overflow.png'), (await window.webContents.capturePage()).toPNG());
|
|
121
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
122
|
+
window.webContents.stopPainting();
|
|
123
|
+
window.destroy();
|
|
124
|
+
app.quit();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
await writeFile(path.join(process.env.CATS_NAV_SMOKE_OUTPUT, 'failure.png'), (await window.webContents.capturePage()).toPNG());
|
|
127
|
+
console.error(error);
|
|
128
|
+
console.error(errors.join('\n'));
|
|
129
|
+
window.webContents.stopPainting();
|
|
130
|
+
window.destroy();
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
app.quit();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Isolated production Chat renderer. All services below use synthetic fixture data.
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
5
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
6
|
+
import { GuideCatPlacementProvider } from '../../src/app/renderer/GuideCatPlacementProvider.js';
|
|
7
|
+
import { sharedQueryClient } from '../../src/products/shared/renderer/queryClient.js';
|
|
8
|
+
import type { AppShellPayload } from '../../src/products/shared/api/workspaceContracts.js';
|
|
9
|
+
import type { ChannelSubscriptionState } from '../../src/products/shared/renderer/entitySubscriptionChannelDispatcher.js';
|
|
10
|
+
import '../../src/design/index.css';
|
|
11
|
+
|
|
12
|
+
const fixture = await (await fetch('/fixture.json')).json();
|
|
13
|
+
const ids: string[] = fixture.ids;
|
|
14
|
+
const payloads = fixture.payloads as Record<string, AppShellPayload>;
|
|
15
|
+
const snapshots = fixture.snapshots as Record<string, ChannelSubscriptionState>;
|
|
16
|
+
const calls: Array<{ url: string; method: string; at: number }> = [];
|
|
17
|
+
const sources = new Set<FixtureEventSource>();
|
|
18
|
+
const delays = { shell: 0, preference: 2_000, snapshot: 20 };
|
|
19
|
+
let selected = ids[0];
|
|
20
|
+
let shellCalls = 0;
|
|
21
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
|
|
23
|
+
class FixtureEventSource extends EventTarget {
|
|
24
|
+
onerror: (() => void) | null = null;
|
|
25
|
+
closed = false;
|
|
26
|
+
constructor(readonly url: string) {
|
|
27
|
+
super(); sources.add(this);
|
|
28
|
+
calls.push({ url, method: 'SSE', at: performance.now() });
|
|
29
|
+
const params = new URL(url, location.origin).searchParams;
|
|
30
|
+
const id = params.get('id');
|
|
31
|
+
if (params.get('kind') === 'channel' && id) {
|
|
32
|
+
void sleep(delays.snapshot).then(() => {
|
|
33
|
+
if (!this.closed) this.emit('snapshot', { kind: 'channel', id, version: 1, state: snapshots[id] });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
emit(type: string, data: unknown) { this.dispatchEvent(new MessageEvent(type, { data: JSON.stringify(data) })); }
|
|
38
|
+
close() { this.closed = true; sources.delete(this); }
|
|
39
|
+
}
|
|
40
|
+
Object.defineProperty(globalThis, 'EventSource', { value: FixtureEventSource, configurable: true });
|
|
41
|
+
globalThis.fetch = async (input, init) => {
|
|
42
|
+
const url = String(input); calls.push({ url, method: init?.method ?? 'GET', at: performance.now() });
|
|
43
|
+
if (url === '/api/app-shell') {
|
|
44
|
+
if (shellCalls++ > 0) await sleep(delays.shell);
|
|
45
|
+
return Response.json(payloads[selected]);
|
|
46
|
+
}
|
|
47
|
+
if (url === '/api/preferences') {
|
|
48
|
+
await sleep(delays.preference);
|
|
49
|
+
selected = JSON.parse(String(init?.body)).selectedChannelId ?? selected;
|
|
50
|
+
return Response.json({ preferences: { selectedChannelId: selected } });
|
|
51
|
+
}
|
|
52
|
+
if (url === '/api/core') return Response.json(fixture.core);
|
|
53
|
+
if (url === '/api/core/approvals') return Response.json({ approvals: [] });
|
|
54
|
+
if (url === '/api/providers') return Response.json({ state: 'ready', warnings: [], providers: ids.map((id, index) => {
|
|
55
|
+
const channel = snapshots[id].selectedChannel;
|
|
56
|
+
return { id: channel.pendingProvider, label: channel.pendingProvider,
|
|
57
|
+
defaultModel: channel.pendingModel, defaultInstance: 'native', defaultBackend: 'cli',
|
|
58
|
+
instances: [{ id: 'native', label: 'Native', target: 'cli/native', backend: 'cli', default: true }],
|
|
59
|
+
modelsPath: `/api/providers/${channel.pendingProvider}/models` };
|
|
60
|
+
}) });
|
|
61
|
+
const provider = /^\/api\/providers\/([^/]+)\/models/u.exec(url)?.[1];
|
|
62
|
+
if (provider) {
|
|
63
|
+
const channel = Object.values(snapshots).find((snapshot) => snapshot.selectedChannel.pendingProvider === provider)!.selectedChannel;
|
|
64
|
+
const model = { id: channel.pendingModel, label: channel.pendingModel, default: true };
|
|
65
|
+
const catalog = { provider, backend: 'cli', instance: 'cli/native', source: 'dynamic', cache: null,
|
|
66
|
+
defaultModel: channel.pendingModel, models: [model], entries: [model], presets: [], warnings: [],
|
|
67
|
+
defaultSelection: channel.pendingModelSelection, support: { tier: 'full', notes: [] },
|
|
68
|
+
controls: [{ key: `${provider}.reasoning_effort`, label: 'Reasoning effort', kind: 'enum', scope: 'both',
|
|
69
|
+
values: ['low', 'medium', 'high', 'max'].map((value) => ({ value, label: value })) }] };
|
|
70
|
+
return Response.json({ catalog });
|
|
71
|
+
}
|
|
72
|
+
if (/\/api\/channels\/[^/]+$/u.test(url) && init?.method === 'PATCH') {
|
|
73
|
+
const id = url.split('/').at(-1)!;
|
|
74
|
+
Object.assign(snapshots[id].selectedChannel, JSON.parse(String(init.body)));
|
|
75
|
+
return Response.json({ channel: payloads[id].chat.channels.find((channel) => channel.id === id) });
|
|
76
|
+
}
|
|
77
|
+
return Response.json({});
|
|
78
|
+
};
|
|
79
|
+
(window as any).__navigationSmoke = { ids, calls, delays, payloads, snapshots,
|
|
80
|
+
disconnect(id: string, serverClose = false) {
|
|
81
|
+
for (const source of sources) {
|
|
82
|
+
if (!source.url.includes(`id=${id}`)) continue;
|
|
83
|
+
if (serverClose) source.emit('close', { reason: 'Temporary fixture read failure', retryable: true });
|
|
84
|
+
else source.onerror?.();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
pushReply(id: string, body: string, notify = true) {
|
|
88
|
+
const state = snapshots[id];
|
|
89
|
+
state.selectedChannel.messages.push({ ...state.selectedChannel.messages.at(-1)!, id: crypto.randomUUID(), body });
|
|
90
|
+
for (const source of sources) {
|
|
91
|
+
if (notify && source.url.includes(`id=${id}`)) source.emit('patch', { kind: 'channel', id, version: 1, patch: { kind: 'message.appended', state } });
|
|
92
|
+
}
|
|
93
|
+
} };
|
|
94
|
+
const { default: App } = await import('../../src/products/chat/renderer/App.js');
|
|
95
|
+
createRoot(document.getElementById('root')!).render(
|
|
96
|
+
<QueryClientProvider client={sharedQueryClient}><BrowserRouter useTransitions={false}>
|
|
97
|
+
<GuideCatPlacementProvider guideCat={null} placement="docked" floatingAnchor={null}
|
|
98
|
+
sidecarMode="auto" onPersistSeen={() => {}} onCommit={() => {}}>
|
|
99
|
+
<Routes><Route path="/chat/*" element={<App />} /></Routes>
|
|
100
|
+
</GuideCatPlacementProvider>
|
|
101
|
+
</BrowserRouter></QueryClientProvider>,
|
|
102
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage: node scripts/testing/conversation-navigation-smoke.mjs [--help]
|
|
3
|
+
* Builds an isolated production Chat renderer and verifies navigation in hidden
|
|
4
|
+
* Electron/Chromium. All records and services are synthetic; user data is untouched.
|
|
5
|
+
* Writes build/conversation-navigation-smoke/result.json and a screenshot.
|
|
6
|
+
*/
|
|
7
|
+
import { build } from 'esbuild';
|
|
8
|
+
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { once } from 'node:events';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import electron from 'electron';
|
|
16
|
+
|
|
17
|
+
if (process.argv.includes('--help')) {
|
|
18
|
+
console.log('Usage: node scripts/testing/conversation-navigation-smoke.mjs\nSynthetic Chat navigation verification in hidden Electron. Results: build/conversation-navigation-smoke/.');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
const out = path.resolve('build/conversation-navigation-smoke');
|
|
22
|
+
const temporary = await mkdtemp(path.join(tmpdir(), 'cats-navigation-browser-'));
|
|
23
|
+
await mkdir(out, { recursive: true });
|
|
24
|
+
await build({ entryPoints: ['tests/fixtures/conversationNavigation.ts'], outfile: path.join(out, 'fixture.mjs'),
|
|
25
|
+
platform: 'node', format: 'esm', bundle: true, packages: 'external' });
|
|
26
|
+
await build({ entryPoints: ['scripts/testing/conversation-navigation-smoke-renderer.tsx'], outfile: path.join(out, 'renderer.js'),
|
|
27
|
+
platform: 'browser', format: 'esm', bundle: true, define: { 'process.env.NODE_ENV': '"production"' },
|
|
28
|
+
loader: { '.woff2': 'file', '.woff': 'file', '.png': 'file', '.svg': 'file' } });
|
|
29
|
+
const { createConversationNavigationFixture } = await import(pathToFileURL(path.join(out, 'fixture.mjs')));
|
|
30
|
+
const fixture = await createConversationNavigationFixture(temporary, true);
|
|
31
|
+
const allIds = fixture.state.channels.map((channel) => channel.id);
|
|
32
|
+
const body = JSON.stringify({ ids: fixture.ids, payloads: Object.fromEntries(allIds.map((id) => [id, fixture.payload(id)])),
|
|
33
|
+
snapshots: Object.fromEntries(allIds.map((id) => [id, fixture.snapshot(id)])), core: await fixture.store.readCore() });
|
|
34
|
+
const server = createServer(async (request, response) => {
|
|
35
|
+
try {
|
|
36
|
+
if (request.url === '/fixture.json') { response.setHeader('content-type', 'application/json'); response.end(body); return; }
|
|
37
|
+
const name = path.basename(new URL(request.url, 'http://localhost').pathname);
|
|
38
|
+
if (/\.(?:js|css|woff2?|png|svg)$/u.test(name)) {
|
|
39
|
+
response.setHeader('content-type', name.endsWith('.js') ? 'text/javascript' : name.endsWith('.css') ? 'text/css' : 'application/octet-stream');
|
|
40
|
+
response.end(await readFile(path.join(out, name))); return;
|
|
41
|
+
}
|
|
42
|
+
response.setHeader('content-type', 'text/html');
|
|
43
|
+
response.end('<!doctype html><html><head><link rel="stylesheet" href="/renderer.css"></head><body><div id="root"></div><script type="module" src="/renderer.js"></script></body></html>');
|
|
44
|
+
} catch (error) { response.writeHead(500); response.end(String(error)); }
|
|
45
|
+
});
|
|
46
|
+
server.listen(0, '127.0.0.1');
|
|
47
|
+
await once(server, 'listening');
|
|
48
|
+
const url = `http://127.0.0.1:${server.address().port}/chat/chats/${fixture.ids[0]}`;
|
|
49
|
+
const childEnv = { ...process.env, CATS_NAV_SMOKE_URL: url,
|
|
50
|
+
CATS_NAV_SMOKE_PROFILE: path.join(temporary, 'electron'), CATS_NAV_SMOKE_OUTPUT: out };
|
|
51
|
+
delete childEnv.ELECTRON_RUN_AS_NODE;
|
|
52
|
+
const child = spawn(electron, ['scripts/testing/conversation-navigation-smoke-electron.cjs'], {
|
|
53
|
+
windowsHide: true, stdio: 'inherit', env: childEnv,
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const [code] = await once(child, 'exit');
|
|
57
|
+
if (code !== 0) throw new Error(`Navigation browser verification exited ${code}`);
|
|
58
|
+
const result = JSON.parse(await readFile(path.join(out, 'result.json'), 'utf8'));
|
|
59
|
+
console.log(JSON.stringify({ timings: result.timings,
|
|
60
|
+
checks: result.checks.map((check) => typeof check === 'string' ? check : check.mode), errors: result.errors }, null, 2));
|
|
61
|
+
} finally {
|
|
62
|
+
server.closeAllConnections();
|
|
63
|
+
await new Promise((resolve) => server.close(resolve));
|
|
64
|
+
if (path.dirname(path.resolve(temporary)) !== path.resolve(tmpdir())
|
|
65
|
+
|| !path.basename(temporary).startsWith('cats-navigation-browser-')) throw new Error('Unexpected smoke cleanup path');
|
|
66
|
+
await rm(temporary, { recursive: true, force: true });
|
|
67
|
+
}
|
|
@@ -34,7 +34,10 @@ const PROJECT_ROOT = resolve(dirname(SCRIPT_PATH), '..');
|
|
|
34
34
|
*/
|
|
35
35
|
export const DESKTOP_RELEASE_MATRIX = [
|
|
36
36
|
{ platform: 'windows', formats: ['nsis'], arches: ['x64'] },
|
|
37
|
-
// macOS ships x64 only
|
|
37
|
+
// macOS ships x64 only: there is no macOS arm64 test environment, so a
|
|
38
|
+
// universal build would ship a slice nobody executed before release. Apple
|
|
39
|
+
// Silicon runs this under Rosetta 2. SPEC-111 section 8 records the exit
|
|
40
|
+
// condition for revisiting it.
|
|
38
41
|
{ platform: 'macos', formats: ['dmg', 'zip'], arches: ['x64'] },
|
|
39
42
|
// Linux ships an arm64 .deb. electron-updater installs it with dpkg, which
|
|
40
43
|
// needs elevation -- unlike the per-user Windows path.
|