feat: add beta release channel
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"main": "index.ts",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "APP_VARIANT=development expo start",
|
||||
@@ -31,9 +31,9 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@floating-ui/react-native": "^0.10.7",
|
||||
"@getpaseo/expo-two-way-audio": "0.1.60-rc.3",
|
||||
"@getpaseo/highlight": "0.1.60-rc.3",
|
||||
"@getpaseo/server": "0.1.60-rc.3",
|
||||
"@getpaseo/expo-two-way-audio": "0.1.60-beta.1",
|
||||
"@getpaseo/highlight": "0.1.60-beta.1",
|
||||
"@getpaseo/server": "0.1.60-beta.1",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
|
||||
@@ -35,12 +35,13 @@ export function SegmentedControl<T extends string>({
|
||||
testID,
|
||||
}: SegmentedControlProps<T>) {
|
||||
const { theme } = useUnistyles();
|
||||
const containerSizeStyle = size === "sm" ? styles.containerSm : styles.containerMd;
|
||||
const segmentSizeStyle = size === "sm" ? styles.segmentSm : styles.segmentMd;
|
||||
const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd;
|
||||
const iconSize = size === "sm" ? theme.iconSize.sm : theme.iconSize.md;
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]} testID={testID}>
|
||||
<View style={[styles.container, containerSizeStyle, style]} testID={testID}>
|
||||
{options.map((option) => {
|
||||
const isSelected = option.value === value;
|
||||
const iconColor = isSelected ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
@@ -89,39 +90,46 @@ export function SegmentedControl<T extends string>({
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
alignItems: "stretch",
|
||||
maxWidth: "100%",
|
||||
gap: 4,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
containerSm: {
|
||||
padding: 2,
|
||||
},
|
||||
containerMd: {
|
||||
padding: 3,
|
||||
},
|
||||
segment: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: "transparent",
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
segmentSm: {
|
||||
paddingVertical: theme.spacing[1],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
segmentMd: {
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[6],
|
||||
},
|
||||
segmentSelected: {
|
||||
backgroundColor: theme.colors.surface3,
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
segmentHover: {
|
||||
backgroundColor: theme.colors.surface3,
|
||||
borderColor: theme.colors.borderAccent,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
segmentPressed: {
|
||||
backgroundColor: theme.colors.surface3,
|
||||
borderColor: theme.colors.borderAccent,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
segmentDisabled: {
|
||||
opacity: theme.opacity[50],
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface DesktopAppUpdateInstallResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type DesktopReleaseChannel = "stable" | "beta";
|
||||
|
||||
export interface LocalDaemonUpdateResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
@@ -69,8 +71,12 @@ export async function getLocalDaemonVersion(): Promise<LocalDaemonVersionResult>
|
||||
return parseLocalDaemonVersionResult(result);
|
||||
}
|
||||
|
||||
export async function checkDesktopAppUpdate(): Promise<DesktopAppUpdateCheckResult> {
|
||||
const result = await invokeDesktopCommand<unknown>("check_app_update");
|
||||
export async function checkDesktopAppUpdate({
|
||||
releaseChannel,
|
||||
}: {
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
}): Promise<DesktopAppUpdateCheckResult> {
|
||||
const result = await invokeDesktopCommand<unknown>("check_app_update", { releaseChannel });
|
||||
if (!isRecord(result)) {
|
||||
throw new Error("Unexpected response while checking desktop updates.");
|
||||
}
|
||||
@@ -85,8 +91,12 @@ export async function checkDesktopAppUpdate(): Promise<DesktopAppUpdateCheckResu
|
||||
};
|
||||
}
|
||||
|
||||
export async function installDesktopAppUpdate(): Promise<DesktopAppUpdateInstallResult> {
|
||||
const result = await invokeDesktopCommand<unknown>("install_app_update");
|
||||
export async function installDesktopAppUpdate({
|
||||
releaseChannel,
|
||||
}: {
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
}): Promise<DesktopAppUpdateInstallResult> {
|
||||
const result = await invokeDesktopCommand<unknown>("install_app_update", { releaseChannel });
|
||||
if (!isRecord(result)) {
|
||||
throw new Error("Unexpected response while installing desktop update.");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type DesktopAppUpdateCheckResult,
|
||||
type DesktopAppUpdateInstallResult,
|
||||
} from "@/desktop/updates/desktop-updates";
|
||||
import { useAppSettings } from "@/hooks/use-settings";
|
||||
|
||||
export type DesktopAppUpdateStatus =
|
||||
| "idle"
|
||||
@@ -83,6 +84,8 @@ function formatStatusText(input: {
|
||||
|
||||
export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
const isDesktopApp = shouldShowDesktopUpdateSection();
|
||||
const { settings } = useAppSettings();
|
||||
const releaseChannel = settings.releaseChannel;
|
||||
const requestVersionRef = useRef(0);
|
||||
const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle");
|
||||
const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null);
|
||||
@@ -105,7 +108,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const result = await checkDesktopAppUpdate();
|
||||
const result = await checkDesktopAppUpdate({ releaseChannel });
|
||||
if (requestVersion !== requestVersionRef.current) {
|
||||
return result;
|
||||
}
|
||||
@@ -140,9 +143,17 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[isDesktopApp],
|
||||
[isDesktopApp, releaseChannel],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktopApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
void checkForUpdates({ silent: true });
|
||||
}, [checkForUpdates, isDesktopApp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktopApp || status !== "pending") {
|
||||
return undefined;
|
||||
@@ -166,7 +177,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const result = await installDesktopAppUpdate();
|
||||
const result = await installDesktopAppUpdate({ releaseChannel });
|
||||
setLastCheckedAt(Date.now());
|
||||
|
||||
if (result.installed) {
|
||||
@@ -186,7 +197,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
setErrorMessage(message);
|
||||
return null;
|
||||
}
|
||||
}, [isDesktopApp]);
|
||||
}, [isDesktopApp, releaseChannel]);
|
||||
|
||||
return {
|
||||
isDesktopApp,
|
||||
|
||||
@@ -40,6 +40,16 @@ describe("use-settings", () => {
|
||||
expect(result.theme).toBe("auto");
|
||||
});
|
||||
|
||||
it("defaults release channel to stable when storage is empty", async () => {
|
||||
asyncStorageMock.getItem.mockResolvedValue(null);
|
||||
asyncStorageMock.setItem.mockResolvedValue();
|
||||
|
||||
const mod = await import("./use-settings");
|
||||
const result = await mod.loadSettingsFromStorage();
|
||||
|
||||
expect(result.releaseChannel).toBe("stable");
|
||||
});
|
||||
|
||||
it("loads persisted built-in daemon management state", async () => {
|
||||
asyncStorageMock.getItem.mockImplementation(async (key: string) => {
|
||||
if (key === "@paseo:app-settings") {
|
||||
@@ -58,7 +68,24 @@ describe("use-settings", () => {
|
||||
theme: "light",
|
||||
manageBuiltInDaemon: false,
|
||||
sendBehavior: "interrupt",
|
||||
releaseChannel: "stable",
|
||||
});
|
||||
expect(asyncStorageMock.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads persisted beta release channel", async () => {
|
||||
asyncStorageMock.getItem.mockImplementation(async (key: string) => {
|
||||
if (key === "@paseo:app-settings") {
|
||||
return JSON.stringify({
|
||||
releaseChannel: "beta",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const mod = await import("./use-settings");
|
||||
const result = await mod.loadSettingsFromStorage();
|
||||
|
||||
expect(result.releaseChannel).toBe("beta");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,19 +9,23 @@ const APP_SETTINGS_QUERY_KEY = ["app-settings"];
|
||||
import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme";
|
||||
|
||||
export type SendBehavior = "interrupt" | "queue";
|
||||
export type ReleaseChannel = "stable" | "beta";
|
||||
|
||||
const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]);
|
||||
const VALID_RELEASE_CHANNELS = new Set<string>(["stable", "beta"]);
|
||||
|
||||
export interface AppSettings {
|
||||
theme: ThemeName | "auto";
|
||||
manageBuiltInDaemon: boolean;
|
||||
sendBehavior: SendBehavior;
|
||||
releaseChannel: ReleaseChannel;
|
||||
}
|
||||
|
||||
export const DEFAULT_APP_SETTINGS: AppSettings = {
|
||||
theme: "auto",
|
||||
manageBuiltInDaemon: true,
|
||||
sendBehavior: "interrupt",
|
||||
releaseChannel: "stable",
|
||||
};
|
||||
|
||||
export interface UseAppSettingsReturn {
|
||||
@@ -85,6 +89,9 @@ export async function loadSettingsFromStorage(): Promise<AppSettings> {
|
||||
if (parsed.theme && !VALID_THEMES.has(parsed.theme)) {
|
||||
parsed.theme = DEFAULT_APP_SETTINGS.theme;
|
||||
}
|
||||
if (parsed.releaseChannel && !VALID_RELEASE_CHANNELS.has(parsed.releaseChannel)) {
|
||||
parsed.releaseChannel = DEFAULT_APP_SETTINGS.releaseChannel;
|
||||
}
|
||||
return { ...DEFAULT_APP_SETTINGS, ...parsed };
|
||||
}
|
||||
|
||||
@@ -115,6 +122,9 @@ function pickAppSettingsFromLegacy(legacy: Record<string, unknown>): Partial<App
|
||||
if (typeof legacy.manageBuiltInDaemon === "boolean") {
|
||||
result.manageBuiltInDaemon = legacy.manageBuiltInDaemon;
|
||||
}
|
||||
if (legacy.releaseChannel === "stable" || legacy.releaseChannel === "beta") {
|
||||
result.releaseChannel = legacy.releaseChannel;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,6 +281,7 @@ function AboutSection({ appVersionText, isDesktopApp }: AboutSectionProps) {
|
||||
}
|
||||
|
||||
function DesktopAppUpdateRow() {
|
||||
const { settings, updateSettings } = useAppSettings();
|
||||
const {
|
||||
isDesktopApp,
|
||||
statusText,
|
||||
@@ -309,6 +310,13 @@ function DesktopAppUpdateRow() {
|
||||
void checkForUpdates();
|
||||
}, [checkForUpdates, isDesktopApp]);
|
||||
|
||||
const handleReleaseChannelChange = useCallback(
|
||||
(releaseChannel: AppSettings["releaseChannel"]) => {
|
||||
void updateSettings({ releaseChannel });
|
||||
},
|
||||
[updateSettings],
|
||||
);
|
||||
|
||||
const handleInstallUpdate = useCallback(() => {
|
||||
if (!isDesktopApp) {
|
||||
return;
|
||||
@@ -337,40 +345,59 @@ function DesktopAppUpdateRow() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>App updates</Text>
|
||||
<Text style={settingsStyles.rowHint}>{statusText}</Text>
|
||||
{availableUpdate?.latestVersion ? (
|
||||
<>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Release channel</Text>
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
||||
Switch to Beta to get updates sooner and help shape them.
|
||||
</Text>
|
||||
) : null}
|
||||
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
|
||||
</View>
|
||||
<View style={styles.aboutUpdateActions}>
|
||||
<Button
|
||||
variant="outline"
|
||||
</View>
|
||||
<SegmentedControl
|
||||
size="sm"
|
||||
onPress={handleCheckForUpdates}
|
||||
disabled={isChecking || isInstalling}
|
||||
>
|
||||
{isChecking ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onPress={handleInstallUpdate}
|
||||
disabled={isChecking || isInstalling || !availableUpdate}
|
||||
>
|
||||
{isInstalling
|
||||
? "Installing..."
|
||||
: availableUpdate?.latestVersion
|
||||
? `Update to ${formatVersionWithPrefix(availableUpdate.latestVersion)}`
|
||||
: "Update"}
|
||||
</Button>
|
||||
value={settings.releaseChannel}
|
||||
onValueChange={handleReleaseChannelChange}
|
||||
options={[
|
||||
{ value: "stable", label: "Stable" },
|
||||
{ value: "beta", label: "Beta" },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>App updates</Text>
|
||||
<Text style={settingsStyles.rowHint}>{statusText}</Text>
|
||||
{availableUpdate?.latestVersion ? (
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
||||
</Text>
|
||||
) : null}
|
||||
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
|
||||
</View>
|
||||
<View style={styles.aboutUpdateActions}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onPress={handleCheckForUpdates}
|
||||
disabled={isChecking || isInstalling}
|
||||
>
|
||||
{isChecking ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onPress={handleInstallUpdate}
|
||||
disabled={isChecking || isInstalling || !availableUpdate}
|
||||
>
|
||||
{isInstalling
|
||||
? "Installing..."
|
||||
: availableUpdate?.latestVersion
|
||||
? `Update to ${formatVersionWithPrefix(availableUpdate.latestVersion)}`
|
||||
: "Update"}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -858,9 +885,10 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
||||
layout="desktop"
|
||||
/>
|
||||
<View style={desktopStyles.contentPane}>
|
||||
{detailHeader ? (
|
||||
<ScreenHeader
|
||||
left={
|
||||
<ScreenHeader
|
||||
borderless={!detailHeader}
|
||||
left={
|
||||
detailHeader ? (
|
||||
<>
|
||||
<HeaderIconBadge>
|
||||
<detailHeader.Icon
|
||||
@@ -873,10 +901,10 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
||||
</ScreenTitle>
|
||||
{detailHeader.titleAccessory}
|
||||
</>
|
||||
}
|
||||
leftStyle={desktopStyles.detailLeft}
|
||||
/>
|
||||
) : null}
|
||||
) : null
|
||||
}
|
||||
leftStyle={desktopStyles.detailLeft}
|
||||
/>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom }}
|
||||
|
||||
@@ -19,58 +19,66 @@ type RawWindowControlsPadding = {
|
||||
|
||||
type WindowControlsPaddingRole = "sidebar" | "header" | "tabRow" | "explorerSidebar";
|
||||
|
||||
// Module-level cache so hook remounts (e.g., on navigation) don't briefly
|
||||
// fall back to the default `false` while the async fullscreen check resolves.
|
||||
// Without this, in fullscreen the sidebar flashes with traffic-light padding
|
||||
// on first frame and then snaps to 0 once the async read completes.
|
||||
let cachedIsFullscreen = false;
|
||||
const fullscreenSubscribers = new Set<(value: boolean) => void>();
|
||||
let fullscreenSubscriptionStarted = false;
|
||||
|
||||
function setCachedFullscreen(value: boolean) {
|
||||
if (cachedIsFullscreen === value) return;
|
||||
cachedIsFullscreen = value;
|
||||
for (const sub of fullscreenSubscribers) {
|
||||
sub(value);
|
||||
}
|
||||
}
|
||||
|
||||
function startFullscreenSubscription() {
|
||||
if (fullscreenSubscriptionStarted) return;
|
||||
if (isNative || !getIsElectronRuntime()) return;
|
||||
fullscreenSubscriptionStarted = true;
|
||||
|
||||
void (async () => {
|
||||
const win = getDesktopWindow();
|
||||
if (!win) return;
|
||||
|
||||
if (typeof win.isFullscreen === "function") {
|
||||
try {
|
||||
setCachedFullscreen(await win.isFullscreen());
|
||||
} catch (error) {
|
||||
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof win.onResized !== "function") return;
|
||||
|
||||
try {
|
||||
await win.onResized(async () => {
|
||||
if (typeof win.isFullscreen !== "function") return;
|
||||
try {
|
||||
setCachedFullscreen(await win.isFullscreen());
|
||||
} catch (error) {
|
||||
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[DesktopWindow] Failed to subscribe to resize", error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function useRawWindowControlsPadding(): RawWindowControlsPadding {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(cachedIsFullscreen);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNative || !getIsElectronRuntime()) return;
|
||||
|
||||
let disposed = false;
|
||||
let cleanup: (() => void) | undefined;
|
||||
let didCleanup = false;
|
||||
|
||||
function runCleanup() {
|
||||
if (!cleanup || didCleanup) return;
|
||||
didCleanup = true;
|
||||
try {
|
||||
void Promise.resolve(cleanup()).catch((error) => {
|
||||
console.warn("[DesktopWindow] Failed to remove resize listener", error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[DesktopWindow] Failed to remove resize listener", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
const win = getDesktopWindow();
|
||||
if (!win) return;
|
||||
|
||||
const fullscreen = typeof win.isFullscreen === "function" ? await win.isFullscreen() : false;
|
||||
if (disposed) return;
|
||||
setIsFullscreen(fullscreen);
|
||||
|
||||
if (typeof win.onResized !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const unlisten = await win.onResized(async () => {
|
||||
if (disposed) return;
|
||||
const fs = typeof win.isFullscreen === "function" ? await win.isFullscreen() : false;
|
||||
if (disposed) return;
|
||||
setIsFullscreen(fs);
|
||||
});
|
||||
|
||||
cleanup = unlisten;
|
||||
if (disposed) {
|
||||
runCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
void setup();
|
||||
|
||||
startFullscreenSubscription();
|
||||
// Sync to any value that resolved between render and effect.
|
||||
setIsFullscreen(cachedIsFullscreen);
|
||||
fullscreenSubscribers.add(setIsFullscreen);
|
||||
return () => {
|
||||
disposed = true;
|
||||
runCleanup();
|
||||
fullscreenSubscribers.delete(setIsFullscreen);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"type": "module",
|
||||
"files": [
|
||||
@@ -24,8 +24,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/relay": "0.1.60-rc.3",
|
||||
"@getpaseo/server": "0.1.60-rc.3",
|
||||
"@getpaseo/relay": "0.1.60-beta.1",
|
||||
"@getpaseo/server": "0.1.60-beta.1",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"main": "dist/main.js",
|
||||
@@ -13,8 +13,8 @@
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "0.1.60-rc.3",
|
||||
"@getpaseo/server": "0.1.60-rc.3",
|
||||
"@getpaseo/cli": "0.1.60-beta.1",
|
||||
"@getpaseo/server": "0.1.60-beta.1",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-updater": "^6.6.2",
|
||||
"ws": "^8.14.2"
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
readManagedFileBase64,
|
||||
writeAttachmentBase64,
|
||||
} from "../features/attachments.js";
|
||||
import { checkForAppUpdate, downloadAndInstallUpdate } from "../features/auto-updater.js";
|
||||
import {
|
||||
checkForAppUpdate,
|
||||
downloadAndInstallUpdate,
|
||||
type AppReleaseChannel,
|
||||
} from "../features/auto-updater.js";
|
||||
import {
|
||||
installCli,
|
||||
getCliInstallStatus,
|
||||
@@ -65,6 +69,10 @@ type DesktopPairingOffer = {
|
||||
|
||||
type DesktopCommandHandler = (args?: Record<string, unknown>) => Promise<unknown> | unknown;
|
||||
|
||||
function parseReleaseChannel(args: Record<string, unknown> | undefined): AppReleaseChannel {
|
||||
return args?.releaseChannel === "beta" ? "beta" : "stable";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -483,15 +491,18 @@ export function createDaemonCommandHandlers(): Record<string, DesktopCommandHand
|
||||
: "";
|
||||
if (sessionId) closeLocalTransportSession(sessionId);
|
||||
},
|
||||
check_app_update: async () => {
|
||||
check_app_update: async (args) => {
|
||||
const currentVersion = await resolveCurrentUpdateVersion();
|
||||
return checkForAppUpdate(currentVersion);
|
||||
return checkForAppUpdate({ currentVersion, releaseChannel: parseReleaseChannel(args) });
|
||||
},
|
||||
install_app_update: async () => {
|
||||
install_app_update: async (args) => {
|
||||
const currentVersion = await resolveCurrentUpdateVersion();
|
||||
return downloadAndInstallUpdate(currentVersion, async () => {
|
||||
await stopDaemon();
|
||||
});
|
||||
return downloadAndInstallUpdate(
|
||||
{ currentVersion, releaseChannel: parseReleaseChannel(args) },
|
||||
async () => {
|
||||
await stopDaemon();
|
||||
},
|
||||
);
|
||||
},
|
||||
get_local_daemon_version: () => getLocalDaemonVersion(),
|
||||
install_cli: () => installCli(),
|
||||
|
||||
@@ -20,6 +20,8 @@ export type AppUpdateInstallResult = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type AppReleaseChannel = "stable" | "beta";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -28,18 +30,29 @@ let cachedUpdateInfo: UpdateInfo | null = null;
|
||||
let downloadedUpdateVersion: string | null = null;
|
||||
let downloading = false;
|
||||
let autoUpdaterConfigured = false;
|
||||
let configuredReleaseChannel: AppReleaseChannel | null = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function configureAutoUpdater(): void {
|
||||
function configureAutoUpdater(releaseChannel: AppReleaseChannel): void {
|
||||
// Download updates in the background and only prompt once they are ready to install.
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
// Suppress built-in dialogs; the renderer handles UI.
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
autoUpdater.allowPrerelease = releaseChannel === "beta";
|
||||
autoUpdater.channel = releaseChannel === "beta" ? "beta" : "latest";
|
||||
autoUpdater.allowDowngrade = false;
|
||||
|
||||
if (configuredReleaseChannel !== releaseChannel) {
|
||||
cachedUpdateInfo = null;
|
||||
downloadedUpdateVersion = null;
|
||||
downloading = false;
|
||||
configuredReleaseChannel = releaseChannel;
|
||||
}
|
||||
|
||||
if (autoUpdaterConfigured) {
|
||||
return;
|
||||
@@ -109,7 +122,13 @@ function scheduleQuitAndInstall(onBeforeQuit?: () => Promise<void>): void {
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkForAppUpdate(currentVersion: string): Promise<AppUpdateCheckResult> {
|
||||
export async function checkForAppUpdate({
|
||||
currentVersion,
|
||||
releaseChannel,
|
||||
}: {
|
||||
currentVersion: string;
|
||||
releaseChannel: AppReleaseChannel;
|
||||
}): Promise<AppUpdateCheckResult> {
|
||||
if (!app.isPackaged) {
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
@@ -118,7 +137,7 @@ export async function checkForAppUpdate(currentVersion: string): Promise<AppUpda
|
||||
});
|
||||
}
|
||||
|
||||
configureAutoUpdater();
|
||||
configureAutoUpdater(releaseChannel);
|
||||
|
||||
const cachedVersion = cachedUpdateInfo?.version ?? null;
|
||||
if (cachedVersion && cachedVersion !== currentVersion) {
|
||||
@@ -176,7 +195,13 @@ export async function checkForAppUpdate(currentVersion: string): Promise<AppUpda
|
||||
}
|
||||
|
||||
export async function downloadAndInstallUpdate(
|
||||
currentVersion: string,
|
||||
{
|
||||
currentVersion,
|
||||
releaseChannel,
|
||||
}: {
|
||||
currentVersion: string;
|
||||
releaseChannel: AppReleaseChannel;
|
||||
},
|
||||
onBeforeQuit?: () => Promise<void>,
|
||||
): Promise<AppUpdateInstallResult> {
|
||||
if (!app.isPackaged) {
|
||||
@@ -195,7 +220,7 @@ export async function downloadAndInstallUpdate(
|
||||
};
|
||||
}
|
||||
|
||||
configureAutoUpdater();
|
||||
configureAutoUpdater(releaseChannel);
|
||||
|
||||
const readyVersion = cachedUpdateInfo.version;
|
||||
if (isReadyToInstallVersion(readyVersion)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"description": "Paseo backend server",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
@@ -60,8 +60,8 @@
|
||||
"@ai-sdk/openai": "2.0.52",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||
"@deepgram/sdk": "^3.4.0",
|
||||
"@getpaseo/highlight": "0.1.60-rc.3",
|
||||
"@getpaseo/relay": "0.1.60-rc.3",
|
||||
"@getpaseo/highlight": "0.1.60-beta.1",
|
||||
"@getpaseo/relay": "0.1.60-beta.1",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@mariozechner/pi-coding-agent": "^0.67.68",
|
||||
|
||||
@@ -234,7 +234,7 @@ const MIN_VERSION_FLEXIBLE_EDITOR_IDS = "0.1.50";
|
||||
|
||||
function isAppVersionAtLeast(appVersion: string | null, minVersion: string): boolean {
|
||||
if (!appVersion) return false;
|
||||
// Strip RC/prerelease suffix: "0.1.45-rc.4" → "0.1.45"
|
||||
// Strip prerelease suffix: "0.1.45-beta.4" -> "0.1.45"
|
||||
const base = appVersion.replace(/-.*$/, "");
|
||||
const parts = base.split(".").map(Number);
|
||||
const minParts = minVersion.split(".").map(Number);
|
||||
|
||||
@@ -1089,7 +1089,7 @@ describe("slugify", () => {
|
||||
});
|
||||
|
||||
it("keeps very long names within the hostname label length limit", () => {
|
||||
const result = slugify("Release Candidate ".repeat(12));
|
||||
const result = slugify("Beta Build ".repeat(12));
|
||||
|
||||
expect(result.length).toBeLessThanOrEqual(63);
|
||||
expectValidHostnameLabel(result);
|
||||
@@ -1107,7 +1107,7 @@ describe("slugify", () => {
|
||||
"feature/cool stuff",
|
||||
" Café Launch ",
|
||||
"__bar__",
|
||||
"Release Candidate ".repeat(12),
|
||||
"Beta Build ".repeat(12),
|
||||
"release***candidate",
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.60-rc.3",
|
||||
"version": "0.1.60-beta.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user