mirror of
https://github.com/cline/cline.git
synced 2026-10-30 18:12:49 +08:00
feat: global auto-update setting for CLI startup updates (#11326)
* feat: global auto-update setting for CLI startup updates * patches --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
autoUpdateOnStartup,
|
||||
checkForUpdates,
|
||||
getInstallationInfo,
|
||||
PackageManager,
|
||||
withMinimumReleaseAgeBypass,
|
||||
@@ -10,6 +12,9 @@ import {
|
||||
|
||||
const originalArgv = [...process.argv];
|
||||
const originalWrapperPath = process.env.CLINE_WRAPPER_PATH;
|
||||
const originalGlobalSettingsPath = process.env.CLINE_GLOBAL_SETTINGS_PATH;
|
||||
const originalIsDev = process.env.IS_DEV;
|
||||
const originalNoAutoUpdate = process.env.CLINE_NO_AUTO_UPDATE;
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function createFile(path: string): string {
|
||||
@@ -32,6 +37,22 @@ describe("getInstallationInfo", () => {
|
||||
} else {
|
||||
process.env.CLINE_WRAPPER_PATH = originalWrapperPath;
|
||||
}
|
||||
if (originalGlobalSettingsPath === undefined) {
|
||||
delete process.env.CLINE_GLOBAL_SETTINGS_PATH;
|
||||
} else {
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = originalGlobalSettingsPath;
|
||||
}
|
||||
if (originalIsDev === undefined) {
|
||||
delete process.env.IS_DEV;
|
||||
} else {
|
||||
process.env.IS_DEV = originalIsDev;
|
||||
}
|
||||
if (originalNoAutoUpdate === undefined) {
|
||||
delete process.env.CLINE_NO_AUTO_UPDATE;
|
||||
} else {
|
||||
process.env.CLINE_NO_AUTO_UPDATE = originalNoAutoUpdate;
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -72,6 +93,66 @@ describe("getInstallationInfo", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto update settings", () => {
|
||||
afterEach(() => {
|
||||
process.argv = [...originalArgv];
|
||||
if (originalWrapperPath === undefined) {
|
||||
delete process.env.CLINE_WRAPPER_PATH;
|
||||
} else {
|
||||
process.env.CLINE_WRAPPER_PATH = originalWrapperPath;
|
||||
}
|
||||
if (originalGlobalSettingsPath === undefined) {
|
||||
delete process.env.CLINE_GLOBAL_SETTINGS_PATH;
|
||||
} else {
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = originalGlobalSettingsPath;
|
||||
}
|
||||
if (originalIsDev === undefined) {
|
||||
delete process.env.IS_DEV;
|
||||
} else {
|
||||
process.env.IS_DEV = originalIsDev;
|
||||
}
|
||||
if (originalNoAutoUpdate === undefined) {
|
||||
delete process.env.CLINE_NO_AUTO_UPDATE;
|
||||
} else {
|
||||
process.env.CLINE_NO_AUTO_UPDATE = originalNoAutoUpdate;
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("skips startup auto update when disabled globally", () => {
|
||||
const settingsPath = createTempFile("data/global-settings.json");
|
||||
writeFileSync(settingsPath, JSON.stringify({ autoUpdateEnabled: false }));
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
|
||||
delete process.env.IS_DEV;
|
||||
delete process.env.CLINE_NO_AUTO_UPDATE;
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockRejectedValue(new Error("should not fetch"));
|
||||
|
||||
autoUpdateOnStartup();
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still lets manual update checks run when startup auto update is disabled", async () => {
|
||||
const settingsPath = createTempFile("data/global-settings.json");
|
||||
writeFileSync(settingsPath, JSON.stringify({ autoUpdateEnabled: false }));
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
|
||||
delete process.env.CLINE_NO_AUTO_UPDATE;
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ version: "0.0.0" }),
|
||||
} as Response);
|
||||
|
||||
await checkForUpdates({ includeKanban: false });
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("withMinimumReleaseAgeBypass", () => {
|
||||
it("adds the package-manager-specific cooldown bypass", () => {
|
||||
expect(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { realpathSync } from "node:fs";
|
||||
import {
|
||||
clearHubDiscovery,
|
||||
isAutoUpdateEnabledGlobally,
|
||||
probeHubServer,
|
||||
readHubDiscovery,
|
||||
resolveSharedHubOwnerContext,
|
||||
@@ -340,6 +341,7 @@ async function restartHubServerIfRunning(): Promise<void> {
|
||||
export function autoUpdateOnStartup(): void {
|
||||
if (process.env.IS_DEV === "true") return;
|
||||
if (process.env.CLINE_NO_AUTO_UPDATE === "1") return;
|
||||
if (!isAutoUpdateEnabledGlobally()) return;
|
||||
|
||||
const { packageName, packageManager, updateCommand } =
|
||||
getInstallationInfo(version);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readGlobalSettings, setAutoUpdateEnabledGlobally } from "@cline/core";
|
||||
import { useTerminalDimensions } from "@opentui/react";
|
||||
import type { ChoiceContext } from "@opentui-ui/dialog";
|
||||
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
|
||||
@@ -368,6 +369,9 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
const [autoApprove, setAutoApprove] = useState(
|
||||
config.toolPolicies["*"]?.autoApprove !== false,
|
||||
);
|
||||
const [autoUpdateEnabled, setAutoUpdateEnabled] = useState(
|
||||
() => readGlobalSettings().autoUpdateEnabled,
|
||||
);
|
||||
const [verbose, setVerbose] = useState(config.verbose);
|
||||
const [compactionMode, setCompactionMode] = useState(
|
||||
props.currentCompactionMode,
|
||||
@@ -445,6 +449,7 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
id: "auto-approve",
|
||||
label: "Auto-approve all",
|
||||
});
|
||||
r.push({ kind: "toggle", id: "auto-update", label: "Auto update" });
|
||||
r.push({ kind: "toggle", id: "verbose", label: "Verbose" });
|
||||
} else {
|
||||
const activeItems = resolveActiveConfigItems(configData, activeTab);
|
||||
@@ -584,6 +589,13 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
setAutoApprove(!autoApprove);
|
||||
props.onToggleAutoApprove();
|
||||
break;
|
||||
case "auto-update":
|
||||
setAutoUpdateEnabled((previous) => {
|
||||
const next = !previous;
|
||||
setAutoUpdateEnabledGlobally(next);
|
||||
return next;
|
||||
});
|
||||
break;
|
||||
case "compaction": {
|
||||
const nextMode = getNextCliCompactionMode(compactionMode);
|
||||
setCompactionMode(nextMode);
|
||||
@@ -789,6 +801,9 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
} else if (row.id === "auto-approve") {
|
||||
value = autoApprove ? "● on" : "○ off";
|
||||
valueColor = autoApprove ? palette.success : "gray";
|
||||
} else if (row.id === "auto-update") {
|
||||
value = autoUpdateEnabled ? "● on" : "○ off";
|
||||
valueColor = autoUpdateEnabled ? palette.success : "gray";
|
||||
} else if (row.id === "compaction") {
|
||||
value = formatCliCompactionMode(compactionMode);
|
||||
valueColor = COMPACTION_MODE_COLORS[compactionMode];
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveLocalClineAuthToken,
|
||||
saveLocalProviderOAuthCredentials,
|
||||
saveLocalProviderSettings,
|
||||
setAutoUpdateEnabledGlobally,
|
||||
setDisabledPlugin,
|
||||
setDisabledTools,
|
||||
setTelemetryOptOutGlobally,
|
||||
@@ -155,6 +156,13 @@ export async function handleDesktopCommand(
|
||||
setTelemetryOptOutGlobally(args.telemetry_opt_out);
|
||||
return readGlobalSettings();
|
||||
}
|
||||
if (command === "set_auto_update_enabled") {
|
||||
if (typeof args?.auto_update_enabled !== "boolean") {
|
||||
throw new Error("auto_update_enabled must be a boolean");
|
||||
}
|
||||
setAutoUpdateEnabledGlobally(args.auto_update_enabled);
|
||||
return readGlobalSettings();
|
||||
}
|
||||
if (command === "list_connector_channels") {
|
||||
return connectorChannelsPayload();
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export type SettingsSection = (typeof navCategories)[number];
|
||||
type Theme = "dark" | "light";
|
||||
type GlobalSettingsResponse = {
|
||||
telemetryOptOut: boolean;
|
||||
autoUpdateEnabled: boolean;
|
||||
};
|
||||
|
||||
const PROVIDER_CATALOG_CACHE_TTL_MS = 60_000;
|
||||
@@ -525,20 +526,29 @@ function GeneralSettingsContent({
|
||||
const [telemetryLoading, setTelemetryLoading] = useState(true);
|
||||
const [telemetrySaving, setTelemetrySaving] = useState(false);
|
||||
const [telemetryError, setTelemetryError] = useState<string | null>(null);
|
||||
const [autoUpdateEnabled, setAutoUpdateEnabled] = useState(true);
|
||||
const [autoUpdateLoading, setAutoUpdateLoading] = useState(true);
|
||||
const [autoUpdateSaving, setAutoUpdateSaving] = useState(false);
|
||||
const [autoUpdateError, setAutoUpdateError] = useState<string | null>(null);
|
||||
|
||||
const loadGlobalSettings = useCallback(async () => {
|
||||
setTelemetryLoading(true);
|
||||
setTelemetryError(null);
|
||||
setAutoUpdateLoading(true);
|
||||
setAutoUpdateError(null);
|
||||
try {
|
||||
const settings = await desktopClient.invoke<GlobalSettingsResponse>(
|
||||
"get_global_settings",
|
||||
);
|
||||
setTelemetryOptOut(settings.telemetryOptOut);
|
||||
setAutoUpdateEnabled(settings.autoUpdateEnabled);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setTelemetryError(message);
|
||||
setAutoUpdateError(message);
|
||||
} finally {
|
||||
setTelemetryLoading(false);
|
||||
setAutoUpdateLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -571,6 +581,28 @@ function GeneralSettingsContent({
|
||||
}
|
||||
};
|
||||
|
||||
const updateAutoUpdateEnabled = async (nextValue: boolean) => {
|
||||
const previousValue = autoUpdateEnabled;
|
||||
setAutoUpdateEnabled(nextValue);
|
||||
setAutoUpdateSaving(true);
|
||||
setAutoUpdateError(null);
|
||||
try {
|
||||
const settings = await desktopClient.invoke<GlobalSettingsResponse>(
|
||||
"set_auto_update_enabled",
|
||||
{
|
||||
auto_update_enabled: nextValue,
|
||||
},
|
||||
);
|
||||
setAutoUpdateEnabled(settings.autoUpdateEnabled);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setAutoUpdateEnabled(previousValue);
|
||||
setAutoUpdateError(message);
|
||||
} finally {
|
||||
setAutoUpdateSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="mx-auto max-w-3xl px-8 py-6">
|
||||
@@ -605,6 +637,29 @@ function GeneralSettingsContent({
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mt-4 rounded-lg border border-border p-5">
|
||||
<div className="flex items-center justify-between gap-5 max-[720px]:flex-col max-[720px]:items-stretch">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Auto update</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Automatically install CLI updates on startup.
|
||||
</p>
|
||||
{autoUpdateError ? (
|
||||
<p className="mt-2 text-xs text-destructive">
|
||||
Failed to update auto update setting: {autoUpdateError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Switch
|
||||
aria-label="Auto update"
|
||||
checked={autoUpdateEnabled}
|
||||
disabled={autoUpdateLoading || autoUpdateSaving}
|
||||
onCheckedChange={(checked) =>
|
||||
void updateAutoUpdateEnabled(checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mt-4 rounded-lg border border-border p-5">
|
||||
<div className="flex items-center justify-between gap-5 max-[720px]:flex-col max-[720px]:items-stretch">
|
||||
<div>
|
||||
|
||||
@@ -421,12 +421,14 @@ export {
|
||||
filterDisabledTools,
|
||||
filterExtensionToolRegistrations,
|
||||
GlobalSettingsSchema,
|
||||
isAutoUpdateEnabledGlobally,
|
||||
isPluginDisabledGlobally,
|
||||
isTelemetryOptedOutGlobally,
|
||||
isToolDisabledGlobally,
|
||||
readGlobalSettings,
|
||||
resolveDisabledPluginPaths,
|
||||
resolveDisabledToolNames,
|
||||
setAutoUpdateEnabledGlobally,
|
||||
setDisabledPlugin,
|
||||
setDisabledTools,
|
||||
setTelemetryOptOutGlobally,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
GlobalSettingsSchema,
|
||||
readGlobalSettings,
|
||||
setAutoUpdateEnabledGlobally,
|
||||
setDisabledPlugin,
|
||||
setDisabledTools,
|
||||
setTelemetryOptOutGlobally,
|
||||
@@ -26,6 +27,7 @@ describe("global-settings", () => {
|
||||
disabledPlugins: ["/plugins/example.js", "/plugins/example.js"],
|
||||
}),
|
||||
).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledPlugins: ["/plugins/example.js"],
|
||||
disabledTools: ["editor", "read_files"],
|
||||
telemetryOptOut: false,
|
||||
@@ -35,8 +37,9 @@ describe("global-settings", () => {
|
||||
disabledTools: [],
|
||||
telemetryOptOut: true,
|
||||
}),
|
||||
).toEqual({ telemetryOptOut: true });
|
||||
).toEqual({ autoUpdateEnabled: true, telemetryOptOut: true });
|
||||
expect(GlobalSettingsSchema.parse({ disabledTools: [] })).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
expect(
|
||||
@@ -45,6 +48,7 @@ describe("global-settings", () => {
|
||||
extra: true,
|
||||
}),
|
||||
).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["read_files"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -55,8 +59,17 @@ describe("global-settings", () => {
|
||||
telemetryOptOut: true,
|
||||
}),
|
||||
).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
telemetryOptOut: true,
|
||||
});
|
||||
expect(
|
||||
GlobalSettingsSchema.parse({
|
||||
autoUpdateEnabled: false,
|
||||
}),
|
||||
).toEqual({
|
||||
autoUpdateEnabled: false,
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the schema when reading and writing settings", async () => {
|
||||
@@ -71,10 +84,12 @@ describe("global-settings", () => {
|
||||
});
|
||||
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["editor", "read_files"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
expect(JSON.parse(await readFile(settingsPath, "utf8"))).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["editor", "read_files"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -88,6 +103,7 @@ describe("global-settings", () => {
|
||||
}),
|
||||
);
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["read_files"],
|
||||
telemetryOptOut: true,
|
||||
});
|
||||
@@ -100,7 +116,10 @@ describe("global-settings", () => {
|
||||
telemetryOptOut: true,
|
||||
}),
|
||||
);
|
||||
expect(readGlobalSettings()).toEqual({ telemetryOptOut: true });
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
telemetryOptOut: true,
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -117,11 +136,13 @@ describe("global-settings", () => {
|
||||
setDisabledTools(["editor"], false);
|
||||
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledPlugins: ["/plugins/example.js"],
|
||||
disabledTools: ["read_files"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
expect(JSON.parse(await readFile(settingsPath, "utf8"))).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledPlugins: ["/plugins/example.js"],
|
||||
disabledTools: ["read_files"],
|
||||
telemetryOptOut: false,
|
||||
@@ -147,7 +168,32 @@ describe("global-settings", () => {
|
||||
|
||||
expect(captureRequired).toHaveBeenCalledTimes(1);
|
||||
expect(captureRequired).toHaveBeenCalledWith("user.opt_out", undefined);
|
||||
expect(readGlobalSettings()).toEqual({ telemetryOptOut: false });
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves other settings when auto update is changed", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "core-global-settings-"));
|
||||
try {
|
||||
const settingsPath = join(root, "global-settings.json");
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
|
||||
|
||||
writeGlobalSettings({
|
||||
disabledTools: ["editor"],
|
||||
telemetryOptOut: true,
|
||||
});
|
||||
setAutoUpdateEnabledGlobally(false);
|
||||
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: false,
|
||||
disabledTools: ["editor"],
|
||||
telemetryOptOut: true,
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -165,6 +211,7 @@ describe("global-settings", () => {
|
||||
writeGlobalSettings({ disabledTools: ["read_files"] });
|
||||
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["read_files"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -187,6 +234,7 @@ describe("global-settings", () => {
|
||||
);
|
||||
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["read_files"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -205,6 +253,7 @@ describe("global-settings", () => {
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = pathA;
|
||||
writeGlobalSettings({ disabledTools: ["editor"] });
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["editor"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -212,12 +261,14 @@ describe("global-settings", () => {
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = pathB;
|
||||
writeGlobalSettings({ disabledTools: ["read_files"] });
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["read_files"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = pathA;
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["editor"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -233,8 +284,14 @@ describe("global-settings", () => {
|
||||
const settingsPath = join(root, "missing-global-settings.json");
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
|
||||
|
||||
expect(readGlobalSettings()).toEqual({ telemetryOptOut: false });
|
||||
expect(readGlobalSettings()).toEqual({ telemetryOptOut: false });
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -272,7 +329,10 @@ describe("global-settings", () => {
|
||||
const settingsPath = join(root, "global-settings.json");
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
|
||||
|
||||
expect(readGlobalSettings()).toEqual({ telemetryOptOut: false });
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
settingsPath,
|
||||
@@ -280,6 +340,7 @@ describe("global-settings", () => {
|
||||
);
|
||||
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["editor"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ const GlobalSettingsStringListSchema = z
|
||||
export const GlobalSettingsSchema = z
|
||||
.object({
|
||||
telemetryOptOut: z.boolean().default(false).catch(false),
|
||||
autoUpdateEnabled: z.boolean().default(true).catch(true),
|
||||
disabledTools: GlobalSettingsStringListSchema.optional(),
|
||||
disabledPlugins: GlobalSettingsStringListSchema.optional(),
|
||||
})
|
||||
@@ -39,9 +40,11 @@ export const GlobalSettingsSchema = z
|
||||
.transform((settings) => {
|
||||
const normalized: {
|
||||
telemetryOptOut: boolean;
|
||||
autoUpdateEnabled: boolean;
|
||||
disabledTools?: string[];
|
||||
disabledPlugins?: string[];
|
||||
} = {
|
||||
autoUpdateEnabled: settings.autoUpdateEnabled,
|
||||
telemetryOptOut: settings.telemetryOptOut,
|
||||
};
|
||||
if (settings.disabledTools?.length) {
|
||||
@@ -160,6 +163,23 @@ export function setTelemetryOptOutGlobally(
|
||||
);
|
||||
}
|
||||
|
||||
export function isAutoUpdateEnabledGlobally(): boolean {
|
||||
return readGlobalSettings().autoUpdateEnabled;
|
||||
}
|
||||
|
||||
export function setAutoUpdateEnabledGlobally(
|
||||
autoUpdateEnabled: boolean,
|
||||
options: WriteGlobalSettingsOptions = {},
|
||||
): void {
|
||||
writeGlobalSettings(
|
||||
{
|
||||
...readGlobalSettings(),
|
||||
autoUpdateEnabled,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDisabledToolNames(
|
||||
disabledToolNames?: ReadonlyArray<string>,
|
||||
): Set<string> {
|
||||
|
||||
@@ -106,6 +106,7 @@ describe("plugin uninstall service", () => {
|
||||
expect(result.installPath).toBe(installPath);
|
||||
expect(existsSync(installPath)).toBe(false);
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledPlugins: ["/tmp/other-plugin.ts"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -146,6 +147,7 @@ describe("plugin uninstall service", () => {
|
||||
await expect(uninstallPlugin({ path: pluginPath })).rejects.toThrow();
|
||||
expect(existsSync(pluginPath)).toBe(true);
|
||||
expect(readGlobalSettings()).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledPlugins: [pluginPath],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
|
||||
@@ -287,6 +287,7 @@ Use this skill.`,
|
||||
await readFile(process.env.CLINE_GLOBAL_SETTINGS_PATH, "utf8"),
|
||||
),
|
||||
).toEqual({
|
||||
autoUpdateEnabled: true,
|
||||
disabledTools: ["plugin-tool"],
|
||||
telemetryOptOut: false,
|
||||
});
|
||||
@@ -297,6 +298,6 @@ Use this skill.`,
|
||||
JSON.parse(
|
||||
await readFile(process.env.CLINE_GLOBAL_SETTINGS_PATH, "utf8"),
|
||||
),
|
||||
).toEqual({ telemetryOptOut: false });
|
||||
).toEqual({ autoUpdateEnabled: true, telemetryOptOut: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,12 +109,14 @@ export {
|
||||
filterDisabledTools,
|
||||
filterExtensionToolRegistrations,
|
||||
GlobalSettingsSchema,
|
||||
isAutoUpdateEnabledGlobally,
|
||||
isPluginDisabledGlobally,
|
||||
isTelemetryOptedOutGlobally,
|
||||
isToolDisabledGlobally,
|
||||
readGlobalSettings,
|
||||
resolveDisabledPluginPaths,
|
||||
resolveDisabledToolNames,
|
||||
setAutoUpdateEnabledGlobally,
|
||||
setDisabledPlugin,
|
||||
setDisabledTools,
|
||||
setTelemetryOptOutGlobally,
|
||||
|
||||
Reference in New Issue
Block a user