mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat: Update permissions command to support modifying trust for other… (#11642)
This commit is contained in:
@@ -196,14 +196,20 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
|
const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
|
||||||
|
const [permissionsDialogProps, setPermissionsDialogProps] = useState<{
|
||||||
|
targetDirectory?: string;
|
||||||
|
} | null>(null);
|
||||||
const openPermissionsDialog = useCallback(
|
const openPermissionsDialog = useCallback(
|
||||||
() => setPermissionsDialogOpen(true),
|
(props?: { targetDirectory?: string }) => {
|
||||||
[],
|
setPermissionsDialogOpen(true);
|
||||||
);
|
setPermissionsDialogProps(props ?? null);
|
||||||
const closePermissionsDialog = useCallback(
|
},
|
||||||
() => setPermissionsDialogOpen(false),
|
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
const closePermissionsDialog = useCallback(() => {
|
||||||
|
setPermissionsDialogOpen(false);
|
||||||
|
setPermissionsDialogProps(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleDebugProfiler = useCallback(
|
const toggleDebugProfiler = useCallback(
|
||||||
() => setShowDebugProfiler((prev) => !prev),
|
() => setShowDebugProfiler((prev) => !prev),
|
||||||
@@ -1301,6 +1307,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
isSettingsDialogOpen,
|
isSettingsDialogOpen,
|
||||||
isModelDialogOpen,
|
isModelDialogOpen,
|
||||||
isPermissionsDialogOpen,
|
isPermissionsDialogOpen,
|
||||||
|
permissionsDialogProps,
|
||||||
slashCommands,
|
slashCommands,
|
||||||
pendingSlashCommandHistoryItems,
|
pendingSlashCommandHistoryItems,
|
||||||
commandContext,
|
commandContext,
|
||||||
@@ -1384,6 +1391,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
isSettingsDialogOpen,
|
isSettingsDialogOpen,
|
||||||
isModelDialogOpen,
|
isModelDialogOpen,
|
||||||
isPermissionsDialogOpen,
|
isPermissionsDialogOpen,
|
||||||
|
permissionsDialogProps,
|
||||||
slashCommands,
|
slashCommands,
|
||||||
pendingSlashCommandHistoryItems,
|
pendingSlashCommandHistoryItems,
|
||||||
commandContext,
|
commandContext,
|
||||||
@@ -1474,6 +1482,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
exitPrivacyNotice,
|
exitPrivacyNotice,
|
||||||
closeSettingsDialog,
|
closeSettingsDialog,
|
||||||
closeModelDialog,
|
closeModelDialog,
|
||||||
|
openPermissionsDialog,
|
||||||
closePermissionsDialog,
|
closePermissionsDialog,
|
||||||
setShellModeActive,
|
setShellModeActive,
|
||||||
vimHandleInput,
|
vimHandleInput,
|
||||||
@@ -1502,6 +1511,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
exitPrivacyNotice,
|
exitPrivacyNotice,
|
||||||
closeSettingsDialog,
|
closeSettingsDialog,
|
||||||
closeModelDialog,
|
closeModelDialog,
|
||||||
|
openPermissionsDialog,
|
||||||
closePermissionsDialog,
|
closePermissionsDialog,
|
||||||
setShellModeActive,
|
setShellModeActive,
|
||||||
vimHandleInput,
|
vimHandleInput,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { directoryCommand, expandHomeDir } from './directoryCommand.js';
|
import { directoryCommand } from './directoryCommand.js';
|
||||||
|
import { expandHomeDir } from '../utils/directoryUtils.js';
|
||||||
import type { Config, WorkspaceContext } from '@google/gemini-cli-core';
|
import type { Config, WorkspaceContext } from '@google/gemini-cli-core';
|
||||||
import type { CommandContext } from './types.js';
|
import type { CommandContext } from './types.js';
|
||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
|
|||||||
@@ -7,22 +7,8 @@
|
|||||||
import type { SlashCommand, CommandContext } from './types.js';
|
import type { SlashCommand, CommandContext } from './types.js';
|
||||||
import { CommandKind } from './types.js';
|
import { CommandKind } from './types.js';
|
||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
import * as os from 'node:os';
|
|
||||||
import * as path from 'node:path';
|
|
||||||
import { refreshServerHierarchicalMemory } from '@google/gemini-cli-core';
|
import { refreshServerHierarchicalMemory } from '@google/gemini-cli-core';
|
||||||
|
import { expandHomeDir } from '../utils/directoryUtils.js';
|
||||||
export function expandHomeDir(p: string): string {
|
|
||||||
if (!p) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
let expandedPath = p;
|
|
||||||
if (p.toLowerCase().startsWith('%userprofile%')) {
|
|
||||||
expandedPath = os.homedir() + p.substring('%userprofile%'.length);
|
|
||||||
} else if (p === '~' || p.startsWith('~/')) {
|
|
||||||
expandedPath = os.homedir() + p.substring(1);
|
|
||||||
}
|
|
||||||
return path.normalize(expandedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const directoryCommand: SlashCommand = {
|
export const directoryCommand: SlashCommand = {
|
||||||
name: 'directory',
|
name: 'directory',
|
||||||
|
|||||||
@@ -4,32 +4,113 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||||
|
import * as process from 'node:process';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
import { permissionsCommand } from './permissionsCommand.js';
|
import { permissionsCommand } from './permissionsCommand.js';
|
||||||
import { type CommandContext, CommandKind } from './types.js';
|
import { type CommandContext, CommandKind } from './types.js';
|
||||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||||
|
|
||||||
|
vi.mock('node:fs');
|
||||||
|
|
||||||
describe('permissionsCommand', () => {
|
describe('permissionsCommand', () => {
|
||||||
let mockContext: CommandContext;
|
let mockContext: CommandContext;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = createMockCommandContext();
|
mockContext = createMockCommandContext();
|
||||||
|
vi.mocked(fs).statSync.mockReturnValue({
|
||||||
|
isDirectory: vi.fn(() => true),
|
||||||
|
} as unknown as fs.Stats);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have the correct name and description', () => {
|
it('should have the correct name and description', () => {
|
||||||
expect(permissionsCommand.name).toBe('permissions');
|
expect(permissionsCommand.name).toBe('permissions');
|
||||||
expect(permissionsCommand.description).toBe('Manage folder trust settings');
|
expect(permissionsCommand.description).toBe(
|
||||||
|
'Manage folder trust settings and other permissions',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be a built-in command', () => {
|
it('should be a built-in command', () => {
|
||||||
expect(permissionsCommand.kind).toBe(CommandKind.BUILT_IN);
|
expect(permissionsCommand.kind).toBe(CommandKind.BUILT_IN);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an action to open the permissions dialog', () => {
|
it('should have a trust subcommand', () => {
|
||||||
const actionResult = permissionsCommand.action?.(mockContext, '');
|
const trustCommand = permissionsCommand.subCommands?.find(
|
||||||
|
(cmd) => cmd.name === 'trust',
|
||||||
|
);
|
||||||
|
expect(trustCommand).toBeDefined();
|
||||||
|
expect(trustCommand?.name).toBe('trust');
|
||||||
|
expect(trustCommand?.description).toBe(
|
||||||
|
'Manage folder trust settings. Usage: /permissions trust [<directory-path>]',
|
||||||
|
);
|
||||||
|
expect(trustCommand?.kind).toBe(CommandKind.BUILT_IN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an action to open the permissions dialog with a specified directory', () => {
|
||||||
|
const trustCommand = permissionsCommand.subCommands?.find(
|
||||||
|
(cmd) => cmd.name === 'trust',
|
||||||
|
);
|
||||||
|
const actionResult = trustCommand?.action?.(mockContext, '/test/dir');
|
||||||
expect(actionResult).toEqual({
|
expect(actionResult).toEqual({
|
||||||
type: 'dialog',
|
type: 'dialog',
|
||||||
dialog: 'permissions',
|
dialog: 'permissions',
|
||||||
|
props: {
|
||||||
|
targetDirectory: path.resolve('/test/dir'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an action to open the permissions dialog with the current directory if no path is provided', () => {
|
||||||
|
const trustCommand = permissionsCommand.subCommands?.find(
|
||||||
|
(cmd) => cmd.name === 'trust',
|
||||||
|
);
|
||||||
|
const actionResult = trustCommand?.action?.(mockContext, '');
|
||||||
|
expect(actionResult).toEqual({
|
||||||
|
type: 'dialog',
|
||||||
|
dialog: 'permissions',
|
||||||
|
props: {
|
||||||
|
targetDirectory: process.cwd(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error message if the provided path does not exist', () => {
|
||||||
|
const trustCommand = permissionsCommand.subCommands?.find(
|
||||||
|
(cmd) => cmd.name === 'trust',
|
||||||
|
);
|
||||||
|
vi.mocked(fs).statSync.mockImplementation(() => {
|
||||||
|
throw new Error('ENOENT: no such file or directory');
|
||||||
|
});
|
||||||
|
const actionResult = trustCommand?.action?.(
|
||||||
|
mockContext,
|
||||||
|
'/nonexistent/dir',
|
||||||
|
);
|
||||||
|
expect(actionResult).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Error accessing path: ${path.resolve(
|
||||||
|
'/nonexistent/dir',
|
||||||
|
)}. ENOENT: no such file or directory`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error message if the provided path is not a directory', () => {
|
||||||
|
const trustCommand = permissionsCommand.subCommands?.find(
|
||||||
|
(cmd) => cmd.name === 'trust',
|
||||||
|
);
|
||||||
|
vi.mocked(fs).statSync.mockReturnValue({
|
||||||
|
isDirectory: vi.fn(() => false),
|
||||||
|
} as unknown as fs.Stats);
|
||||||
|
const actionResult = trustCommand?.action?.(mockContext, '/file/not/dir');
|
||||||
|
expect(actionResult).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Path is not a directory: ${path.resolve('/file/not/dir')}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,15 +4,80 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { OpenDialogActionReturn, SlashCommand } from './types.js';
|
import type {
|
||||||
|
OpenDialogActionReturn,
|
||||||
|
SlashCommand,
|
||||||
|
SlashCommandActionReturn,
|
||||||
|
} from './types.js';
|
||||||
import { CommandKind } from './types.js';
|
import { CommandKind } from './types.js';
|
||||||
|
import * as process from 'node:process';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import { expandHomeDir } from '../utils/directoryUtils.js';
|
||||||
|
|
||||||
export const permissionsCommand: SlashCommand = {
|
export const permissionsCommand: SlashCommand = {
|
||||||
name: 'permissions',
|
name: 'permissions',
|
||||||
description: 'Manage folder trust settings',
|
description: 'Manage folder trust settings and other permissions',
|
||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
action: (): OpenDialogActionReturn => ({
|
subCommands: [
|
||||||
|
{
|
||||||
|
name: 'trust',
|
||||||
|
description:
|
||||||
|
'Manage folder trust settings. Usage: /permissions trust [<directory-path>]',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
action: (context, input): SlashCommandActionReturn => {
|
||||||
|
const dirPath = input.trim();
|
||||||
|
let targetDirectory: string;
|
||||||
|
|
||||||
|
if (!dirPath) {
|
||||||
|
targetDirectory = process.cwd();
|
||||||
|
} else {
|
||||||
|
targetDirectory = path.resolve(expandHomeDir(dirPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.statSync(targetDirectory).isDirectory()) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Path is not a directory: ${targetDirectory}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Error accessing path: ${targetDirectory}. ${message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
type: 'dialog',
|
type: 'dialog',
|
||||||
dialog: 'permissions',
|
dialog: 'permissions',
|
||||||
}),
|
props: {
|
||||||
|
targetDirectory,
|
||||||
|
},
|
||||||
|
} as OpenDialogActionReturn;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
action: (context, input): SlashCommandActionReturn => {
|
||||||
|
const parts = input.trim().split(' ');
|
||||||
|
const subcommand = parts[0];
|
||||||
|
|
||||||
|
if (!subcommand) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Please provide a subcommand for /permissions. Usage: /permissions trust [<directory-path>]`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Invalid subcommand for /permissions: ${subcommand}. Usage: /permissions trust [<directory-path>]`,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export interface MessageActionReturn {
|
|||||||
*/
|
*/
|
||||||
export interface OpenDialogActionReturn {
|
export interface OpenDialogActionReturn {
|
||||||
type: 'dialog';
|
type: 'dialog';
|
||||||
|
props?: Record<string, unknown>;
|
||||||
|
|
||||||
dialog:
|
dialog:
|
||||||
| 'help'
|
| 'help'
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export const DialogManager = ({
|
|||||||
<PermissionsModifyTrustDialog
|
<PermissionsModifyTrustDialog
|
||||||
onExit={uiActions.closePermissionsDialog}
|
onExit={uiActions.closePermissionsDialog}
|
||||||
addItem={addItem}
|
addItem={addItem}
|
||||||
|
targetDirectory={uiState.permissionsDialogProps?.targetDirectory}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
|||||||
import { relaunchApp } from '../../utils/processUtils.js';
|
import { relaunchApp } from '../../utils/processUtils.js';
|
||||||
import { type UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
|
import { type UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
|
||||||
|
|
||||||
interface PermissionsModifyTrustDialogProps {
|
export interface PermissionsDialogProps {
|
||||||
|
targetDirectory?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionsModifyTrustDialogProps extends PermissionsDialogProps {
|
||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
addItem: UseHistoryManagerReturn['addItem'];
|
addItem: UseHistoryManagerReturn['addItem'];
|
||||||
}
|
}
|
||||||
@@ -24,9 +28,11 @@ interface PermissionsModifyTrustDialogProps {
|
|||||||
export function PermissionsModifyTrustDialog({
|
export function PermissionsModifyTrustDialog({
|
||||||
onExit,
|
onExit,
|
||||||
addItem,
|
addItem,
|
||||||
|
targetDirectory,
|
||||||
}: PermissionsModifyTrustDialogProps): React.JSX.Element {
|
}: PermissionsModifyTrustDialogProps): React.JSX.Element {
|
||||||
const dirName = path.basename(process.cwd());
|
const currentDirectory = targetDirectory ?? process.cwd();
|
||||||
const parentFolder = path.basename(path.dirname(process.cwd()));
|
const dirName = path.basename(currentDirectory);
|
||||||
|
const parentFolder = path.basename(path.dirname(currentDirectory));
|
||||||
|
|
||||||
const TRUST_LEVEL_ITEMS = [
|
const TRUST_LEVEL_ITEMS = [
|
||||||
{
|
{
|
||||||
@@ -54,7 +60,7 @@ export function PermissionsModifyTrustDialog({
|
|||||||
needsRestart,
|
needsRestart,
|
||||||
updateTrustLevel,
|
updateTrustLevel,
|
||||||
commitTrustLevelChange,
|
commitTrustLevelChange,
|
||||||
} = usePermissionsModifyTrust(onExit, addItem);
|
} = usePermissionsModifyTrust(onExit, addItem, currentDirectory);
|
||||||
|
|
||||||
useKeypress(
|
useKeypress(
|
||||||
(key) => {
|
(key) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { type FolderTrustChoice } from '../components/FolderTrustDialog.js';
|
|||||||
import { type AuthType, type EditorType } from '@google/gemini-cli-core';
|
import { type AuthType, type EditorType } from '@google/gemini-cli-core';
|
||||||
import { type LoadableSettingScope } from '../../config/settings.js';
|
import { type LoadableSettingScope } from '../../config/settings.js';
|
||||||
import type { AuthState } from '../types.js';
|
import type { AuthState } from '../types.js';
|
||||||
|
import { type PermissionsDialogProps } from '../components/PermissionsModifyTrustDialog.js';
|
||||||
|
|
||||||
export interface UIActions {
|
export interface UIActions {
|
||||||
handleThemeSelect: (themeName: string, scope: LoadableSettingScope) => void;
|
handleThemeSelect: (themeName: string, scope: LoadableSettingScope) => void;
|
||||||
@@ -30,6 +31,7 @@ export interface UIActions {
|
|||||||
exitPrivacyNotice: () => void;
|
exitPrivacyNotice: () => void;
|
||||||
closeSettingsDialog: () => void;
|
closeSettingsDialog: () => void;
|
||||||
closeModelDialog: () => void;
|
closeModelDialog: () => void;
|
||||||
|
openPermissionsDialog: (props?: PermissionsDialogProps) => void;
|
||||||
closePermissionsDialog: () => void;
|
closePermissionsDialog: () => void;
|
||||||
setShellModeActive: (value: boolean) => void;
|
setShellModeActive: (value: boolean) => void;
|
||||||
vimHandleInput: (key: Key) => boolean;
|
vimHandleInput: (key: Key) => boolean;
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface UIState {
|
|||||||
isSettingsDialogOpen: boolean;
|
isSettingsDialogOpen: boolean;
|
||||||
isModelDialogOpen: boolean;
|
isModelDialogOpen: boolean;
|
||||||
isPermissionsDialogOpen: boolean;
|
isPermissionsDialogOpen: boolean;
|
||||||
|
permissionsDialogProps: { targetDirectory?: string } | null;
|
||||||
slashCommands: readonly SlashCommand[] | undefined;
|
slashCommands: readonly SlashCommand[] | undefined;
|
||||||
pendingSlashCommandHistoryItems: HistoryItemWithoutId[];
|
pendingSlashCommandHistoryItems: HistoryItemWithoutId[];
|
||||||
commandContext: CommandContext;
|
commandContext: CommandContext;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ interface SlashCommandProcessorActions {
|
|||||||
openPrivacyNotice: () => void;
|
openPrivacyNotice: () => void;
|
||||||
openSettingsDialog: () => void;
|
openSettingsDialog: () => void;
|
||||||
openModelDialog: () => void;
|
openModelDialog: () => void;
|
||||||
openPermissionsDialog: () => void;
|
openPermissionsDialog: (props?: { targetDirectory?: string }) => void;
|
||||||
quit: (messages: HistoryItem[]) => void;
|
quit: (messages: HistoryItem[]) => void;
|
||||||
setDebugMessage: (message: string) => void;
|
setDebugMessage: (message: string) => void;
|
||||||
toggleCorgiMode: () => void;
|
toggleCorgiMode: () => void;
|
||||||
@@ -405,7 +405,9 @@ export const useSlashCommandProcessor = (
|
|||||||
actions.openModelDialog();
|
actions.openModelDialog();
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
case 'permissions':
|
case 'permissions':
|
||||||
actions.openPermissionsDialog();
|
actions.openPermissionsDialog(
|
||||||
|
result.props as { targetDirectory?: string },
|
||||||
|
);
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
case 'help':
|
case 'help':
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ vi.mock('node:process', () => ({
|
|||||||
cwd: mockedCwd,
|
cwd: mockedCwd,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('node:path', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal();
|
||||||
|
return {
|
||||||
|
...(actual && typeof actual === 'object' ? actual : {}),
|
||||||
|
resolve: vi.fn((p) => p),
|
||||||
|
join: vi.fn((...args) => args.join('/')),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('../../config/trustedFolders.js', () => ({
|
vi.mock('../../config/trustedFolders.js', () => ({
|
||||||
loadTrustedFolders: mockedLoadTrustedFolders,
|
loadTrustedFolders: mockedLoadTrustedFolders,
|
||||||
isWorkspaceTrusted: mockedIsWorkspaceTrusted,
|
isWorkspaceTrusted: mockedIsWorkspaceTrusted,
|
||||||
@@ -74,6 +83,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when targetDirectory is the current workspace', () => {
|
||||||
it('should initialize with the correct trust level', () => {
|
it('should initialize with the correct trust level', () => {
|
||||||
mockedLoadTrustedFolders.mockReturnValue({
|
mockedLoadTrustedFolders.mockReturnValue({
|
||||||
user: { config: { '/test/dir': TrustLevel.TRUST_FOLDER } },
|
user: { config: { '/test/dir': TrustLevel.TRUST_FOLDER } },
|
||||||
@@ -84,7 +94,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.current.currentTrustLevel).toBe(TrustLevel.TRUST_FOLDER);
|
expect(result.current.currentTrustLevel).toBe(TrustLevel.TRUST_FOLDER);
|
||||||
@@ -101,7 +111,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.current.isInheritedTrustFromParent).toBe(true);
|
expect(result.current.isInheritedTrustFromParent).toBe(true);
|
||||||
@@ -118,7 +128,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.current.isInheritedTrustFromIde).toBe(true);
|
expect(result.current.isInheritedTrustFromIde).toBe(true);
|
||||||
@@ -137,7 +147,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
.mockReturnValueOnce({ isTrusted: true, source: 'file' });
|
.mockReturnValueOnce({ isTrusted: true, source: 'file' });
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -161,7 +171,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -188,7 +198,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
.mockReturnValueOnce({ isTrusted: true, source: 'file' });
|
.mockReturnValueOnce({ isTrusted: true, source: 'file' });
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -218,7 +228,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -245,7 +255,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -260,6 +270,76 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when targetDirectory is not the current workspace', () => {
|
||||||
|
const otherDirectory = '/other/dir';
|
||||||
|
|
||||||
|
it('should not detect inherited trust', () => {
|
||||||
|
mockedLoadTrustedFolders.mockReturnValue({
|
||||||
|
user: { config: {} },
|
||||||
|
} as unknown as LoadedTrustedFolders);
|
||||||
|
mockedIsWorkspaceTrusted.mockReturnValue({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, otherDirectory),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.isInheritedTrustFromParent).toBe(false);
|
||||||
|
expect(result.current.isInheritedTrustFromIde).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save immediately without needing a restart', () => {
|
||||||
|
const mockSetValue = vi.fn();
|
||||||
|
mockedLoadTrustedFolders.mockReturnValue({
|
||||||
|
user: { config: {} },
|
||||||
|
setValue: mockSetValue,
|
||||||
|
} as unknown as LoadedTrustedFolders);
|
||||||
|
mockedIsWorkspaceTrusted.mockReturnValue({
|
||||||
|
isTrusted: false,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, otherDirectory),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.needsRestart).toBe(false);
|
||||||
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
|
otherDirectory,
|
||||||
|
TrustLevel.TRUST_FOLDER,
|
||||||
|
);
|
||||||
|
expect(mockOnExit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add a warning when setting DO_NOT_TRUST', () => {
|
||||||
|
mockedLoadTrustedFolders.mockReturnValue({
|
||||||
|
user: { config: {} },
|
||||||
|
setValue: vi.fn(),
|
||||||
|
} as unknown as LoadedTrustedFolders);
|
||||||
|
mockedIsWorkspaceTrusted.mockReturnValue({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, otherDirectory),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateTrustLevel(TrustLevel.DO_NOT_TRUST);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddItem).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should emit feedback when setValue throws in updateTrustLevel', () => {
|
it('should emit feedback when setValue throws in updateTrustLevel', () => {
|
||||||
const mockSetValue = vi.fn().mockImplementation(() => {
|
const mockSetValue = vi.fn().mockImplementation(() => {
|
||||||
@@ -278,7 +358,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -308,7 +388,7 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import * as process from 'node:process';
|
import * as process from 'node:process';
|
||||||
|
import * as path from 'node:path';
|
||||||
import {
|
import {
|
||||||
loadTrustedFolders,
|
loadTrustedFolders,
|
||||||
TrustLevel,
|
TrustLevel,
|
||||||
@@ -27,9 +28,19 @@ interface TrustState {
|
|||||||
function getInitialTrustState(
|
function getInitialTrustState(
|
||||||
settings: LoadedSettings,
|
settings: LoadedSettings,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
|
isCurrentWorkspace: boolean,
|
||||||
): TrustState {
|
): TrustState {
|
||||||
const folders = loadTrustedFolders();
|
const folders = loadTrustedFolders();
|
||||||
const explicitTrustLevel = folders.user.config[cwd];
|
const explicitTrustLevel = folders.user.config[cwd];
|
||||||
|
|
||||||
|
if (!isCurrentWorkspace) {
|
||||||
|
return {
|
||||||
|
currentTrustLevel: explicitTrustLevel,
|
||||||
|
isInheritedTrustFromParent: false,
|
||||||
|
isInheritedTrustFromIde: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { isTrusted, source } = isWorkspaceTrusted(settings.merged);
|
const { isTrusted, source } = isWorkspaceTrusted(settings.merged);
|
||||||
|
|
||||||
const isInheritedTrust =
|
const isInheritedTrust =
|
||||||
@@ -46,11 +57,19 @@ function getInitialTrustState(
|
|||||||
export const usePermissionsModifyTrust = (
|
export const usePermissionsModifyTrust = (
|
||||||
onExit: () => void,
|
onExit: () => void,
|
||||||
addItem: UseHistoryManagerReturn['addItem'],
|
addItem: UseHistoryManagerReturn['addItem'],
|
||||||
|
targetDirectory: string,
|
||||||
) => {
|
) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const cwd = process.cwd();
|
const cwd = targetDirectory;
|
||||||
|
// Normalize paths for case-insensitive file systems (macOS/Windows) to ensure
|
||||||
|
// accurate comparison between targetDirectory and process.cwd().
|
||||||
|
const isCurrentWorkspace =
|
||||||
|
path.resolve(targetDirectory).toLowerCase() ===
|
||||||
|
path.resolve(process.cwd()).toLowerCase();
|
||||||
|
|
||||||
const [initialState] = useState(() => getInitialTrustState(settings, cwd));
|
const [initialState] = useState(() =>
|
||||||
|
getInitialTrustState(settings, cwd, isCurrentWorkspace),
|
||||||
|
);
|
||||||
|
|
||||||
const [currentTrustLevel] = useState<TrustLevel | undefined>(
|
const [currentTrustLevel] = useState<TrustLevel | undefined>(
|
||||||
initialState.currentTrustLevel,
|
initialState.currentTrustLevel,
|
||||||
@@ -70,6 +89,16 @@ export const usePermissionsModifyTrust = (
|
|||||||
|
|
||||||
const updateTrustLevel = useCallback(
|
const updateTrustLevel = useCallback(
|
||||||
(trustLevel: TrustLevel) => {
|
(trustLevel: TrustLevel) => {
|
||||||
|
// If we are not editing the current workspace, the logic is simple:
|
||||||
|
// just save the setting and exit. No restart or warnings are needed.
|
||||||
|
if (!isCurrentWorkspace) {
|
||||||
|
const folders = loadTrustedFolders();
|
||||||
|
folders.setValue(cwd, trustLevel);
|
||||||
|
onExit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All logic below only applies when editing the current workspace.
|
||||||
const wasTrusted = isWorkspaceTrusted(settings.merged).isTrusted;
|
const wasTrusted = isWorkspaceTrusted(settings.merged).isTrusted;
|
||||||
|
|
||||||
// Create a temporary config to check the new trust status without writing
|
// Create a temporary config to check the new trust status without writing
|
||||||
@@ -113,7 +142,7 @@ export const usePermissionsModifyTrust = (
|
|||||||
onExit();
|
onExit();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[cwd, settings.merged, onExit, addItem],
|
[cwd, settings.merged, onExit, addItem, isCurrentWorkspace],
|
||||||
);
|
);
|
||||||
|
|
||||||
const commitTrustLevelChange = useCallback(() => {
|
const commitTrustLevelChange = useCallback(() => {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
|
export function expandHomeDir(p: string): string {
|
||||||
|
if (!p) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let expandedPath = p;
|
||||||
|
if (p.toLowerCase().startsWith('%userprofile%')) {
|
||||||
|
expandedPath = os.homedir() + p.substring('%userprofile%'.length);
|
||||||
|
} else if (p === '~' || p.startsWith('~/')) {
|
||||||
|
expandedPath = os.homedir() + p.substring(1);
|
||||||
|
}
|
||||||
|
return path.normalize(expandedPath);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user