mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 12:46:58 +10:00
164 lines
22 KiB
TypeScript
164 lines
22 KiB
TypeScript
|
|
import { basename, sep } from 'path';
|
||
|
|
import React, { type ReactNode } from 'react';
|
||
|
|
import { getOriginalCwd } from '../../bootstrap/state.js';
|
||
|
|
import { Text } from '../../ink.js';
|
||
|
|
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js';
|
||
|
|
import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js';
|
||
|
|
function commandListDisplay(commands: string[]): ReactNode {
|
||
|
|
switch (commands.length) {
|
||
|
|
case 0:
|
||
|
|
return '';
|
||
|
|
case 1:
|
||
|
|
return <Text bold>{commands[0]}</Text>;
|
||
|
|
case 2:
|
||
|
|
return <Text>
|
||
|
|
<Text bold>{commands[0]}</Text> and <Text bold>{commands[1]}</Text>
|
||
|
|
</Text>;
|
||
|
|
default:
|
||
|
|
return <Text>
|
||
|
|
<Text bold>{commands.slice(0, -1).join(', ')}</Text>, and{' '}
|
||
|
|
<Text bold>{commands.slice(-1)[0]}</Text>
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function commandListDisplayTruncated(commands: string[]): ReactNode {
|
||
|
|
// Check if the plain text representation would be too long
|
||
|
|
const plainText = commands.join(', ');
|
||
|
|
if (plainText.length > 50) {
|
||
|
|
return 'similar';
|
||
|
|
}
|
||
|
|
return commandListDisplay(commands);
|
||
|
|
}
|
||
|
|
function formatPathList(paths: string[]): ReactNode {
|
||
|
|
if (paths.length === 0) return '';
|
||
|
|
|
||
|
|
// Extract directory names from paths
|
||
|
|
const names = paths.map(p => basename(p) || p);
|
||
|
|
if (names.length === 1) {
|
||
|
|
return <Text>
|
||
|
|
<Text bold>{names[0]}</Text>
|
||
|
|
{sep}
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
if (names.length === 2) {
|
||
|
|
return <Text>
|
||
|
|
<Text bold>{names[0]}</Text>
|
||
|
|
{sep} and <Text bold>{names[1]}</Text>
|
||
|
|
{sep}
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// For 3+, show first two with "and N more"
|
||
|
|
return <Text>
|
||
|
|
<Text bold>{names[0]}</Text>
|
||
|
|
{sep}, <Text bold>{names[1]}</Text>
|
||
|
|
{sep} and {paths.length - 2} more
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate the label for the "Yes, and apply suggestions" option in shell
|
||
|
|
* permission dialogs (Bash, PowerShell). Parametrized by the shell tool name
|
||
|
|
* and an optional command transform (e.g., Bash strips output redirections so
|
||
|
|
* filenames don't show as commands).
|
||
|
|
*/
|
||
|
|
export function generateShellSuggestionsLabel(suggestions: PermissionUpdate[], shellToolName: string, commandTransform?: (command: string) => string): ReactNode | null {
|
||
|
|
// Collect all rules for display
|
||
|
|
const allRules = suggestions.filter(s => s.type === 'addRules').flatMap(s => s.rules || []);
|
||
|
|
|
||
|
|
// Separate Read rules from shell rules
|
||
|
|
const readRules = allRules.filter(r => r.toolName === 'Read');
|
||
|
|
const shellRules = allRules.filter(r => r.toolName === shellToolName);
|
||
|
|
|
||
|
|
// Get directory info
|
||
|
|
const directories = suggestions.filter(s => s.type === 'addDirectories').flatMap(s => s.directories || []);
|
||
|
|
|
||
|
|
// Extract paths from Read rules (keep separate from directories)
|
||
|
|
const readPaths = readRules.map(r => r.ruleContent?.replace('/**', '') || '').filter(p => p);
|
||
|
|
|
||
|
|
// Extract shell command prefixes, optionally transforming for display
|
||
|
|
const shellCommands = [...new Set(shellRules.flatMap(rule => {
|
||
|
|
if (!rule.ruleContent) return [];
|
||
|
|
const command = permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent;
|
||
|
|
return commandTransform ? commandTransform(command) : command;
|
||
|
|
}))];
|
||
|
|
|
||
|
|
// Check what we have
|
||
|
|
const hasDirectories = directories.length > 0;
|
||
|
|
const hasReadPaths = readPaths.length > 0;
|
||
|
|
const hasCommands = shellCommands.length > 0;
|
||
|
|
|
||
|
|
// Handle single type cases
|
||
|
|
if (hasReadPaths && !hasDirectories && !hasCommands) {
|
||
|
|
// Only Read rules - use "reading from" language
|
||
|
|
if (readPaths.length === 1) {
|
||
|
|
const firstPath = readPaths[0]!;
|
||
|
|
const dirName = basename(firstPath) || firstPath;
|
||
|
|
return <Text>
|
||
|
|
Yes, allow reading from <Text bold>{dirName}</Text>
|
||
|
|
{sep} from this project
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Multiple read paths
|
||
|
|
return <Text>
|
||
|
|
Yes, allow reading from {formatPathList(readPaths)} from this project
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
if (hasDirectories && !hasReadPaths && !hasCommands) {
|
||
|
|
// Only directory permissions - use "access to" language
|
||
|
|
if (directories.length === 1) {
|
||
|
|
const firstDir = directories[0]!;
|
||
|
|
const dirName = basename(firstDir) || firstDir;
|
||
|
|
return <Text>
|
||
|
|
Yes, and always allow access to <Text bold>{dirName}</Text>
|
||
|
|
{sep} from this project
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Multiple directories
|
||
|
|
return <Text>
|
||
|
|
Yes, and always allow access to {formatPathList(directories)} from this
|
||
|
|
project
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
if (hasCommands && !hasDirectories && !hasReadPaths) {
|
||
|
|
// Only shell command permissions
|
||
|
|
return <Text>
|
||
|
|
{"Yes, and don't ask again for "}
|
||
|
|
{commandListDisplayTruncated(shellCommands)} commands in{' '}
|
||
|
|
<Text bold>{getOriginalCwd()}</Text>
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle mixed cases
|
||
|
|
if ((hasDirectories || hasReadPaths) && !hasCommands) {
|
||
|
|
// Combine directories and read paths since they're both path access
|
||
|
|
const allPaths = [...directories, ...readPaths];
|
||
|
|
if (hasDirectories && hasReadPaths) {
|
||
|
|
// Mixed - use generic "access to"
|
||
|
|
return <Text>
|
||
|
|
Yes, and always allow access to {formatPathList(allPaths)} from this
|
||
|
|
project
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ((hasDirectories || hasReadPaths) && hasCommands) {
|
||
|
|
// Build descriptive message for both types
|
||
|
|
const allPaths = [...directories, ...readPaths];
|
||
|
|
|
||
|
|
// Keep it concise but informative
|
||
|
|
if (allPaths.length === 1 && shellCommands.length === 1) {
|
||
|
|
return <Text>
|
||
|
|
Yes, and allow access to {formatPathList(allPaths)} and{' '}
|
||
|
|
{commandListDisplayTruncated(shellCommands)} commands
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
return <Text>
|
||
|
|
Yes, and allow {formatPathList(allPaths)} access and{' '}
|
||
|
|
{commandListDisplayTruncated(shellCommands)} commands
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXNlbmFtZSIsInNlcCIsIlJlYWN0IiwiUmVhY3ROb2RlIiwiZ2V0T3JpZ2luYWxDd2QiLCJUZXh0IiwiUGVybWlzc2lvblVwZGF0ZSIsInBlcm1pc3Npb25SdWxlRXh0cmFjdFByZWZpeCIsImNvbW1hbmRMaXN0RGlzcGxheSIsImNvbW1hbmRzIiwibGVuZ3RoIiwic2xpY2UiLCJqb2luIiwiY29tbWFuZExpc3REaXNwbGF5VHJ1bmNhdGVkIiwicGxhaW5UZXh0IiwiZm9ybWF0UGF0aExpc3QiLCJwYXRocyIsIm5hbWVzIiwibWFwIiwicCIsImdlbmVyYXRlU2hlbGxTdWdnZXN0aW9uc0xhYmVsIiwic3VnZ2VzdGlvbnMiLCJzaGVsbFRvb2xOYW1lIiwiY29tbWFuZFRyYW5zZm9ybSIsImNvbW1hbmQiLCJhbGxSdWxlcyIsImZpbHRlciIsInMiLCJ0eXBlIiwiZmxhdE1hcCIsInJ1bGVzIiwicmVhZFJ1bGVzIiwiciIsInRvb2xOYW1lIiwic2hlbGxSdWxlcyIsImRpcmVjdG9yaWVzIiwicmVhZFBhdGhzIiwicnVsZUNvbnRlbnQiLCJyZXBsYWNlIiwic2hlbGxDb21tYW5kcyIsIlNldCIsInJ1bGUiLCJoYXNEaXJlY3RvcmllcyIsImhhc1JlYWRQYXRocyIsImhhc0NvbW1hbmRzIiwiZmlyc3RQYXRoIiwiZGlyTmFtZSIsImZpcnN0RGlyIiwiYWxsUGF0aHMiXSwic291cmNlcyI6WyJzaGVsbFBlcm1pc3Npb25IZWxwZXJzLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBiYXNlbmFtZSwgc2VwIH0gZnJvbSAncGF0aCdcbmltcG9ydCBSZWFjdCwgeyB0eXBlIFJlYWN0Tm9kZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgZ2V0T3JpZ2luYWxDd2QgfSBmcm9tICcuLi8uLi9ib290c3RyYXAvc3RhdGUuanMnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBQZXJtaXNzaW9uVXBkYXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvUGVybWlzc2lvblVwZGF0ZVNjaGVtYS5qcydcbmltcG9ydCB7IHBlcm1pc3Npb25SdWxlRXh0cmFjdFByZWZpeCB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL3NoZWxsUnVsZU1hdGNoaW5nLmpzJ1xuXG5mdW5jdGlvbiBjb21tYW5kTGlzdERpc3BsYXkoY29tbWFuZHM6IHN0cmluZ1tdKTogUmVhY3ROb2RlIHtcbiAgc3dpdGNoIChjb21tYW5kcy5sZW5ndGgpIHtcbiAgICBjYXNlIDA6XG4gICAgICByZXR1cm4gJydcbiAgICBjYXNlIDE6XG4gICAgICByZXR1cm4gPFRleHQgYm9sZD57Y29tbWFuZHNbMF19PC9UZXh0PlxuICAgIGNhc2UgMjpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxUZXh0PlxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2NvbW1hbmRzWzBdfTwvVGV4dD4gYW5kIDxUZXh0IGJvbGQ+e2NvbW1hbmRzWzFdfTwvVGV4dD5cbiAgICAgICAgPC9UZXh0PlxuICAgICAgKVxuICAgIGRlZmF1bHQ6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8VGV4dD5cbiAgICAgICAgICA8VGV4dCBib2xkPntjb21tYW5kcy5zbGljZSgwLCAtMSkuam9pbignLCAnKX08L1RleHQ+LCBhbmR7JyAnfVxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2NvbW1hbmRzLnNsaWNlKC0xKVswXX08L1RleHQ+XG4gICAgICAgIDwvVGV4dD5cbiAgICAgIClcbiAgfVxufVxuXG5mdW5jdGlvbiBjb21tYW5kTGlzdERpc3BsYXlUcnVuY2F0ZWQoY29tbWFuZHM6IHN0cmluZ1tdKTogUmVhY3ROb2RlIHtcbiAgLy8gQ2hlY2sgaWYgdGhlIHBsYWluIHRleHQgcmVwcmVzZW50YXRpb24gd291bGQgYmUgdG9vIGxvbmdcbiAgY29uc3QgcGxhaW5UZXh0ID0gY29tbWFuZHMuam9pbignLCAnKVxuICBpZiAocGxhaW5UZXh0Lmxlbmd0aCA+IDUwKSB7XG4gICAgcmV0dXJuICdzaW1pbGFyJ1xuICB9XG4gIHJldHVybiBjb21tYW5kTGlzdERpc3BsYXkoY29tbWFuZHMpXG59XG5cbmZ1bmN0aW9uIGZvcm1hdFBhdGhMaXN0KHBhdGhzOiBzdHJpbmdbXSk6IFJlYWN0Tm9kZSB7XG4gIGlmIChwYXRocy5sZW5ndGggPT09IDApIHJldHVybiAnJ1xuXG4gIC8vIEV4dHJhY3QgZGlyZWN0b3J5IG5hbWVzIGZyb20gcGF0aHNcbiAgY29uc3QgbmFtZXMgPSBwYXRocy5tYXAocCA9PiBiYXNlbmFtZShwKSB8fCBwKVxuXG4gIGlmIChuYW1lcy5sZW5ndGggPT09IDEpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPFRleHQ+XG4gICAgICAgIDxUZXh0IGJvbGQ+e25hbWVzWzBdfTwvVGV4dD5cbiAgICAgICAge3NlcH1cbiAgICAgIDwvVGV4dD5cbiAgICApXG4gIH1cbiAgaWYgKG5hbWVzLmxlbmd0aCA9PT0gMikge1xuICAgIHJldHVybiAoXG4gICAgICA8VGV4dD5cbiAgICAgICAgPFRleHQgYm9sZD57bmFtZXNbMF19PC9UZXh0PlxuICAgICAgICB7c2VwfSBhbmQgPFRleHQgYm9sZD57bmFtZXNbMV19PC9UZXh0PlxuICAgICAgICB7c2VwfVxuICAgICAgPC9UZXh0PlxuICAgIClcbiAgfVxuXG4gIC8vIEZvciAzKywgc2hvdyBmaXJzdCB0d28gd2l0aCBcImFuZCBOIG1vcmVcIlxuICByZXR1cm4gKFxuICAgIDxUZXh0PlxuICAgICAgPFRleHQgYm9sZD57bmFtZXNbMF19PC9UZXh0PlxuICAgICAge3NlcH0sIDxUZXh0IGJvbGQ+e25hbWVzWzFdfTwvVGV4dD5cbiAgICAgIHtzZXB9IGFuZCB7cGF0aHMubGVuZ3RoIC0gMn0gbW9yZVxuICAgIDwvVGV4dD5cbiAgKVxufVxuXG4vKipcbiAqIEdlbmVyYXRlIHRoZSBsYWJlbCBmb3IgdGhlIFwiWWVzLCBhbmQgYXBwbHkgc3VnZ2VzdGlvbnNcIiBvcHRpb24gaW4gc2hlbGxcbiAqIHBlcm1pc3Npb24gZGlhbG9ncyAoQmFzaCwgUG93ZXJTaGVsbCkuIFBhcmFtZXRyaXplZCBieSB0aGUgc2hlbGwgdG9vbCBuYW1lXG4gKiBhbmQgYW4gb3B0aW9uYWwgY29tbWFuZCB0cmFuc2Zvcm0gKGUuZy4sIEJhc2ggc3RyaXBzIG91dHB1dCByZWRpcmVjdGlvbnMgc29cbiAqIGZpbGVuYW1lcyBkb24ndCBzaG93IGFzIGNvbW1hbmRzKS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGdlbmVyYXRlU2hlbGxTdWdnZXN0aW9uc0xhYmVsKFxuICBzdWdnZXN0aW9uczogUGVybWlzc2lvblVwZGF0ZVtdLFxuICBzaGVsbFRvb2xOYW1lOiBzdHJpbmcsXG4gIGNvbW1hbmRUcmFuc2Zvcm0/OiAoY29tbWFuZDogc3R
|